Skip to content

Commit

Permalink
Merge branch 'main' into feature/edit_camel_case
Browse files Browse the repository at this point in the history
  • Loading branch information
es-kumagai authored May 23, 2024
2 parents 3927660 + 55bdcfb commit bf45efc
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 29 deletions.
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()
}
}
41 changes: 26 additions & 15 deletions Sources/YumemiWeather/YumemiWeather.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,28 +39,33 @@ public enum YumemiWeatherError: Error {
case unknownError
}

/// 天気予報を取得する擬似天気予報 API です。
///
/// ゆめみ iOS 研修用の実装であり、実際の天気予報ではなくランダムな天気予報が得られます。
/// 研修効果を高めるため、さまざまなバージョンの API が用意されていて、
/// その中には適切なパラメーターを与えたときでも一定の確率でエラーを返すものもあります。
final public class YumemiWeather {

/// 擬似 天気予報 API Simple ver
/// 天気予報を読み込む API Simple Version です。
/// - Returns: 天気状況を表す文字列 "sunny" or "cloudy" or "rainy"
public static func fetchWeatherCondition() -> String {
return makeRandomResponse().weatherCondition
}

/// 擬似 天気予報 API Throws ver
/// 天気予報を読み込む API の Throwing Version です。
/// - Throws: YumemiWeatherError
/// - Parameters:
/// - area: 天気予報を取得する対象地域 example: "tokyo"
/// - Returns: 天気状況を表す文字列 "sunny" or "cloudy" or "rainy"
public static func fetchWeatherCondition(at area: String) throws -> String {
if Int.random(in: 0...4) == 4 {
throw YumemiWeatherError.unknownError
}

return makeRandomResponse().weatherCondition
try introduceInstability()
return self.makeRandomResponse().weatherCondition
}

/// 擬似 天気予報 API JSON ver
/// 天気予報を読み込む API の JSON Version です。
///
/// JSON 文字列で地域情報 `area` と日付情報 `date` を持つオブジェクトを受け取って、それに該当する天気予報を取得します。
/// 取得された天気予報は速やかに返されます。
///
/// API に請求する JSON 文字列の例:
///
Expand Down Expand Up @@ -90,14 +95,14 @@ final public class YumemiWeather {
let response = makeRandomResponse(date: request.date)
let responseData = try encoder.encode(response)

if Int.random(in: 0...4) == 4 {
throw YumemiWeatherError.unknownError
}

try introduceInstability()
return String(data: responseData, encoding: .utf8)!
}

/// 擬似 天気予報 API Sync ver
/// 天気予報を読み込む API の Sync Version です。
///
/// JSON 文字列で地域情報 `area` と日付情報 `date` を持つオブジェクトを受け取って、それに該当する天気予報を取得します。
/// この API は同期的に実行され、天気予報を返すまでに若干時間がかかります。
///
/// API に請求する JSON 文字列の例:
///
Expand All @@ -123,7 +128,10 @@ final public class YumemiWeather {
return try fetchWeather(jsonString)
}

/// 擬似 天気予報 API Callback ver
/// 天気予報を読み込む API の Callback Version です。
///
/// JSON 文字列で地域情報 `area` と日付情報 `date` を持つオブジェクトを受け取って、それに該当する天気予報を取得します。
/// この API は非同期的に実行され、天気予報を取得できるとその結果を添えて `completion` を呼び出します。
///
/// API に請求する JSON 文字列の例:
///
Expand Down Expand Up @@ -160,8 +168,11 @@ final public class YumemiWeather {
}
}

