Skip to content

Commit

Permalink
Merge pull request #72 from yumemi-inc/feature/apply_api_guideline
Browse files Browse the repository at this point in the history
既存コードをAPI Guidelineに準拠させる
  • Loading branch information
es-kumagai authored May 23, 2024
2 parents 1a58394 + 95aa9c9 commit 55bdcfb
Show file tree
Hide file tree
Showing 13 changed files with 567 additions and 300 deletions.
3 changes: 3 additions & 0 deletions Documentation/ThreadBlock.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Sync ver
```swift
static func syncFetchWeather(_ jsonString: String) throws -> String
```

このメソッドは、値を返すまでに少し時間がかかります。

[APIの概要](YumemiWeather.md)

## 課題
Expand Down
6 changes: 3 additions & 3 deletions Example/Example/Model/DisasterModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ class DisasterModelImpl: DisasterModel {
}

func fetchDisaster(completion: ((String) -> Void)?) {
self.fetchDisasterHandler = completion
self.yumemiDisaster.fetchDisaster()
fetchDisasterHandler = completion
yumemiDisaster.fetchDisaster()
}
}

extension DisasterModelImpl: YumemiDisasterHandleDelegate {

func handle(disaster: String) {
self.fetchDisasterHandler?(disaster)
fetchDisasterHandler?(disaster)
}

}
26 changes: 13 additions & 13 deletions Example/Example/UI/Weather/WeatherViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class WeatherViewController: UIViewController {
super.viewDidLoad()

NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [unowned self] notification in
self.loadWeather(notification.object)
loadWeather(notification.object)
}
}

Expand All @@ -39,11 +39,11 @@ class WeatherViewController: UIViewController {
}

@IBAction func dismiss(_ sender: Any) {
self.dismiss(animated: true, completion: nil)
dismiss(animated: true, completion: nil)
}

