expo-modules-core 0.4.8 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/android/build.gradle +30 -2
  3. package/android/src/main/java/expo/modules/adapters/react/ModuleRegistryAdapter.java +27 -5
  4. package/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java +49 -5
  5. package/android/src/main/java/expo/modules/core/BasePackage.java +6 -0
  6. package/android/src/main/java/expo/modules/core/ModulePriorities.kt +25 -0
  7. package/android/src/main/java/expo/modules/core/interfaces/ActivityEventListener.java +3 -1
  8. package/android/src/main/java/expo/modules/core/interfaces/Package.java +4 -0
  9. package/android/src/main/java/expo/modules/core/interfaces/ReactActivityHandler.kt +18 -0
  10. package/android/src/main/java/expo/modules/core/interfaces/ReactNativeHostHandler.kt +14 -0
  11. package/android/src/main/java/expo/modules/core/utilities/KotlinUtilities.kt +23 -0
  12. package/android/src/main/java/expo/modules/kotlin/AppContext.kt +166 -0
  13. package/android/src/main/java/expo/modules/kotlin/DynamicExtenstions.kt +9 -0
  14. package/android/src/main/java/expo/modules/kotlin/ExpoModulesHelper.kt +18 -0
  15. package/android/src/main/java/expo/modules/kotlin/KPromiseWrapper.kt +23 -0
  16. package/android/src/main/java/expo/modules/kotlin/KotlinInteropModuleRegistry.kt +98 -0
  17. package/android/src/main/java/expo/modules/kotlin/ModuleHolder.kt +41 -0
  18. package/android/src/main/java/expo/modules/kotlin/ModuleRegistry.kt +56 -0
  19. package/android/src/main/java/expo/modules/kotlin/ModulesProvider.kt +7 -0
  20. package/android/src/main/java/expo/modules/kotlin/Promise.kt +13 -0
  21. package/android/src/main/java/expo/modules/kotlin/ReactLifecycleDelegate.kt +39 -0
  22. package/android/src/main/java/expo/modules/kotlin/ReadableArrayIterator.kt +14 -0
  23. package/android/src/main/java/expo/modules/kotlin/ReadableTypeExtensions.kt +18 -0
  24. package/android/src/main/java/expo/modules/kotlin/allocators/ObjectConstructor.kt +5 -0
  25. package/android/src/main/java/expo/modules/kotlin/allocators/ObjectConstructorFactory.kt +31 -0
  26. package/android/src/main/java/expo/modules/kotlin/allocators/UnsafeAllocator.kt +49 -0
  27. package/android/src/main/java/expo/modules/kotlin/events/EventListener.kt +39 -0
  28. package/android/src/main/java/expo/modules/kotlin/events/EventName.kt +31 -0
  29. package/android/src/main/java/expo/modules/kotlin/events/EventsDefinition.kt +3 -0
  30. package/android/src/main/java/expo/modules/kotlin/events/KEventEmitterWrapper.kt +26 -0
  31. package/android/src/main/java/expo/modules/kotlin/events/OnActivityResultPayload.kt +8 -0
  32. package/android/src/main/java/expo/modules/kotlin/exception/CodedException.kt +70 -0
  33. package/android/src/main/java/expo/modules/kotlin/methods/AnyMethod.kt +50 -0
  34. package/android/src/main/java/expo/modules/kotlin/methods/Method.kt +14 -0
  35. package/android/src/main/java/expo/modules/kotlin/methods/PromiseMethod.kt +15 -0
  36. package/android/src/main/java/expo/modules/kotlin/modules/Module.kt +24 -0
  37. package/android/src/main/java/expo/modules/kotlin/modules/ModuleDefinitionBuilder.kt +227 -0
  38. package/android/src/main/java/expo/modules/kotlin/modules/ModuleDefinitionData.kt +16 -0
  39. package/android/src/main/java/expo/modules/kotlin/records/Field.kt +5 -0
  40. package/android/src/main/java/expo/modules/kotlin/records/Record.kt +3 -0
  41. package/android/src/main/java/expo/modules/kotlin/records/RecordTypeConverter.kt +55 -0
  42. package/android/src/main/java/expo/modules/kotlin/types/AnyType.kt +14 -0
  43. package/android/src/main/java/expo/modules/kotlin/types/ArrayTypeConverter.kt +44 -0
  44. package/android/src/main/java/expo/modules/kotlin/types/BasicTypeConverters.kt +60 -0
  45. package/android/src/main/java/expo/modules/kotlin/types/EnumTypeConverter.kt +84 -0
  46. package/android/src/main/java/expo/modules/kotlin/types/ListTypeConverter.kt +25 -0
  47. package/android/src/main/java/expo/modules/kotlin/types/MapTypeConverter.kt +39 -0
  48. package/android/src/main/java/expo/modules/kotlin/types/PairTypeConverter.kt +28 -0
  49. package/android/src/main/java/expo/modules/kotlin/types/TypeConverter.kt +19 -0
  50. package/android/src/main/java/expo/modules/kotlin/types/TypeConverterProvider.kt +107 -0
  51. package/android/src/main/java/expo/modules/kotlin/views/AnyViewProp.kt +10 -0
  52. package/android/src/main/java/expo/modules/kotlin/views/ConcreteViewProp.kt +17 -0
  53. package/android/src/main/java/expo/modules/kotlin/views/GroupViewManagerWrapper.kt +22 -0
  54. package/android/src/main/java/expo/modules/kotlin/views/SimpleViewManagerWrapper.kt +21 -0
  55. package/android/src/main/java/expo/modules/kotlin/views/ViewManagerDefinition.kt +36 -0
  56. package/android/src/main/java/expo/modules/kotlin/views/ViewManagerDefinitionBuilder.kt +40 -0
  57. package/android/src/main/java/expo/modules/kotlin/views/ViewManagerWrapperDelegate.kt +21 -0
  58. package/android/src/main/java/expo/modules/kotlin/views/ViewWrapperDelegateHolder.kt +5 -0
  59. package/build/NativeModulesProxy.native.d.ts +4 -0
  60. package/build/NativeModulesProxy.native.js +14 -1
  61. package/build/NativeModulesProxy.native.js.map +1 -1
  62. package/build/NativeModulesProxy.types.d.ts +3 -0
  63. package/build/NativeModulesProxy.types.js.map +1 -1
  64. package/ios/AppDelegates/EXAppDelegateWrapper.h +16 -0
  65. package/ios/AppDelegates/EXAppDelegateWrapper.m +42 -0
  66. package/ios/AppDelegates/EXAppDelegatesLoader.h +15 -0
  67. package/ios/AppDelegates/EXAppDelegatesLoader.m +29 -0
  68. package/ios/AppDelegates/EXLegacyAppDelegateWrapper.h +16 -0
  69. package/ios/{EXAppDelegateWrapper.m → AppDelegates/EXLegacyAppDelegateWrapper.m} +2 -2
  70. package/ios/AppDelegates/ExpoAppDelegate.swift +264 -0
  71. package/ios/AppDelegates/ExpoAppDelegateSubscriber.swift +24 -0
  72. package/ios/ExpoModulesCore.podspec +7 -2
  73. package/ios/JSI/ExpoModulesProxySpec.h +24 -0
  74. package/ios/JSI/ExpoModulesProxySpec.mm +135 -0
  75. package/ios/JSI/JSIConversions.h +42 -0
  76. package/ios/JSI/JSIConversions.mm +164 -0
  77. package/ios/JSI/JSIInstaller.h +19 -0
  78. package/ios/JSI/JSIInstaller.mm +22 -0
  79. package/ios/ModuleRegistryAdapter/EXModuleRegistryAdapter.m +1 -6
  80. package/ios/NativeModulesProxy/EXNativeModulesProxy.h +6 -0
  81. package/ios/NativeModulesProxy/{EXNativeModulesProxy.m → EXNativeModulesProxy.mm} +45 -12
  82. package/ios/Services/EXReactNativeEventEmitter.h +6 -0
  83. package/ios/Services/EXReactNativeEventEmitter.m +15 -0
  84. package/ios/Swift/AppContext.swift +14 -1
  85. package/ios/Swift/Arguments/AnyArgument.swift +14 -0
  86. package/ios/Swift/Arguments/AnyArgumentType.swift +13 -0
  87. package/ios/Swift/Arguments/ArgumentType.swift +24 -0
  88. package/ios/Swift/Arguments/ConvertibleArgument.swift +15 -0
  89. package/ios/Swift/Arguments/Convertibles.swift +93 -0
  90. package/ios/Swift/Arguments/Types/ArrayArgumentType.swift +42 -0
  91. package/ios/Swift/Arguments/Types/ConvertibleArgumentType.swift +16 -0
  92. package/ios/Swift/Arguments/Types/EnumArgumentType.swift +105 -0
  93. package/ios/Swift/Arguments/Types/OptionalArgumentType.swift +49 -0
  94. package/ios/Swift/Arguments/Types/PromiseArgumentType.swift +15 -0
  95. package/ios/Swift/Arguments/Types/RawArgumentType.swift +25 -0
  96. package/ios/Swift/Conversions.swift +199 -7
  97. package/ios/Swift/EventListener.swift +37 -5
  98. package/ios/Swift/Functions/AnyFunction.swift +42 -0
  99. package/ios/Swift/{Methods/ConcreteMethod.swift → Functions/ConcreteFunction.swift} +32 -34
  100. package/ios/Swift/ModuleHolder.swift +75 -20
  101. package/ios/Swift/ModuleRegistry.swift +19 -8
  102. package/ios/Swift/Modules/AnyModule.swift +8 -8
  103. package/ios/Swift/Modules/Module.swift +7 -0
  104. package/ios/Swift/Modules/ModuleDefinition.swift +52 -8
  105. package/ios/Swift/Modules/ModuleDefinitionBuilder.swift +1 -1
  106. package/ios/Swift/Modules/ModuleDefinitionComponents.swift +140 -52
  107. package/ios/Swift/ModulesProvider.swift +9 -0
  108. package/ios/Swift/Promise.swift +1 -1
  109. package/ios/Swift/Records/Field.swift +1 -1
  110. package/ios/Swift/Records/Record.swift +8 -1
  111. package/ios/Swift/SwiftInteropBridge.swift +45 -16
  112. package/ios/Swift/Views/AnyViewProp.swift +2 -2
  113. package/ios/Swift/Views/ConcreteViewProp.swift +37 -10
  114. package/ios/Swift/Views/ViewModuleWrapper.swift +9 -4
  115. package/ios/Swift.h +9 -0
  116. package/ios/Tests/ArgumentTypeSpec.swift +145 -0
  117. package/ios/Tests/ConvertiblesSpec.swift +231 -0
  118. package/ios/Tests/{MethodSpec.swift → FunctionSpec.swift} +69 -54
  119. package/ios/Tests/FunctionWithConvertiblesSpec.swift +66 -0
  120. package/ios/Tests/Mocks/ModuleMocks.swift +21 -7
  121. package/ios/Tests/ModuleEventListenersSpec.swift +17 -16
  122. package/ios/Tests/ModuleRegistrySpec.swift +4 -7
  123. package/package.json +3 -3
  124. package/src/NativeModulesProxy.native.ts +22 -2
  125. package/src/NativeModulesProxy.types.ts +8 -0
  126. package/ios/EXAppDelegateWrapper.h +0 -13
  127. package/ios/Swift/Methods/AnyArgumentType.swift +0 -48
  128. package/ios/Swift/Methods/AnyMethod.swift +0 -31
  129. package/ios/Swift/Methods/AnyMethodArgument.swift +0 -13
