Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-write CurrencyMint implementation for new Currency protocol #41

Merged
merged 1 commit into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions Sources/Currency/CurrencyMint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the SwiftCurrency open source project
//
// Copyright (c) 2020 SwiftCurrency project authors
// Copyright (c) 2024 SwiftCurrency project authors
// Licensed under MIT License
//
// See LICENSE.txt for license information
Expand Down Expand Up @@ -51,7 +51,7 @@ extension CurrencyMint {
/// A generator object that supports creation of type-safe currencies by their alphabetic or numeric code identifiers.
public final class CurrencyMint {
/// A closure that receives a currency identifier and finds a matching concrete currency type.
public typealias IdentifierLookup = (CurrencyIdentifier) -> AnyCurrency.Type?
public typealias IdentifierLookup = (CurrencyIdentifier) -> (any Currency.Type)?

/// Returns a shared currency generator that only provides ISO 4217 defined currencies.
public static var standard: CurrencyMint { return .init() }
Expand All @@ -68,8 +68,8 @@ public final class CurrencyMint {

/// Creates an instance that will always resolves the provided currency type when ISO 4217 specification lookup fails.
/// - Parameter defaultCurrency: The default currency type to provide when a currency's identifier is not found in the ISO 4217 specification.
public init<T: CurrencyProtocol>(defaultCurrency: T.Type) {
self.fallbackLookup = { _ in return defaultCurrency }
public init(defaultCurrency: (some Currency).Type) {
self.fallbackLookup = { _ in defaultCurrency }
}

private let fallbackLookup: IdentifierLookup
Expand All @@ -83,23 +83,23 @@ extension CurrencyMint {
/// - identifier: The identifier of the currency to be created.
/// - minorUnits: The quantity of minor units the currency value should represent. The default is `0`.
/// - Returns: An instance of a currency that matches the provided identifier with the desired amount; otherwise `nil`.
public func make(identifier: CurrencyIdentifier, minorUnits value: Int64 = .zero) -> AnyCurrency? {
public func make(identifier: CurrencyIdentifier, minorUnits value: Int64 = .zero) -> (any Currency)? {
guard let currencyType = self.lookup(identifier) else { return nil }
return currencyType.init(minorUnits: value)
}

/// Creates a currency value for the provided identifier.
/// - Parameters:
/// - identifier: The identifier of the currency to be created.
/// - amount: The amount the currency value should represent.
/// - value: The amount the currency value should represent.
/// - Returns: An instance of a currency that matches the provided identifier with the desired amount; otherwise `nil`.
public func make(identifier: CurrencyIdentifier, amount value: Decimal) -> AnyCurrency? {
public func make(identifier: CurrencyIdentifier, exactAmount value: Decimal) -> (any Currency)? {
guard let currencyType = self.lookup(identifier) else { return nil }
return currencyType.init(amount: value)
return currencyType.init(exactAmount: value)
}

private func lookup(_ identifier: CurrencyIdentifier) -> AnyCurrency.Type? {
var typeFound: AnyCurrency.Type? = nil
private func lookup(_ identifier: CurrencyIdentifier) -> (any Currency.Type)? {
var typeFound: (any Currency.Type)? = nil
switch identifier {
case let .alphaCode(value): typeFound = CurrencyMint.lookup(byAlphaCode: value)
case let .numericCode(value): typeFound = CurrencyMint.lookup(byNumCode: value)
Expand Down
4 changes: 2 additions & 2 deletions Sources/ISOStandardCodegen/MintISOSupportGeneration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ fileprivate func makeIdentifierLookupSnippet(
.map {
let patternMatch = type == .numeric ? $0.identifiers.numeric.description : "\"\($0.identifiers.alphabetic)\""

return "case \(patternMatch): return \($0.identifiers.alphabetic).self"
return "case \(patternMatch): return _New_\($0.identifiers.alphabetic).self"
}
.joined(separator: "\n\t\t")

let argumentName = type == .numeric ? "byNumCode" : "byAlphaCode"
let parameterType = type == .numeric ? "UInt16" : "String"

return """
internal static func lookup(\(argumentName) code: \(parameterType)) -> AnyCurrency.Type? {
internal static func lookup(\(argumentName) code: \(parameterType)) -> (any Currency.Type)? {
\t\tswitch code {
\t\t\(casesSnippet)

Expand Down
158 changes: 97 additions & 61 deletions Tests/CurrencyTests/CurrencyMintTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,82 +15,118 @@
import Currency
import XCTest

final class CurrencyMintTests: XCTestCase {
func testLookupByString_passes() {
let pounds = CurrencyMint.standard.make(identifier: "GBP")
XCTAssertTrue(pounds is GBP)
XCTAssertEqual(pounds?.amount, .zero)
}

func testLookupByString_withAmount() {
final class CurrencyMintTests: XCTestCase { }

// MARK: Fallback Lookup

extension CurrencyMintTests {
func test_defaultLookup_whenLookupFails_returnsNil() {
let mint = CurrencyMint.standard

let yen = mint.make(identifier: "JPY", amount: 302.98)
XCTAssertTrue(yen is JPY)
XCTAssertEqual(yen?.amount, 303)

let usd = mint.make(identifier: "USD", minorUnits: 549)
XCTAssertTrue(usd is USD)
XCTAssertEqual(usd?.amount, 5.49)
}

func testLookupByString_fails() {
let darseks = CurrencyMint.standard.make(identifier: "KED", amount: 30.23)

var darseks = mint.make(identifier: .fakeCurrencyAlphaCode, exactAmount: 30.23)
XCTAssertNil(darseks)
}

func testLookupByNum_passes() {
let euros = CurrencyMint.standard.make(identifier: 978)
XCTAssertTrue(euros is EUR)
XCTAssertEqual(euros?.amount, .zero)
}

func testLookupByNum_withAmount() {
let mint = CurrencyMint.standard

let pesos = mint.make(identifier: 484, amount: 3098.9823)
XCTAssertTrue(pesos is MXN)
XCTAssertEqual(pesos?.amount, 3098.98)

let omanis = mint.make(identifier: 512, minorUnits: 198239)
XCTAssertTrue(omanis is OMR)
XCTAssertEqual(omanis?.amount, 198.239)
}

func testLookupByNum_fails() {
let darseks = CurrencyMint.standard.make(identifier: 666)

darseks = CurrencyMint.standard.make(identifier: .fakeCurrencyNumericCode, minorUnits: 300)
XCTAssertNil(darseks)
}

func testDefaultCurrency() {
let mint = CurrencyMint(defaultCurrency: USD.self)
let money = mint.make(identifier: "KED")
XCTAssertTrue(money is USD)
}

func testFallbackLookup() {
struct KED: CurrencyProtocol, CurrencyMetadata {

func test_withFallbackLookup_whenLookupFails_callsFallbackLookup() {
struct KED: Currency, CurrencyMetadata {
static var name: String { return "Klingon Darseks" }
static var alphabeticCode: String { return "KED" }
static var numericCode: UInt16 { return 666 }
static var minorUnits: UInt8 { return 3 }

var minorUnits: Int64

public init<T : BinaryInteger>(minorUnits: T) { self.minorUnits = .init(minorUnits) }
let exactAmount: Decimal
}


let expectation = self.expectation(description: "fallback lookup closure to be called")
expectation.expectedFulfillmentCount = 2

let mint = CurrencyMint(fallbackLookup: { identifier in
expectation.fulfill()

guard identifier == .alphaCode("KED") || identifier == .numericCode(666) else { return nil }
return KED.self
})

let d1 = mint.make(identifier: "KED", amount: 30.239)

let expectedAmount = Decimal(string: "30.239")!
let d1 = mint.make(identifier: "KED", exactAmount: expectedAmount)
XCTAssertTrue(d1 is KED)
XCTAssertEqual(d1 as? KED, 30.239)

let d2 = mint.make(identifier: 666, minorUnits: d1?.minorUnits ?? .zero)

let d2 = mint.make(identifier: 666, minorUnits: .zero)
XCTAssertTrue(d2 is KED)
XCTAssertEqual(d2?.minorUnits, 30239)

self.waitForExpectations(timeout: 1)
}

func test_withDefaultCurrencyLookup_whenLookupFails_returnsDefaultCurrency() {
let mint = CurrencyMint(defaultCurrency: _New_USD.self)

var money = mint.make(identifier: .fakeCurrencyAlphaCode)
XCTAssertTrue(money is _New_USD)

money = mint.make(identifier: .fakeCurrencyNumericCode)
XCTAssertTrue(money is _New_USD)
}
}

// MARK: Factory Methods

extension CurrencyMintTests {
func test_make_withMinorUnits_returnsISOCurrency() {
let mint = CurrencyMint.standard

let pounds = mint.make(identifier: .poundsAlphaCode, minorUnits: .zero)
XCTAssertTrue(pounds is _New_GBP)

let yen = mint.make(identifier: .yenNumericCode, minorUnits: 300)
XCTAssertTrue(yen is _New_JPY)

let euros = CurrencyMint.standard.make(identifier: 978, minorUnits: .zero)
XCTAssertTrue(euros is _New_EUR)
}

func test_make_withAmount_returnsISOCurrency() {
let mint = CurrencyMint.standard

let pounds = mint.make(identifier: .poundsAlphaCode, exactAmount: .zero)
XCTAssertTrue(pounds is _New_GBP)

let yen = mint.make(identifier: .yenNumericCode, exactAmount: 192.8)
XCTAssertTrue(yen is _New_JPY)

let euros = CurrencyMint.standard.make(identifier: 978, exactAmount: .zero)
XCTAssertTrue(euros is _New_EUR)
}

func test_make_withMinorUnits_matchesMinorUnits() {
let mint = CurrencyMint.standard

let pounds = mint.make(identifier: .poundsAlphaCode, minorUnits: 549)
XCTAssertEqual(pounds?.minorUnits, 549)

let yen = mint.make(identifier: .yenNumericCode, minorUnits: 302)
XCTAssertEqual(yen?.minorUnits, 302)
}

func test_make_withAmount_matchesExactAmount() {
let mint = CurrencyMint.standard

let pounds = mint.make(identifier: .poundsAlphaCode, exactAmount: .zero)
XCTAssertEqual(pounds?.exactAmount, .zero)

let yen = mint.make(identifier: .yenNumericCode, exactAmount: 302.98)
XCTAssertEqual(yen?.exactAmount, 302.98)
}
}

// MARK: Test Helpers

extension CurrencyMint.CurrencyIdentifier {
fileprivate static var fakeCurrencyAlphaCode: Self { "KED" } // "darseks"
fileprivate static var fakeCurrencyNumericCode: Self { 666 }

fileprivate static var poundsAlphaCode: Self { .alphaCode(_New_GBP.alphabeticCode) }
fileprivate static var yenNumericCode: Self { .numericCode(_New_JPY.numericCode) }
}
Loading