/// 擬似 天気予報 API Async ver
/// 天気予報を読み込む API Async Version です。
///
/// JSON 文字列で地域情報 `area` と日付情報 `date` を持つオブジェクトを受け取って、それに該当する天気予報を取得します。
/// この API は非同期的に実行され、天気予報を取得できるまでは Swift Concurrency により処理が中断されます。
///
/// API に請求する JSON 文字列の例:
///
/// {
Expand Down
33 changes: 26 additions & 7 deletions Sources/YumemiWeather/YumemiWeatherList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,20 @@ public enum Area: String, CaseIterable, Codable {

public extension YumemiWeather {

/// 擬似 天気予報一覧 API JSON ver
/// 天気予報一覧を読み込む API の JSON Version です。
///
/// JSON 文字列で地域情報 `areas` と日付情報 `date` を持つオブジェクトを受け取って、それに該当する天気予報を取得します。
/// 取得された天気予報は速やかに返されます。
///
/// 地域情報は配列で複数の地域を一括指定し、それらの地域の天気予報を取得できます。地域を指定しない場合は全地域の天気予報を取得します。
///
/// API に請求する JSON 文字列の例:
///
/// {
/// "areas": ["Tokyo"],
/// "date": "2020-04-01T12:00:00+09:00"
/// }
///
/// 返された AreaResponse の JSON 文字列の例
///
/// [
Expand All @@ -64,9 +70,7 @@ public extension YumemiWeather {
throw YumemiWeatherError.invalidParameterError
}

if Int.random(in: 0...4) == 4 {
throw YumemiWeatherError.unknownError
}
try introduceInstability()

let areas = request.areas.isEmpty ? Area.allCases : request.areas.compactMap { Area(rawValue: $0) }
let areaResponses = areas.map { area -> AreaResponse in
Expand All @@ -78,7 +82,12 @@ public extension YumemiWeather {
return String(data: responseData, encoding: .utf8)!
}

/// 擬似 天気予報一覧 API Sync ver
/// 天気予報一覧を読み込む API の Sync Version です。
///
/// JSON 文字列で地域情報 `areas` と日付情報 `date` を持つオブジェクトを受け取って、それに該当する天気予報を取得します。
/// この API は同期的に実行され、天気予報を返すまでに若干時間がかかります。
///
/// 地域情報は配列で複数の地域を一括指定し、それらの地域の天気予報を取得できます。地域を指定しない場合は全地域の天気予報を取得します。
///
/// API に請求する JSON 文字列の例:
///
Expand Down Expand Up @@ -107,7 +116,12 @@ public extension YumemiWeather {
return try fetchWeatherList(jsonString)
}

/// 擬似 天気予報一覧 API Callback ver
/// 天気予報一覧を読み込む API の Callback Version です。
///
/// JSON 文字列で地域情報 `areas` と日付情報 `date` を持つオブジェクトを受け取って、それに該当する天気予報を取得します。
/// この API は非同期的に実行され、天気予報を取得できるとその結果を添えて `completion` を呼び出します。
///
/// 地域情報は配列で複数の地域を一括指定し、それらの地域の天気予報を取得できます。地域を指定しない場合は全地域の天気予報を取得します。
///
/// API に請求する JSON 文字列の例:
///
Expand Down Expand Up @@ -149,7 +163,12 @@ public extension YumemiWeather {
}
}

/// 擬似 天気予報一覧API Async ver
/// 天気予報一覧を読み込む API の Async Version です。
///
/// JSON 文字列で地域情報 `areas` と日付情報 `date` を持つオブジェクトを受け取って、それに該当する天気予報を取得します。
/// この API は非同期的に実行され、天気予報を取得できるまでは Swift Concurrency により処理が中断されます。
///
/// 地域情報は配列で複数の地域を一括指定し、それらの地域の天気予報を取得できます。地域を指定しない場合は全地域の天気予報を取得します。
///
/// API に請求する JSON 文字列の例:
///
Expand Down
67 changes: 67 additions & 0 deletions Tests/YumemiWeatherTests/TestMethods.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//
// TestMethods.swift
//
//
// Created by Tomohiro Kumagai on 2024/05/02
//
//

import XCTest
@testable import YumemiWeather

/// 失敗数が想定内かを判定します。
/// - Parameters:
/// - probability: 失敗率です。
/// - tryingCount: 総試行回数です。
/// - failureCount: うち、失敗した回数です。
/// - file: 判定コードが記載されたファイルです。
/// - line: 判定コードが記載された行です。
func XCTAssertFailureCount(probability: Double, tryingCount: Int, failureCount: Int, file: StaticString = #filePath, line: UInt = #line) {

let estimatedFailureCount = Double(tryingCount) * probability
let expectedFailureMargin = Double(tryingCount) * 0.01

let expectedFailureRange = estimatedFailureCount - expectedFailureMargin ..< estimatedFailureCount + expectedFailureMargin

XCTAssertTrue(expectedFailureRange.contains(Double(failureCount)), "Failures: \(failureCount), Estimated failures = \(estimatedFailureCount)±\(expectedFailureMargin)", file: file, line: line)
}

/// YumemiWether API の品質をテストします。
/// - Parameters:
/// - tryingCount: 総試行回数です。
/// - quality: API の想定品質です。
/// - file: 判定コードが記載されたファイルです。
/// - line: 判定コードが記載された行です。
func XCTAssertAPIQuality(_ predicate: () throws -> Void, tryingCount: Int, quality: YumemiWeather.APIQuality, file: StaticString = #filePath, line: UInt = #line) {

var succeededCount = 0
var failedCount = 0

YumemiWeather.apiQuality = quality

for _ in 0 ..< tryingCount {

switch Result(catching: predicate) {

case .success():
succeededCount += 1

case .failure(_):
failedCount += 1
}
}

switch quality {

case .neverFails:
XCTAssertEqual(succeededCount, tryingCount, file: file, line: line)
XCTAssertEqual(failedCount, 0, file: file, line: line)

case .alwaysFails:
XCTAssertEqual(succeededCount, 0, file: file, line: line)
XCTAssertEqual(failedCount, tryingCount, file: file, line: line)

case .sometimesFails(let probability):
XCTAssertFailureCount(probability: probability, tryingCount: tryingCount, failureCount: failedCount, file: file, line: line)
}
}
30 changes: 26 additions & 4 deletions Tests/YumemiWeatherTests/YumemiWeatherListTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,28 @@ import XCTest

final class YumemiWeatherListTests: XCTestCase {

override func setUpWithError() throws {
YumemiWeather.apiQuality = .neverFails
}

func test_APIは必ずしも成功しない() throws {

let tryingCount = 15_000
let request = """
{
"area": "tokyo",
"date": "2020-04-01T12:00:00+09:00"
}
"""
let predicate = { _ = try YumemiWeather.fetchWeather(request) }

XCTAssertAPIQuality(predicate, tryingCount: tryingCount, quality: .sometimesFails(probability: 0.25))
XCTAssertAPIQuality(predicate, tryingCount: tryingCount, quality: .sometimesFails(probability: 0.8))
XCTAssertAPIQuality(predicate, tryingCount: tryingCount, quality: .sometimesFails(probability: 0.05))
XCTAssertAPIQuality(predicate, tryingCount: tryingCount, quality: .alwaysFails)
XCTAssertAPIQuality(predicate, tryingCount: tryingCount, quality: .neverFails)
}

func test_Areasに空を指定したときに全ての地域が取得される() throws {

let request = """
Expand All @@ -24,7 +46,7 @@ final class YumemiWeatherListTests: XCTestCase {
XCTAssertEqual(response.map(\.area), Area.allCases)
}

func test_fetchWeatherList_jsonString() {
func test_fetchWeatherList_jsonString() throws {
let parameter = """
{
"areas": [],
Expand All @@ -49,7 +71,7 @@ final class YumemiWeatherListTests: XCTestCase {
}
}

func test_fetchWeatherList_jsonString_one() {
func test_fetchWeatherList_jsonString_one() throws {
let parameter = """
{
"areas": ["Tokyo"],
Expand Down Expand Up @@ -81,7 +103,7 @@ final class YumemiWeatherListTests: XCTestCase {
}
}

func test_fetchWeatherList_jsonString_two() {
func test_fetchWeatherList_jsonString_two() throws {
let parameter = """
{
"areas": ["Tokyo", "Nagoya"],
Expand Down Expand Up @@ -111,7 +133,7 @@ final class YumemiWeatherListTests: XCTestCase {
}
}

func test_fetchWeatherList_jsonString_none() {
func test_fetchWeatherList_jsonString_none() throws {
let parameter = """
{
"areas": ["LosAngeles"],
Expand Down
10 changes: 7 additions & 3 deletions Tests/YumemiWeatherTests/YumemiWeatherTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import XCTest

final class YumemiWeatherTests: XCTestCase {

override func setUpWithError() throws {
YumemiWeather.apiQuality = .neverFails
}

func test_ランダムにレスポンスを生成する() {

// 日付を省略すると実行した瞬間のものが得られてしまうため、
Expand Down Expand Up @@ -62,7 +66,7 @@ final class YumemiWeatherTests: XCTestCase {
}
}

func test_fetchWeather_jsonString() {
func test_fetchWeather_jsonString() throws {
let parameter = """
{
"area": "Tokyo",
Expand All @@ -86,7 +90,7 @@ final class YumemiWeatherTests: XCTestCase {
}
}

func test_fetchWeather_jsonString_sync() {
func test_fetchWeather_jsonString_sync() throws {
let beginDate = Date()
let parameter = """
{
Expand Down Expand Up @@ -139,7 +143,7 @@ final class YumemiWeatherTests: XCTestCase {
}

@available(iOS 13, macOS 10.15, *)
func test_fetchWeather_jsonString_async() async {
func test_fetchWeather_jsonString_async() async throws {
let beginDate = Date()
let parameter = """
{
Expand Down

0 comments on commit bf45efc

Please sign in to comment.