diff --git a/Sources/JavaScriptKit/BasicObjects/JSArray.swift b/Sources/JavaScriptKit/BasicObjects/JSArray.swift index 4d5bf231f..142145a34 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSArray.swift @@ -1,21 +1,30 @@ /// A wrapper around [the JavaScript Array class](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array) /// that exposes its properties in a type-safe and Swifty way. -public class JSArray { - static let classObject = JSObject.global.Array.function! +public class JSArray: JSBridgedClass { + public static let constructor = JSObject.global.Array.function! static func isArray(_ object: JSObject) -> Bool { - classObject.isArray!(object).boolean! + constructor.isArray!(object).boolean! + } + + public let jsObject: JSObject + + public required convenience init?(from value: JSValue) { + guard let object = value.object else { return nil } + self.init(object) } - let ref: JSObject - /// Construct a `JSArray` from Array `JSObject`. /// Return `nil` if the object is not an Array. /// /// - Parameter object: A `JSObject` expected to be a JavaScript Array - public init?(_ ref: JSObject) { - guard Self.isArray(ref) else { return nil } - self.ref = ref + public convenience init?(_ jsObject: JSObject) { + guard Self.isArray(jsObject) else { return nil } + self.init(unsafelyWrapping: jsObject) + } + + public required init(unsafelyWrapping jsObject: JSObject) { + self.jsObject = jsObject } } @@ -23,32 +32,32 @@ extension JSArray: RandomAccessCollection { public typealias Element = JSValue public func makeIterator() -> Iterator { - Iterator(ref: ref) + Iterator(jsObject: jsObject) } public class Iterator: IteratorProtocol { - let ref: JSObject - var index = 0 - init(ref: JSObject) { - self.ref = ref + private let jsObject: JSObject + private var index = 0 + init(jsObject: JSObject) { + self.jsObject = jsObject } public func next() -> Element? { let currentIndex = index - guard currentIndex < Int(ref.length.number!) else { + guard currentIndex < Int(jsObject.length.number!) else { return nil } index += 1 - guard ref.hasOwnProperty!(currentIndex).boolean! else { + guard jsObject.hasOwnProperty!(currentIndex).boolean! else { return next() } - let value = ref[currentIndex] + let value = jsObject[currentIndex] return value } } public subscript(position: Int) -> JSValue { - ref[position] + jsObject[position] } public var startIndex: Int { 0 } @@ -68,14 +77,14 @@ extension JSArray: RandomAccessCollection { /// array.count // 2 /// ``` public var length: Int { - return Int(ref.length.number!) + Int(jsObject.length.number!) } /// The number of elements in that array **not** including empty hole. /// Note that `count` syncs with the number that `Iterator` can iterate. /// See also: `JSArray.length` public var count: Int { - return getObjectValuesLength(ref) + getObjectValuesLength(jsObject) } } diff --git a/Sources/JavaScriptKit/BasicObjects/JSDate.swift b/Sources/JavaScriptKit/BasicObjects/JSDate.swift index e871c4ce6..651a39d1b 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSDate.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSDate.swift @@ -6,9 +6,9 @@ in the naming. Parts of the JavaScript `Date` API that are not consistent across implementations are not exposed in a type-safe manner, you should access the underlying `jsObject` property if you need those. */ -public final class JSDate { +public final class JSDate: JSBridgedClass { /// The constructor function used to create new `Date` objects. - private static let constructor = JSObject.global.Date.function! + public static let constructor = JSObject.global.Date.function! /// The underlying JavaScript `Date` object. public let jsObject: JSObject @@ -39,6 +39,10 @@ public final class JSDate { jsObject = Self.constructor.new(year, monthIndex, day, hours, minutes, seconds, milliseconds) } + public init(unsafelyWrapping jsObject: JSObject) { + self.jsObject = jsObject + } + /// Year of this date in local time zone. public var fullYear: Int { get { diff --git a/Sources/JavaScriptKit/BasicObjects/JSError.swift b/Sources/JavaScriptKit/BasicObjects/JSError.swift index 4dff7585b..8edd3f690 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSError.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSError.swift @@ -2,9 +2,9 @@ class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) that exposes its properties in a type-safe way. */ -public final class JSError: Error { +public final class JSError: Error, JSBridgedClass { /// The constructor function used to create new `Error` objects. - private static let constructor = JSObject.global.Error.function! + public static let constructor = JSObject.global.Error.function! /// The underlying JavaScript `Error` object. public let jsObject: JSObject @@ -14,6 +14,10 @@ public final class JSError: Error { jsObject = Self.constructor.new([message]) } + public init(unsafelyWrapping jsObject: JSObject) { + self.jsObject = jsObject + } + /// The error message of the underlying `Error` object. public var message: String { jsObject.message.string! diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift index 1217d6992..371a09007 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -4,53 +4,42 @@ import _CJavaScriptKit +/// A protocol that allows a Swift numeric type to be mapped to the JavaScript TypedArray that holds integers of its type public protocol TypedArrayElement: JSValueConvertible, JSValueConstructible { + /// The constructor function for the TypedArray class for this particular kind of number static var typedArrayClass: JSFunction { get } } -/// A wrapper around [the JavaScript TypedArray class](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/TypedArray) -/// that exposes its properties in a type-safe and Swifty way. -public class JSTypedArray: JSValueConvertible, ExpressibleByArrayLiteral where Element: TypedArrayElement { - let ref: JSObject - public func jsValue() -> JSValue { - .object(ref) - } +/// A wrapper around all JavaScript [TypedArray](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray) classes that exposes their properties in a type-safe way. +/// FIXME: the BigInt-based TypedArrays are not supported (https://github.com/swiftwasm/JavaScriptKit/issues/56) +public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral where Element: TypedArrayElement { + public static var constructor: JSFunction { Element.typedArrayClass } + public var jsObject: JSObject public subscript(_ index: Int) -> Element { get { - return Element.construct(from: getJSValue(this: ref, index: Int32(index)))! + return Element.construct(from: jsObject[index])! } set { - setJSValue(this: ref, index: Int32(index), value: newValue.jsValue()) + self.jsObject[index] = newValue.jsValue() } } - // This private initializer assumes that the passed object is TypedArray - private init(unsafe object: JSObject) { - self.ref = object - } - - /// Construct a `JSTypedArray` from TypedArray `JSObject`. - /// Return `nil` if the object is not TypedArray. + /// Initialize a new instance of TypedArray in JavaScript environment with given length. + /// All the elements will be initialized to zero. /// - /// - Parameter object: A `JSObject` expected to be TypedArray - public init?(_ object: JSObject) { - guard object.isInstanceOf(Element.typedArrayClass) else { return nil } - self.ref = object + /// - Parameter length: The number of elements that will be allocated. + public init(length: Int) { + jsObject = Element.typedArrayClass.new(length) } - /// Initialize a new instance of TypedArray in JavaScript environment with given length zero value. - /// - /// - Parameter length: The length of elements that will be allocated. - public convenience init(length: Int) { - let jsObject = Element.typedArrayClass.new(length) - self.init(unsafe: jsObject) + required public init(unsafelyWrapping jsObject: JSObject) { + self.jsObject = jsObject } required public convenience init(arrayLiteral elements: Element...) { self.init(elements) } - /// Initialize a new instance of TypedArray in JavaScript environment with given elements. /// /// - Parameter array: The array that will be copied to create a new instance of TypedArray @@ -59,7 +48,7 @@ public class JSTypedArray: JSValueConvertible, ExpressibleByArrayLitera array.withUnsafeBufferPointer { ptr in _create_typed_array(Element.typedArrayClass.id, ptr.baseAddress!, Int32(array.count), &resultObj) } - self.init(unsafe: JSObject(id: resultObj)) + self.init(unsafelyWrapping: JSObject(id: resultObj)) } /// Convenience initializer for `Sequence`. @@ -90,8 +79,6 @@ extension UInt: TypedArrayElement { valueForBitWidth(typeName: "UInt", bitWidth: Int.bitWidth, when32: JSObject.global.Uint32Array).function! } -// MARK: - Concrete TypedArray classes - extension Int8: TypedArrayElement { public static var typedArrayClass = JSObject.global.Int8Array.function! } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index 71cec9d58..9ac11626e 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -62,10 +62,7 @@ public class JSFunction: JSObject { let argv = bufferPointer.baseAddress let argc = bufferPointer.count var resultObj = JavaScriptObjectRef() - _call_new( - self.id, argv, Int32(argc), - &resultObj - ) + _call_new(self.id, argv, Int32(argc), &resultObj) return JSObject(id: resultObj) } } @@ -81,6 +78,10 @@ public class JSFunction: JSObject { fatalError("unavailable") } + public override class func construct(from value: JSValue) -> Self? { + return value.function as? Self + } + override public func jsValue() -> JSValue { .function(self) } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 0d3532ab5..1342922f7 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -32,13 +32,20 @@ public class JSObject: Equatable { /// - Parameter name: The name of this object's member to access. /// - Returns: The `name` member method binding this object as `this` context. @_disfavoredOverload - public subscript(dynamicMember name: String) -> ((JSValueConvertible...) -> JSValue)? { + public subscript(_ name: String) -> ((JSValueConvertible...) -> JSValue)? { guard let function = self[name].function else { return nil } return { (arguments: JSValueConvertible...) in function(this: self, arguments: arguments) } } + /// A convenience method of `subscript(_ name: String) -> ((JSValueConvertible...) -> JSValue)?` + /// to access the member through Dynamic Member Lookup. + @_disfavoredOverload + public subscript(dynamicMember name: String) -> ((JSValueConvertible...) -> JSValue)? { + self[name] + } + /// A convenience method of `subscript(_ name: String) -> JSValue` /// to access the member through Dynamic Member Lookup. public subscript(dynamicMember name: String) -> JSValue { @@ -62,9 +69,9 @@ public class JSObject: Equatable { set { setJSValue(this: self, index: Int32(index), value: newValue) } } - /// Return `true` if this object is an instance of the `constructor`. Return `false`, if not. + /// Return `true` if this value is an instance of the passed `constructor` function. /// - Parameter constructor: The constructor function to check. - /// - Returns: The result of `instanceof` in JavaScript environment. + /// - Returns: The result of `instanceof` in the JavaScript environment. public func isInstanceOf(_ constructor: JSFunction) -> Bool { _instanceof(id, constructor.id) } @@ -86,6 +93,10 @@ public class JSObject: Equatable { return lhs.id == rhs.id } + public class func construct(from value: JSValue) -> Self? { + return value.object as? Self + } + public func jsValue() -> JSValue { .object(self) } diff --git a/Sources/JavaScriptKit/JSBridgedType.swift b/Sources/JavaScriptKit/JSBridgedType.swift new file mode 100644 index 000000000..d47be81ab --- /dev/null +++ b/Sources/JavaScriptKit/JSBridgedType.swift @@ -0,0 +1,41 @@ +/// Use this protocol when your type has no single JavaScript class. +/// For example, a union type of multiple classes or primitive values. +public protocol JSBridgedType: JSValueCodable, CustomStringConvertible { + /// This is the value your class wraps. + var value: JSValue { get } + + /// If your class is incompatible with the provided value, return `nil`. + init?(from value: JSValue) +} + +extension JSBridgedType { + public static func construct(from value: JSValue) -> Self? { + return Self.init(from: value) + } + + public func jsValue() -> JSValue { value } + + public var description: String { value.description } +} + +/// Conform to this protocol when your Swift class wraps a JavaScript class. +public protocol JSBridgedClass: JSBridgedType { + /// The constructor function for the JavaScript class + static var constructor: JSFunction { get } + + /// The JavaScript object wrapped by this instance. + /// You may assume that `jsObject instanceof Self.constructor == true` + var jsObject: JSObject { get } + + /// Create an instannce wrapping the given JavaScript object. + /// You may assume that `jsObject instanceof Self.constructor` + init(unsafelyWrapping jsObject: JSObject) +} + +extension JSBridgedClass { + public var value: JSValue { jsObject.jsValue() } + public init?(from value: JSValue) { + guard let object = value.object, object.isInstanceOf(Self.constructor) else { return nil } + self.init(unsafelyWrapping: object) + } +} diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift index 0862e59c1..d35b7aaf7 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -57,12 +57,21 @@ public enum JSValue: Equatable { /// Returns the `true` if this JS value is null. /// If not, returns `false`. - public var isNull: Bool { return self == .null } + public var isNull: Bool { + return self == .null + } /// Returns the `true` if this JS value is undefined. /// If not, returns `false`. - public var isUndefined: Bool { return self == .undefined } + public var isUndefined: Bool { + return self == .undefined + } +} +extension JSValue { + public func fromJSValue() -> Type? where Type: JSValueConstructible { + return Type.construct(from: self) + } } extension JSValue { @@ -104,7 +113,13 @@ extension JSValue: ExpressibleByStringLiteral { } extension JSValue: ExpressibleByIntegerLiteral { - public init(integerLiteral value: Double) { + public init(integerLiteral value: Int32) { + self = .number(Double(value)) + } +} + +extension JSValue: ExpressibleByFloatLiteral { + public init(floatLiteral value: Double) { self = .number(value) } } @@ -144,3 +159,39 @@ public func setJSValue(this: JSObject, index: Int32, value: JSValue) { rawValue.payload1, rawValue.payload2, rawValue.payload3) } } + +extension JSValue { + /// Return `true` if this value is an instance of the passed `constructor` function. + /// Returns `false` for everything except objects and functions. + /// - Parameter constructor: The constructor function to check. + /// - Returns: The result of `instanceof` in the JavaScript environment. + public func isInstanceOf(_ constructor: JSFunction) -> Bool { + switch self { + case .boolean, .string, .number, .null, .undefined: + return false + case let .object(ref): + return ref.isInstanceOf(constructor) + case let .function(ref): + return ref.isInstanceOf(constructor) + } + } +} + +extension JSValue: CustomStringConvertible { + public var description: String { + switch self { + case let .boolean(boolean): + return boolean.description + case .string(let string): + return string + case .number(let number): + return number.description + case .object(let object), .function(let object as JSObject): + return object.toString!().fromJSValue()! + case .null: + return "null" + case .undefined: + return "undefined" + } + } +} diff --git a/Sources/JavaScriptKit/JSValueConstructible.swift b/Sources/JavaScriptKit/JSValueConstructible.swift index 59a8c63d2..ae380e6b3 100644 --- a/Sources/JavaScriptKit/JSValueConstructible.swift +++ b/Sources/JavaScriptKit/JSValueConstructible.swift @@ -1,7 +1,7 @@ /// Types conforming to this protocol can be constructed from `JSValue`. public protocol JSValueConstructible { /// Construct an instance of `Self`, if possible, from the given `JSValue`. - /// Return `nil` if fail to construct. + /// Return `nil` if the value is not compatible with the conforming Swift type. /// /// - Parameter value: The `JSValue` to decode /// - Returns: An instance of `Self`, if one was successfully constructed from the value. diff --git a/Sources/JavaScriptKit/JSValueConvertible.swift b/Sources/JavaScriptKit/JSValueConvertible.swift index 4b4c7e641..e8008b9de 100644 --- a/Sources/JavaScriptKit/JSValueConvertible.swift +++ b/Sources/JavaScriptKit/JSValueConvertible.swift @@ -1,12 +1,18 @@ import _CJavaScriptKit -/// Confirming types are convertible to `JSValue`. +/// Objects that can be converted to a JavaScript value, preferably in a lossless manner. public protocol JSValueConvertible { - /// Convert this object into a `JSValue`. + /// Create a JSValue that represents this object func jsValue() -> JSValue } -extension JSValue: JSValueConvertible { +public typealias JSValueCodable = JSValueConvertible & JSValueConstructible + +extension JSValue: JSValueCodable { + public static func construct(from value: JSValue) -> Self? { + return value + } + public func jsValue() -> JSValue { self } } @@ -18,20 +24,20 @@ extension Int: JSValueConvertible { public func jsValue() -> JSValue { .number(Double(self)) } } -extension Int8: JSValueConvertible { +extension UInt: JSValueConvertible { public func jsValue() -> JSValue { .number(Double(self)) } } -extension Int16: JSValueConvertible { +extension Float: JSValueConvertible { public func jsValue() -> JSValue { .number(Double(self)) } } -extension Int32: JSValueConvertible { - public func jsValue() -> JSValue { .number(Double(self)) } +extension Double: JSValueConvertible { + public func jsValue() -> JSValue { .number(self) } } -extension UInt: JSValueConvertible { - public func jsValue() -> JSValue { .number(Double(self)) } +extension String: JSValueConvertible { + public func jsValue() -> JSValue { .string(self) } } extension UInt8: JSValueConvertible { @@ -43,27 +49,36 @@ extension UInt16: JSValueConvertible { } extension UInt32: JSValueConvertible { - public func jsValue() -> JSValue { .number(Double(self)) } + public func jsValue() -> JSValue { .number(Double(self)) } } -extension Float: JSValueConvertible { +extension UInt64: JSValueConvertible { public func jsValue() -> JSValue { .number(Double(self)) } } -extension Double: JSValueConvertible { - public func jsValue() -> JSValue { .number(self) } +extension Int8: JSValueConvertible { + public func jsValue() -> JSValue { .number(Double(self)) } } -extension String: JSValueConvertible { - public func jsValue() -> JSValue { .string(self) } +extension Int16: JSValueConvertible { + public func jsValue() -> JSValue { .number(Double(self)) } } -extension JSObject: JSValueConvertible { +extension Int32: JSValueConvertible { + public func jsValue() -> JSValue { .number(Double(self)) } +} + +extension Int64: JSValueConvertible { + public func jsValue() -> JSValue { .number(Double(self)) } +} + +extension JSObject: JSValueCodable { // `JSObject.jsValue` is defined in JSObject.swift to be able to overridden // from `JSFunction` } -private let Object = JSObject.global.Object.function! +private let objectConstructor = JSObject.global.Object.function! +private let arrayConstructor = JSObject.global.Array.function! extension Dictionary where Value: JSValueConvertible, Key == String { public func jsValue() -> JSValue { @@ -73,7 +88,7 @@ extension Dictionary where Value: JSValueConvertible, Key == String { extension Dictionary: JSValueConvertible where Value == JSValueConvertible, Key == String { public func jsValue() -> JSValue { - let object = Object.new() + let object = objectConstructor.new() for (key, value) in self { object[key] = value.jsValue() } @@ -81,17 +96,54 @@ extension Dictionary: JSValueConvertible where Value == JSValueConvertible, Key } } -private let Array = JSObject.global.Array.function! +extension Dictionary: JSValueConstructible where Value: JSValueConstructible, Key == String { + public static func construct(from value: JSValue) -> Self? { + guard + let objectRef = value.object, + let keys: [String] = objectConstructor.keys!(objectRef.jsValue()).fromJSValue() + else { return nil } + + var entries = [(String, Value)]() + entries.reserveCapacity(keys.count) + for key in keys { + guard let value: Value = objectRef[key].fromJSValue() else { + return nil + } + entries.append((key, value)) + } + return Dictionary(uniqueKeysWithValues: entries) + } +} + +extension Optional: JSValueConstructible where Wrapped: JSValueConstructible { + public static func construct(from value: JSValue) -> Self? { + switch value { + case .null, .undefined: + return nil + default: + return Wrapped.construct(from: value) + } + } +} + +extension Optional: JSValueConvertible where Wrapped: JSValueConvertible { + public func jsValue() -> JSValue { + switch self { + case .none: return .null + case let .some(wrapped): return wrapped.jsValue() + } + } +} extension Array where Element: JSValueConvertible { public func jsValue() -> JSValue { - Swift.Array.jsValue(self)() + Array.jsValue(self)() } } extension Array: JSValueConvertible where Element == JSValueConvertible { public func jsValue() -> JSValue { - let array = Array.new(count) + let array = arrayConstructor.new(count) for (index, element) in enumerated() { array[index] = element.jsValue() } @@ -99,6 +151,26 @@ extension Array: JSValueConvertible where Element == JSValueConvertible { } } +extension Array: JSValueConstructible where Element: JSValueConstructible { + public static func construct(from value: JSValue) -> [Element]? { + guard + let objectRef = value.object, + objectRef.isInstanceOf(JSObject.global.Array.function!) + else { return nil } + + let count: Int = objectRef.length.fromJSValue()! + var array = [Element]() + array.reserveCapacity(count) + + for i in 0 ..< count { + guard let value: Element = objectRef[i].fromJSValue() else { return nil } + array.append(value) + } + + return array + } +} + extension RawJSValue: JSValueConvertible { public func jsValue() -> JSValue { switch kind { @@ -194,6 +266,6 @@ extension Array where Element == JSValueConvertible { extension Array where Element: JSValueConvertible { func withRawJSValues(_ body: ([RawJSValue]) -> T) -> T { - Swift.Array.withRawJSValues(self)(body) + [JSValueConvertible].withRawJSValues(self)(body) } }