@IBAction func loadWeather(_ sender: Any?) {
self.activityIndicator.startAnimating()
activityIndicator.startAnimating()
weatherModel.fetchWeather(at: "tokyo", date: Date()) { result in
DispatchQueue.main.async {
self.activityIndicator.stopAnimating()
Expand All @@ -58,9 +58,9 @@ class WeatherViewController: UIViewController {
func handleWeather(result: Result<Response, WeatherError>) {
switch result {
case .success(let response):
self.weatherImageView.set(weather: response.weather)
self.minTempLabel.text = String(response.minTemp)
self.maxTempLabel.text = String(response.maxTemp)
weatherImageView.set(weather: response.weather)
minTempLabel.text = String(response.minTemp)
maxTempLabel.text = String(response.maxTemp)

case .failure(let error):
let message: String
Expand All @@ -79,7 +79,7 @@ class WeatherViewController: UIViewController {
print("Close ViewController by \(alertController)")
}
})
self.present(alertController, animated: true, completion: nil)
present(alertController, animated: true, completion: nil)
}
}
}
Expand All @@ -88,14 +88,14 @@ private extension UIImageView {
func set(weather: Weather) {
switch weather {
case .sunny:
self.image = R.image.sunny()
self.tintColor = R.color.red()
image = R.image.sunny()
tintColor = R.color.red()
case .cloudy:
self.image = R.image.cloudy()
self.tintColor = R.color.gray()
image = R.image.cloudy()
tintColor = R.color.gray()
case .rainy:
self.image = R.image.rainy()
self.tintColor = R.color.blue()
image = R.image.rainy()
tintColor = R.color.blue()
}
}
}
90 changes: 57 additions & 33 deletions Example/ExampleTests/WeatherViewControllerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,66 +12,90 @@ import YumemiWeather

class WeatherViewControllerTests: XCTestCase {

var weahterViewController: WeatherViewController!
var weahterModel: WeatherModelMock!
var weatherViewController: WeatherViewController!
var weatherModel = WeatherModelMock()

override func setUpWithError() throws {
weahterModel = WeatherModelMock()
weahterViewController = R.storyboard.weather.instantiateInitialViewController()!
weahterViewController.weatherModel = weahterModel
_ = weahterViewController.view
weatherViewController = R.storyboard.weather.instantiateInitialViewController()!
weatherViewController.weatherModel = weatherModel
weatherViewController.disasterModel = DisasterModelMock()

weatherViewController.loadViewIfNeeded()
}

override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}

func test_天気予報がsunnyだったらImageViewのImageにsunnyが設定されること_TintColorがredに設定されること() throws {
weahterModel.fetchWeatherImpl = { _ in
Response(weather: .sunny, maxTemp: 0, minTemp: 0, date: Date())
@MainActor
func test_天気予報がsunnyだったらImageViewのImageにsunnyが設定されること_TintColorがredに設定されること() async throws {
weatherModel.fetchWeatherImpl = { _ in
let response = Response(weather: .sunny, maxTemp: 0, minTemp: 0, date: Date())
return Result.success(response)
}

weahterViewController.loadWeather()
XCTAssertEqual(weahterViewController.weatherImageView.tintColor, R.color.red())
XCTAssertEqual(weahterViewController.weatherImageView.image, R.image.sunny())
weatherViewController.loadWeather(self)
await Task.yield()

XCTAssertEqual(weatherViewController.weatherImageView.tintColor, R.color.red())
XCTAssertEqual(weatherViewController.weatherImageView.image, R.image.sunny())
}

func test_天気予報がcloudyだったらImageViewのImageにcloudyが設定されること_TintColorがgrayに設定されること() throws {
weahterModel.fetchWeatherImpl = { _ in
Response(weather: .cloudy, maxTemp: 0, minTemp: 0, date: Date())
@MainActor
func test_天気予報がcloudyだったらImageViewのImageにcloudyが設定されること_TintColorがgrayに設定されること() async throws {
weatherModel.fetchWeatherImpl = { _ in
let response = Response(weather: .cloudy, maxTemp: 0, minTemp: 0, date: Date())
return Result.success(response)
}

weahterViewController.loadWeather()
XCTAssertEqual(weahterViewController.weatherImageView.tintColor, R.color.gray())
XCTAssertEqual(weahterViewController.weatherImageView.image, R.image.cloudy())
weatherViewController.loadWeather(self)
await Task.yield()

XCTAssertEqual(weatherViewController.weatherImageView.tintColor, R.color.gray())
XCTAssertEqual(weatherViewController.weatherImageView.image, R.image.cloudy())
}

func test_天気予報がrainyだったらImageViewのImageにrainyが設定されること_TintColorがblueに設定されること() throws {
weahterModel.fetchWeatherImpl = { _ in
Response(weather: .rainy, maxTemp: 0, minTemp: 0, date: Date())
@MainActor
func test_天気予報がrainyだったらImageViewのImageにrainyが設定されること_TintColorがblueに設定されること() async throws {
weatherModel.fetchWeatherImpl = { _ in
let response = Response(weather: .rainy, maxTemp: 0, minTemp: 0, date: Date())
return Result.success(response)
}

weahterViewController.loadWeather()
XCTAssertEqual(weahterViewController.weatherImageView.tintColor, R.color.blue())
XCTAssertEqual(weahterViewController.weatherImageView.image, R.image.rainy())
weatherViewController.loadWeather(self)
await Task.yield()

XCTAssertEqual(weatherViewController.weatherImageView.tintColor, R.color.blue())
XCTAssertEqual(weatherViewController.weatherImageView.image, R.image.rainy())
}

func test_最高気温_最低気温がUILabelに設定されること() throws {
weahterModel.fetchWeatherImpl = { _ in
Response(weather: .rainy, maxTemp: 100, minTemp: -100, date: Date())
@MainActor
func test_最高気温_最低気温がUILabelに設定されること() async throws {
weatherModel.fetchWeatherImpl = { _ in
let response = Response(weather: .rainy, maxTemp: 100, minTemp: -100, date: Date())
return Result.success(response)
}

weahterViewController.loadWeather()
XCTAssertEqual(weahterViewController.minTempLabel.text, "-100")
XCTAssertEqual(weahterViewController.maxTempLabel.text, "100")
weatherViewController.loadWeather(self)
await Task.yield()

XCTAssertEqual(weatherViewController.minTempLabel.text, "-100")
XCTAssertEqual(weatherViewController.maxTempLabel.text, "100")
}
}

class WeatherModelMock: WeatherModel {

var fetchWeatherImpl: ((Request) throws -> Response)!
var fetchWeatherImpl: ((Request) -> Result<Response, WeatherError>)!

func fetchWeather(_ request: Request) throws -> Response {
return try fetchWeatherImpl(request)
func fetchWeather(at area: String, date: Date, completion: @escaping (Result<Response, WeatherError>) -> Void) {
completion(fetchWeatherImpl(Request(area: area, date: date)))
}
}

final class DisasterModelMock: DisasterModel {

func fetchDisaster(completion: ((String) -> Void)?) {
completion?("只今、災害情報はありません。")
}
}
56 changes: 56 additions & 0 deletions Sources/YumemiWeather/ControllableGenerator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// ControllableGenerator.swift
//
//
// Created by 古宮 伸久 on 2022/04/08.
//

import Foundation

/// 制御可能な乱数生成器です。
struct ControllableGenerator {

/// 唯一のインスタンスを生成します。
///
/// このイニシャライザーは、複数回呼び出しても意味がありません。
/// 乱数生成方法の都合で、同じ振る舞いをするインスタンスになります。
private init() {
}
}

extension ControllableGenerator {

/// 唯一の、制御可能な乱数生成器インスタンスです。
static var shared = ControllableGenerator()

/// 乱数生成時に使用するシード値を `seed` でリセットします。
/// - Parameter seed: 新たに使用するシード値です。
static func reset(withSeed seed: Int) {
srand48(seed)
}

/// 乱数生成時に使用するシード値を `area` と `date` から算出してリセットします。
/// - Parameters:
/// - area: シード値の算出に使う、地域情報
/// - date: シード値の算出に使う、日付情報
static func resetUsing(area: Area, date: Date) {

var hasher = Hasher()

hasher.combine(area)
hasher.combine(date)

let seed = hasher.finalize()

reset(withSeed: seed)
}
}

extension ControllableGenerator : RandomNumberGenerator {

/// 次の乱数を生成します。
/// - Returns: 次の乱数です。
func next() -> UInt64 {
UInt64(drand48() * Double(UInt64.max))
}
}
19 changes: 0 additions & 19 deletions Sources/YumemiWeather/SeedRandomNumberGenerator.swift

This file was deleted.

2 changes: 1 addition & 1 deletion Sources/YumemiWeather/YumemiDisaster.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class YumemiDisaster {
public init() {}

public func fetchDisaster() {
self.delegate?.handle(disaster: "只今、災害情報はありません。")
delegate?.handle(disaster: "只今、災害情報はありません。")
}

}
54 changes: 54 additions & 0 deletions Sources/YumemiWeather/YumemiWeather.APIQuality.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// YumemiWeather.APIQuality.swift
//
//
// Created by Tomohiro Kumagai on 2024/05/02
//
//

extension YumemiWeather {

/// API の品質を表現します。
enum APIQuality {
case sometimesFails(probability: Double)
case alwaysFails
case neverFails
}
}

extension YumemiWeather {

/// API の想定品質です。
static var apiQuality: APIQuality = .sometimesFails(probability: 0.25)

static func whetherHit(with probability: Double) -> Bool {
return (0 ..< probability).contains(.random(in: 0 ..< 1))
}

/// この場所に不安定要素を埋め込みます。
///
/// 不安定さの度合いは `apiQuality` に依存します。
/// - Throws: YumemiError ここで失敗が生成されると YumemiWeatherError.unknownError が送出されます。
static func introduceInstability() throws {

switch apiQuality {

case .neverFails:
return

case .sometimesFails(let probability) where !whetherHit(with: probability):
return

case .sometimesFails, .alwaysFails:
throw YumemiWeatherError.unknownError
}
}

/// この場所に不安定要素を埋め込みます。
///
/// 不安定さの度合いは `apiQuality` に依存します。
/// - Throws: YumemiError ここで失敗が生成されると YumemiWeatherError.unknownError が送出されます。
func introduceInstability() throws {
try Self.introduceInstability()
}
}
Loading

0 comments on commit 55bdcfb

Please sign in to comment.