@@ -1,30 +1,57 @@
1
+ // Copyright 2021-present 650 Industries. All rights reserved.
2
+
1
3
  import UIKit
2
4
 
3
5
  /**
4
6
  Specialized class for the view prop. Specifies the prop name and its setter.
5
7
  */
6
- public class ConcreteViewProp<ViewType: UIView, PropType>: AnyViewProp {
8
+ public final class ConcreteViewProp<ViewType: UIView, PropType: AnyArgument>: AnyViewProp {
7
9
  public typealias SetterType = (ViewType, PropType) -> Void
8
10
 
11
+ /**
12
+ Name of the view prop that JavaScript refers to.
13
+ */
9
14
  public let name: String
10
15
 
11
- let setter: SetterType
16
+ /**
17
+ An argument type wrapper for the prop's value type.
18
+ */
19
+ private let propType: AnyArgumentType
20
+
21
+ /**
22
+ Closure to call to set the actual property on the given view.
23
+ */
24
+ private let setter: SetterType
12
25
 
13
- init(_ name: String, _ setter: @escaping SetterType) {
26
+ internal init(name: String, propType: AnyArgumentType, setter: @escaping SetterType) {
14
27
  self.name = name
28
+ self.propType = propType
15
29
  self.setter = setter
16
30
  }
17
31
 
18
- public func set(value: Any?, onView view: UIView) {
19
- // Method's signature must be type-erased for `AnyViewProp` protocol,
20
- // so we have to get UIView and cast it to the generic type.
21
- // TODO: (@tsapeta) Throw an error instead of crashing the app.
32
+ /**
33
+ Function that sets the underlying prop value for given view.
34
+ */
35
+ public func set(value: Any, onView view: UIView) throws {
36
+ // Method's signature must be type-erased to conform to `AnyViewProp` protocol.
37
+ // Given view must be castable to the generic `ViewType` type.
22
38
  guard let view = view as? ViewType else {
23
- fatalError("Given view must subclass UIView")
39
+ throw IncompatibleViewError(propName: name, viewType: ViewType.self)
24
40
  }
25
- guard let value = value as? PropType else {
26
- fatalError("Given value `\(String(describing: value))` cannot be casted to `\(String(describing: PropType.self))`")
41
+ guard let value = try propType.cast(value) as? PropType else {
42
+ throw Conversions.CastingError<PropType>(value: value)
27
43
  }
28
44
  setter(view, value)
29
45
  }
30
46
  }
47
+
48
+ /**
49
+ An error that is thrown when the view passed to prop's setter doesn't match the type in setter's definition.
50
+ */
51
+ internal struct IncompatibleViewError: CodedError {
52
+ let propName: String
53
+ let viewType: UIView.Type
54
+ var description: String {
55
+ "Tried to set prop `\(propName)` on the view that isn't `\(viewType)`"
56
+ }
57
+ }
@@ -16,7 +16,7 @@ protocol DynamicModuleWrapperProtocol {
16
16
  We're generating its subclasses in runtime as a workaround.
17
17
  */
18
18
  @objc
19
- public class ViewModuleWrapper: RCTViewManager, DynamicModuleWrapperProtocol {
19
+ public final class ViewModuleWrapper: RCTViewManager, DynamicModuleWrapperProtocol {
20
20
  /**
21
21
  A reference to the module holder that stores the module definition.
22
22
  Enforced unwrapping is required since it can be set right after the object is initialized.
@@ -100,14 +100,19 @@ public class ViewModuleWrapper: RCTViewManager, DynamicModuleWrapperProtocol {
100
100
  The setter for `proxiedProperties` prop. In Objective-C style, this function is generated by `RCT_CUSTOM_VIEW_PROPERTY` macro.
101
101
  */
102
102
  @objc
103
- public func set_proxiedProperties(_ json: Any?, forView view: UIView, withDefaultView defaultView: UIView) {
104
- guard let json = json as? [String: Any?],
103
+ public func set_proxiedProperties(_ json: Any, forView view: UIView, withDefaultView defaultView: UIView) {
104
+ guard let json = json as? [String: Any],
105
105
  let props = wrappedModuleHolder.definition.viewManager?.propsDict() else {
106
106
  return
107
107
  }
108
108
  for (key, value) in json {
109
109
  if let prop = props[key] {
110
- prop.set(value: value, onView: view)
110
+ let value = Conversions.fromNSObject(value)
111
+
112
+ // TODO: @tsapeta: Figure out better way to rethrow errors from here.
113
+ // Adding `throws` keyword to the function results in different
114
+ // method signature in Objective-C. Maybe just call `RCTLogError`?
115
+ try? prop.set(value: value, onView: view)
111
116
  }
112
117
  }
113
118
  }
package/ios/Swift.h ADDED
@@ -0,0 +1,9 @@
1
+ // Copyright 2018-present 650 Industries. All rights reserved.
2
+
3
+ // When `use_frameworks!` is used, the generated Swift header is inside ExpoModulesCore module.
4
+ // Otherwise, it's available only locally with double-quoted imports.
5
+ #if __has_include(<ExpoModulesCore/ExpoModulesCore-Swift.h>)
6
+ #import <ExpoModulesCore/ExpoModulesCore-Swift.h>
7
+ #else
8
+ #import "ExpoModulesCore-Swift.h"
9
+ #endif
@@ -0,0 +1,145 @@
1
+ // Copyright 2021-present 650 Industries. All rights reserved.
2
+
3
+ import Quick
4
+ import Nimble
5
+
6
+ @testable import ExpoModulesCore
7
+
8
+ class ArgumentTypeSpec: QuickSpec {
9
+ override func spec() {
10
+
11
+ it("casts primitives") {
12
+ let type = ArgumentType(Int.self)
13
+ let value = 123
14
+ let anyValue = value as Any
15
+
16
+ expect(try type.cast(anyValue)).to(be(value))
17
+ }
18
+
19
+ it("casts optional types") {
20
+ let type = ArgumentType(Double?.self)
21
+ let value: Double? = nil
22
+ let anyValue = value as Any
23
+ let result = try type.cast(anyValue)
24
+
25
+ expect(result).to(beAKindOf(Double?.self))
26
+
27
+ // Since this `nil` is in fact of non-optional `Any` type, under the hood it's described as `Optional` enum.
28
+ // Simply checking `result == nil` does NOT work here, see `Optional.isNil` extension implementation.
29
+ expect(Optional.isNil(result)) == true
30
+ }
31
+
32
+ it("throws null cast error") {
33
+ let type = ArgumentType(Double.self) // non-optional (!)
34
+ let value: Double? = nil
35
+ let anyValue = value as Any
36
+
37
+ expect { try type.cast(anyValue) }.to(throwError(errorType: Conversions.NullCastError<Double>.self))
38
+ }
39
+
40
+ it("casts arrays") {
41
+ let type = ArgumentType([Double].self)
42
+ let value = 9.9
43
+ let anyValue = [value] as [Any]
44
+ let result = try type.cast(anyValue) as! [Any]
45
+
46
+ expect(result).to(beAKindOf([Double].self))
47
+ expect((result as! [Double]).first) == value
48
+ }
49
+
50
+ it("casts convertibles") {
51
+ let type = ArgumentType(ConvertibleTestStruct.self)
52
+ let value = "expo is the best"
53
+ let result = try type.cast(value)
54
+
55
+ expect(result).to(beAKindOf(ConvertibleTestStruct.self))
56
+ expect((result as! ConvertibleTestStruct).value) == value
57
+ }
58
+
59
+ it("casts array of convertibles") {
60
+ let type = ArgumentType([ConvertibleTestStruct].self)
61
+ let value = ["expo is the best"]
62
+ let result = try type.cast(value)
63
+
64
+ expect(result).to(beAKindOf([ConvertibleTestStruct].self))
65
+ expect((result as! [ConvertibleTestStruct]).first!.value) == value.first
66
+ }
67
+
68
+ it("casts array of array of convertibles") {
69
+ let type = ArgumentType([[ConvertibleTestStruct]].self)
70
+ let value = [["expo is the best"]]
71
+ let result = try type.cast(value)
72
+
73
+ expect(result).to(beAKindOf([[ConvertibleTestStruct]].self))
74
+ expect((result as! [[ConvertibleTestStruct]]).first!.first!.value) == value.first!.first
75
+ }
76
+
77
+ describe("EnumArgumentType") {
78
+ it("casts from String") {
79
+ let type = ArgumentType(StringTestEnum.self)
80
+ let input = "expo"
81
+ let output = try type.cast(input)
82
+
83
+ expect(output).to(beAKindOf(StringTestEnum.self))
84
+ expect(output as? StringTestEnum) == StringTestEnum.expo
85
+ expect(output as? StringTestEnum) == StringTestEnum(rawValue: input)
86
+ expect((output as! StringTestEnum).rawValue) == input
87
+ expect((output as! EnumArgument).anyRawValue).to(beAKindOf(String.self))
88
+ }
89
+
90
+ it("casts from Int") {
91
+ let type = ArgumentType(IntTestEnum.self)
92
+ let input: Int = -1
93
+ let output = try type.cast(input)
94
+
95
+ expect(output).to(beAKindOf(IntTestEnum.self))
96
+ expect(output as? IntTestEnum) == IntTestEnum.negative
97
+ expect(output as? IntTestEnum) == IntTestEnum(rawValue: input)
98
+ expect((output as! IntTestEnum).rawValue) == input
99
+ expect((output as! EnumArgument).anyRawValue).to(beAKindOf(Int.self))
100
+ }
101
+
102
+ it("throws casting error") {
103
+ let type = ArgumentType(IntTestEnum.self)
104
+
105
+ // "841" is not a raw value of any `IntTestEnum` case
106
+ expect { try type.cast("string instead of int") }.to(throwError {
107
+ expect($0).to(beAKindOf(EnumCastingError.self))
108
+ })
109
+ }
110
+
111
+ it("throws no such value error") {
112
+ let type = ArgumentType(IntTestEnum.self)
113
+
114
+ // "841" is not a raw value of any `IntTestEnum` case
115
+ expect { try type.cast(841) }.to(throwError {
116
+ expect($0).to(beAKindOf(EnumNoSuchValueError.self))
117
+ })
118
+ }
119
+
120
+ it("gets a list of all raw values") {
121
+ expect(StringTestEnum.allRawValues as? [String]) == ["hello", "expo"]
122
+ expect(IntTestEnum.allRawValues as? [Int]) == [-1, 1]
123
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ struct ConvertibleTestStruct: ConvertibleArgument {
129
+ let value: String
130
+
131
+ static func convert(from value: Any?) throws -> ConvertibleTestStruct {
132
+ guard let str = value as? String else { fatalError() }
133
+ return ConvertibleTestStruct(value: str)
134
+ }
135
+ }
136
+
137
+ enum StringTestEnum: String, EnumArgument {
138
+ case hello
139
+ case expo
140
+ }
141
+
142
+ enum IntTestEnum: Int, EnumArgument {
143
+ case negative = -1
144
+ case positive = 1
145
+ }
@@ -0,0 +1,231 @@
1
+ // Copyright 2018-present 650 Industries. All rights reserved.
2
+
3
+ import CoreGraphics
4
+ import Quick
5
+ import Nimble
6
+
7
+ @testable import ExpoModulesCore
8
+
9
+ class ConvertiblesSpec: QuickSpec {
10
+ override func spec() {
11
+ describe("CGPoint") {
12
+ let x = -8.3
13
+ let y = 4.6
14
+
15
+ it("converts from array of doubles") {
16
+ let point = try CGPoint.convert(from: [x, y])
17
+
18
+ expect(point.x) == x
19
+ expect(point.y) == y
20
+ }
21
+
22
+ it("converts from dict") {
23
+ let point = try CGPoint.convert(from: ["x": x, "y": y])
24
+
25
+ expect(point.x) == x
26
+ expect(point.y) == y
27
+ }
28
+
29
+ it("throws when array size is unexpected") { // different than two
30
+ expect { try CGPoint.convert(from: []) }.to(throwError(errorType: Conversions.ConvertingError<CGPoint>.self))
31
+ expect { try CGPoint.convert(from: [x]) }.to(throwError(errorType: Conversions.ConvertingError<CGPoint>.self))
32
+ expect { try CGPoint.convert(from: [x, y, x]) }.to(throwError(errorType: Conversions.ConvertingError<CGPoint>.self))
33
+ }
34
+
35
+ it("throws when dict is missing some keys") {
36
+ expect { try CGPoint.convert(from: ["test": x]) }.to(throwError {
37
+ expect($0).to(beAKindOf(Conversions.MissingKeysError<Double>.self))
38
+ expect(($0 as! CodedError).description) == Conversions.MissingKeysError<Double>(keys: ["x", "y"]).description
39
+ })
40
+ }
41
+
42
+ it("throws when dict has uncastable keys") {
43
+ expect { try CGPoint.convert(from: ["x": x, "y": "string"]) }.to(throwError {
44
+ expect($0).to(beAKindOf(Conversions.CastingValuesError<Double>.self))
45
+ expect(($0 as! CodedError).description) == Conversions.CastingValuesError<Double>(keys: ["y"]).description
46
+ })
47
+ }
48
+ }
49
+
50
+ describe("CGSize") {
51
+ let width = 52.8
52
+ let height = 81.7
53
+
54
+ it("converts from array of doubles") {
55
+ let size = try CGSize.convert(from: [width, height])
56
+
57
+ expect(size.width) == width
58
+ expect(size.height) == height
59
+ }
60
+
61
+ it("converts from dict") {
62
+ let size = try CGSize.convert(from: ["width": width, "height": height])
63
+
64
+ expect(size.width) == width
65
+ expect(size.height) == height
66
+ }
67
+
68
+ it("throws when array size is unexpected") { // different than two
69
+ expect { try CGSize.convert(from: []) }.to(throwError(errorType: Conversions.ConvertingError<CGSize>.self))
70
+ expect { try CGSize.convert(from: [width]) }.to(throwError(errorType: Conversions.ConvertingError<CGSize>.self))
71
+ expect { try CGSize.convert(from: [width, height, width]) }.to(throwError(errorType: Conversions.ConvertingError<CGSize>.self))
72
+ }
73
+
74
+ it("throws when dict is missing some keys") {
75
+ expect { try CGSize.convert(from: ["width": width]) }.to(throwError {
76
+ expect($0).to(beAKindOf(Conversions.MissingKeysError<Double>.self))
77
+ expect(($0 as! CodedError).description) == Conversions.MissingKeysError<Double>(keys: ["height"]).description
78
+ })
79
+ }
80
+
81
+ it("throws when dict has uncastable keys") {
82
+ expect { try CGSize.convert(from: ["width": "test", "height": height]) }.to(throwError {
83
+ expect($0).to(beAKindOf(Conversions.CastingValuesError<Double>.self))
84
+ expect(($0 as! CodedError).description) == Conversions.CastingValuesError<Double>(keys: ["width"]).description
85
+ })
86
+ }
87
+ }
88
+
89
+ describe("CGVector") {
90
+ let dx = 11.6
91
+ let dy = -4.0
92
+
93
+ it("converts from array of doubles") {
94
+ let vector = try CGVector.convert(from: [dx, dy])
95
+
96
+ expect(vector.dx) == dx
97
+ expect(vector.dy) == dy
98
+ }
99
+
100
+ it("converts from dict") {
101
+ let vector = try CGVector.convert(from: ["dx": dx, "dy": dy])
102
+
103
+ expect(vector.dx) == dx
104
+ expect(vector.dy) == dy
105
+ }
106
+
107
+ it("throws when array size is unexpected") { // different than two
108
+ expect { try CGVector.convert(from: []) }.to(throwError(errorType: Conversions.ConvertingError<CGVector>.self))
109
+ expect { try CGVector.convert(from: [dx]) }.to(throwError(errorType: Conversions.ConvertingError<CGVector>.self))
110
+ expect { try CGVector.convert(from: [dx, dy, dx]) }.to(throwError(errorType: Conversions.ConvertingError<CGVector>.self))
111
+ }
112
+
113
+ it("throws when dict is missing some keys") {
114
+ expect { try CGVector.convert(from: ["dx": dx]) }.to(throwError {
115
+ expect($0).to(beAKindOf(Conversions.MissingKeysError<Double>.self))
116
+ expect(($0 as! CodedError).description) == Conversions.MissingKeysError<Double>(keys: ["dy"]).description
117
+ })
118
+ }
119
+
120
+ it("throws when dict has uncastable keys") {
121
+ expect { try CGVector.convert(from: ["dx": "dx", "dy": dy]) }.to(throwError {
122
+ expect($0).to(beAKindOf(Conversions.CastingValuesError<Double>.self))
123
+ expect(($0 as! CodedError).description) == Conversions.CastingValuesError<Double>(keys: ["dx"]).description
124
+ })
125
+ }
126
+ }
127
+
128
+ describe("CGRect") {
129
+ let x = -8.3
130
+ let y = 4.6
131
+ let width = 52.8
132
+ let height = 81.7
133
+
134
+ it("converts from array of doubles") {
135
+ let rect = try CGRect.convert(from: [x, y, width, height])
136
+
137
+ expect(rect.origin.x) == x
138
+ expect(rect.origin.y) == y
139
+ expect(rect.width) == width
140
+ expect(rect.height) == height
141
+ }
142
+
143
+ it("converts from dict") {
144
+ let rect = try CGRect.convert(from: ["x": x, "y": y, "width": width, "height": height])
145
+
146
+ expect(rect.origin.x) == x
147
+ expect(rect.origin.y) == y
148
+ expect(rect.width) == width
149
+ expect(rect.height) == height
150
+ }
151
+
152
+ it("throws when array size is unexpected") { // different than four
153
+ expect { try CGRect.convert(from: [x]) }.to(throwError(errorType: Conversions.ConvertingError<CGRect>.self))
154
+ expect { try CGRect.convert(from: [x, y]) }.to(throwError(errorType: Conversions.ConvertingError<CGRect>.self))
155
+ expect { try CGRect.convert(from: [x, y, width, height, y]) }.to(throwError(errorType: Conversions.ConvertingError<CGRect>.self))
156
+ }
157
+
158
+ it("throws when dict is missing some keys") {
159
+ expect { try CGRect.convert(from: ["x": x]) }.to(throwError {
160
+ expect($0).to(beAKindOf(Conversions.MissingKeysError<Double>.self))
161
+ expect(($0 as! CodedError).description) == Conversions.MissingKeysError<Double>(keys: ["y", "width", "height"]).description
162
+ })
163
+ }
164
+
165
+ it("throws when dict has uncastable keys") {
166
+ expect { try CGRect.convert(from: ["x": x, "y": nil, "width": width, "height": "\(height)"]) }.to(throwError {
167
+ expect($0).to(beAKindOf(Conversions.CastingValuesError<Double>.self))
168
+ expect(($0 as! CodedError).description) == Conversions.CastingValuesError<Double>(keys: ["y", "height"]).description
169
+ })
170
+ }
171
+ }
172
+
173
+ describe("UIColor/CGColor") {
174
+ func testColorComponents(_ color: CGColor, _ red: CGFloat, _ green: CGFloat, _ blue: CGFloat, _ alpha: CGFloat) {
175
+ expect(color.components?[0]) == red / 255.0
176
+ expect(color.components?[1]) == green / 255.0
177
+ expect(color.components?[2]) == blue / 255.0
178
+ expect(color.components?[3]) == alpha / 255.0
179
+ }
180
+ func testInvalidHexColor(_ hex: String) {
181
+ expect { try CGColor.convert(from: hex) }.to(throwError {
182
+ expect($0).to(beAKindOf(Conversions.InvalidHexColorError.self))
183
+ expect(($0 as! CodedError).description) == Conversions.InvalidHexColorError(hex: hex).description
184
+ })
185
+ }
186
+
187
+ it("converts from ARGB int") {
188
+ // NOTE: int representation has alpha channel at the beginning
189
+ let color = try CGColor.convert(from: 0x5147AC7F)
190
+ testColorComponents(color, 0x47, 0xAC, 0x7F, 0x51)
191
+ }
192
+
193
+ it("converts from RGBA hex string") {
194
+ let color = try CGColor.convert(from: "47AC7F51")
195
+ testColorComponents(color, 0x47, 0xAC, 0x7F, 0x51)
196
+ }
197
+
198
+ it("converts from #RGBA hex string") {
199
+ let color = try CGColor.convert(from: " #47AC7F51")
200
+ testColorComponents(color, 0x47, 0xAC, 0x7F, 0x51)
201
+ }
202
+
203
+ it("converts from 3-character shorthand hex string") {
204
+ let color = try CGColor.convert(from: "C2B ")
205
+ testColorComponents(color, 0xCC, 0x22, 0xBB, 0xFF)
206
+ }
207
+
208
+ it("converts from 4-character shorthand hex string") {
209
+ let color = try CGColor.convert(from: " #9EA5 ")
210
+ testColorComponents(color, 0x99, 0xEE, 0xAA, 0x55)
211
+ }
212
+
213
+ it("throws when hex string is invalid") {
214
+ testInvalidHexColor("")
215
+ testInvalidHexColor("#21")
216
+ testInvalidHexColor("ABCDEFGH")
217
+ testInvalidHexColor("1122334455")
218
+ testInvalidHexColor("XYZ")
219
+ testInvalidHexColor("!@#$%")
220
+ }
221
+
222
+ it("throws when int overflows") {
223
+ let hex = 0xBBAA88FF2
224
+ expect { try CGColor.convert(from: hex) }.to(throwError {
225
+ expect($0).to(beAKindOf(Conversions.HexColorOverflowError.self))
226
+ expect(($0 as! CodedError).description) == Conversions.HexColorOverflowError(hex: UInt64(hex)).description
227
+ })
228
+ }
229
+ }
230
+ }
231
+ }