From b9fb002f397cc88d27a134752ac93f0f8d6226e3 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 3 Jun 2020 14:53:51 -0500 Subject: [PATCH 01/38] Added strongly typed classes --- Package@swift-5.2.swift | 21 ++++ Sources/JavaScriptKit/JS Types/JSArray.swift | 106 ++++++++++++++++++ .../JavaScriptKit/JS Types/JSBluetooth.swift | 36 ++++++ .../JavaScriptKit/JS Types/JSNavigator.swift | 28 +++++ Sources/JavaScriptKit/JS Types/JSObject.swift | 67 +++++++++++ .../JavaScriptKit/JS Types/JSPromise.swift | 21 ++++ Sources/JavaScriptKit/JS Types/JSType.swift | 28 +++++ Sources/JavaScriptKit/JSArrayRef.swift | 41 ------- .../{JSFunction.swift => JSFunctionRef.swift} | 10 +- .../{JSObject.swift => JSObjectRef.swift} | 45 ++++++-- Sources/JavaScriptKit/JSValue.swift | 42 ++++--- .../JavaScriptKit/JSValueConvertible.swift | 52 ++++----- .../_CJavaScriptKit/include/_CJavaScriptKit.h | 18 +-- 13 files changed, 406 insertions(+), 109 deletions(-) create mode 100644 Package@swift-5.2.swift create mode 100644 Sources/JavaScriptKit/JS Types/JSArray.swift create mode 100644 Sources/JavaScriptKit/JS Types/JSBluetooth.swift create mode 100644 Sources/JavaScriptKit/JS Types/JSNavigator.swift create mode 100644 Sources/JavaScriptKit/JS Types/JSObject.swift create mode 100644 Sources/JavaScriptKit/JS Types/JSPromise.swift create mode 100644 Sources/JavaScriptKit/JS Types/JSType.swift delete mode 100644 Sources/JavaScriptKit/JSArrayRef.swift rename Sources/JavaScriptKit/{JSFunction.swift => JSFunctionRef.swift} (92%) rename Sources/JavaScriptKit/{JSObject.swift => JSObjectRef.swift} (71%) diff --git a/Package@swift-5.2.swift b/Package@swift-5.2.swift new file mode 100644 index 000000000..2509ff9dd --- /dev/null +++ b/Package@swift-5.2.swift @@ -0,0 +1,21 @@ +// swift-tools-version:5.2 + +import PackageDescription + +let package = Package( + name: "JavaScriptKit", + products: [ + .library(name: "JavaScriptKit", targets: ["JavaScriptKit"]) + ], + targets: [ + .target( + name: "JavaScriptKit", + dependencies: ["_CJavaScriptKit"] + ), + .target(name: "_CJavaScriptKit"), + .testTarget( + name: "JavaScriptKitTests", + dependencies: ["JavaScriptKit"] + ) + ] +) diff --git a/Sources/JavaScriptKit/JS Types/JSArray.swift b/Sources/JavaScriptKit/JS Types/JSArray.swift new file mode 100644 index 000000000..4727ef01c --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSArray.swift @@ -0,0 +1,106 @@ +/// JavaScript Array +public final class JSArray: JSType { + + // MARK: - Properties + + public let jsObject: JSObjectRef + + // MARK: - Initialization + + public init?(_ jsObject: JSObjectRef) { + guard Self.isArray(jsObject) + else { return nil } + self.jsObject = jsObject + } + + public init(count: Int = 0) { + self.jsObject = Self.classObject.new(count) + assert(Self.isArray(jsObject)) + } + + public convenience init(_ collection: C) where C: Collection, C.Element == JSValue { + self.init(count: collection.count) + collection.enumerated().forEach { self[$0.offset] = $0.element } + } + + public convenience init(_ collection: C) where C: Collection, C.Element == JSValueConvertible { + self.init(collection.lazy.map({ $0.jsValue() })) + } +} + +internal extension JSArray { + + static let classObject = JSObjectRef.global.Array.function! + + static func isArray(_ object: JSObjectRef) -> Bool { + classObject.isArray.function?(object).boolean ?? false + } +} + +// MARK: - Deprecated + +@available(*, renamed: "JSArray") +public typealias JSArrayRef = JSArray + +// MARK: - ExpressibleByArrayLiteral + +extension JSArray: ExpressibleByArrayLiteral { + + public convenience init(arrayLiteral elements: JSValueConvertible...) { + self.init(elements) + } +} + +// MARK: - CustomStringConvertible + +extension JSArray: CustomStringConvertible { + + public var description: String { + return toString() ?? "" + } +} + +// MARK: - Sequence + +extension JSArray: Sequence { + + public typealias Element = JSValue + + public func makeIterator() -> IndexingIterator { + return IndexingIterator(_elements: self) + } +} + +// MARK: - Collection + +extension JSArray: RandomAccessCollection { + + public var count: Int { + return Int(jsObject.length.number ?? 0) + } + + public subscript (index: Int) -> JSValue { + get { return jsObject.get(index) } + set { jsObject.set(index, newValue) } + } + + /// The start `Index`. + public var startIndex: Int { + return 0 + } + + /// The end `Index`. + /// + /// This is the "one-past-the-end" position, and will always be equal to the `count`. + public var endIndex: Int { + return count + } + + public func index(before i: Int) -> Int { + return i - 1 + } + + public func index(after i: Int) -> Int { + return i + 1 + } +} diff --git a/Sources/JavaScriptKit/JS Types/JSBluetooth.swift b/Sources/JavaScriptKit/JS Types/JSBluetooth.swift new file mode 100644 index 000000000..d185ca8ce --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSBluetooth.swift @@ -0,0 +1,36 @@ +// +// JSBluetooth.swift +// +// +// Created by Alsey Coleman Miller on 6/3/20. +// + +/// JavaScript Bluetooth object +public final class JSBluetooth: JSType { + + // MARK: - Properties + + public let jsObject: JSObjectRef + + // MARK: - Initialization + + public init(_ jsObject: JSObjectRef) { + self.jsObject = jsObject + } + + public static var shared: JSBluetooth? { return JSNavigator.shared?.bluetooth } + + // MARK: - Methods + + public func requestDevice() { + + guard let function = jsObject.requestDevice.function + else { assertionFailure("Nil \(#function)"); return } + + let result = function() + + + } + + +} diff --git a/Sources/JavaScriptKit/JS Types/JSNavigator.swift b/Sources/JavaScriptKit/JS Types/JSNavigator.swift new file mode 100644 index 000000000..c30a282d0 --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSNavigator.swift @@ -0,0 +1,28 @@ +// +// JSNavigator.swift +// +// +// Created by Alsey Coleman Miller on 6/3/20. +// + +/// JavaScript Navigator object +public final class JSNavigator: JSType { + + // MARK: - Properties + + public let jsObject: JSObjectRef + + // MARK: - Initialization + + public init(_ jsObject: JSObjectRef) { + self.jsObject = jsObject + // TODO: validate JS class + } + + public static let shared = JSObjectRef.global.navigator.object.flatMap { JSNavigator($0) } + + // MARK: - Properties + + public lazy var bluetooth = jsObject.bluetooth.object.flatMap { JSBluetooth($0) } +} + diff --git a/Sources/JavaScriptKit/JS Types/JSObject.swift b/Sources/JavaScriptKit/JS Types/JSObject.swift new file mode 100644 index 000000000..187fe8b0c --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSObject.swift @@ -0,0 +1,67 @@ +// +// JSObject.swift +// +// +// Created by Alsey Coleman Miller on 6/3/20. +// + +/// JavaScript Object +public final class JSObject: JSType { + + // MARK: - Properties + + public let jsObject: JSObjectRef + + // MARK: - Initialization + + public init?(_ jsObject: JSObjectRef) { + guard Self.isObject(jsObject) + else { return nil } + self.jsObject = jsObject + } + + public init() { + self.jsObject = Self.classObject.new() + assert(Self.isObject(jsObject)) + } + + public convenience init(_ elements: [(key: String, value: JSValue)]) { + self.init() + elements.forEach { self[$0.key] = $0.value } + } +} + +internal extension JSObject { + + static let classObject = JSObjectRef.global.Object.function! + + static func isObject(_ object: JSObjectRef) -> Bool { + classObject.isObject.function?(object).boolean ?? false + } +} + +public extension JSObject { + + subscript (key: String) -> JSValue { + get { jsObject.get(key) } + set { jsObject.set(key, newValue) } + } +} + +// MARK: - ExpressibleByDictionaryLiteral + +extension JSObject: ExpressibleByDictionaryLiteral { + + public convenience init(dictionaryLiteral elements: (String, JSValue)...) { + self.init(elements) + } +} + +// MARK: - CustomStringConvertible + +extension JSObject: CustomStringConvertible { + + public var description: String { + return toString() ?? "" + } +} diff --git a/Sources/JavaScriptKit/JS Types/JSPromise.swift b/Sources/JavaScriptKit/JS Types/JSPromise.swift new file mode 100644 index 000000000..d022c1206 --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSPromise.swift @@ -0,0 +1,21 @@ +// +// JSPromise.swift +// +// +// Created by Alsey Coleman Miller on 6/3/20. +// + +public final class JSPromise: JSType { + + // MARK: - Properties + + public let jsObject: JSObjectRef + + // MARK: - Initialization + + public init?(_ jsObject: JSObjectRef) { + self.jsObject = jsObject + // validate if promise type + } + +} diff --git a/Sources/JavaScriptKit/JS Types/JSType.swift b/Sources/JavaScriptKit/JS Types/JSType.swift new file mode 100644 index 000000000..3e5cb48ab --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSType.swift @@ -0,0 +1,28 @@ +// +// JSType.swift +// +// +// Created by Alsey Coleman Miller on 6/3/20. +// + +/// JavaScript Type (Class) +public protocol JSType: JSValueConvertible { + + init?(_ jsObject: JSObjectRef) + + var jsObject: JSObjectRef { get } +} + +public extension JSType { + + func toString() -> String? { + return jsObject.toString.function?().string + } +} + +// MARK: - JSValueConvertible + +public extension JSType { + + func jsValue() -> JSValue { return .object(jsObject) } +} diff --git a/Sources/JavaScriptKit/JSArrayRef.swift b/Sources/JavaScriptKit/JSArrayRef.swift deleted file mode 100644 index a1eea210a..000000000 --- a/Sources/JavaScriptKit/JSArrayRef.swift +++ /dev/null @@ -1,41 +0,0 @@ - -public class JSArrayRef { - - static let classObject = JSObjectRef.global.Array.function! - - static func isArray(_ object: JSObjectRef) -> Bool { - classObject.isArray.function!(object).boolean! - } - - let ref: JSObjectRef - - public init?(_ ref: JSObjectRef) { - guard Self.isArray(ref) else { return nil } - self.ref = ref - } -} - - -extension JSArrayRef: Sequence { - public typealias Element = JSValue - - public func makeIterator() -> Iterator { - Iterator(ref: ref) - } - - public class Iterator: IteratorProtocol { - let ref: JSObjectRef - var index = 0 - init(ref: JSObjectRef) { - self.ref = ref - } - public func next() -> Element? { - defer { index += 1 } - guard index < Int(ref.length.number!) else { - return nil - } - let value = ref.get(index) - return value.isNull ? nil : value - } - } -} diff --git a/Sources/JavaScriptKit/JSFunction.swift b/Sources/JavaScriptKit/JSFunctionRef.swift similarity index 92% rename from Sources/JavaScriptKit/JSFunction.swift rename to Sources/JavaScriptKit/JSFunctionRef.swift index 4d2acaf95..70180522a 100644 --- a/Sources/JavaScriptKit/JSFunction.swift +++ b/Sources/JavaScriptKit/JSFunctionRef.swift @@ -55,6 +55,7 @@ public class JSFunctionRef: JSObjectRef { } static var sharedFunctions: [([JSValue]) -> JSValue] = [] + public static func from(_ body: @escaping ([JSValue]) -> JSValue) -> JSFunctionRef { let id = JavaScriptHostFuncRef(sharedFunctions.count) sharedFunctions.append(body) @@ -63,26 +64,29 @@ public class JSFunctionRef: JSObjectRef { return JSFunctionRef(id: funcRef) } + + // MARK: - JSValueConvertible public override func jsValue() -> JSValue { .function(self) } } +// MARK: - C Functions @_cdecl("swjs_prepare_host_function_call") -public func _prepare_host_function_call(_ argc: Int32) -> UnsafeMutableRawPointer { +internal func _prepare_host_function_call(_ argc: Int32) -> UnsafeMutableRawPointer { let argumentSize = MemoryLayout.size * Int(argc) return malloc(Int(argumentSize))! } @_cdecl("swjs_cleanup_host_function_call") -public func _cleanup_host_function_call(_ pointer: UnsafeMutableRawPointer) { +internal func _cleanup_host_function_call(_ pointer: UnsafeMutableRawPointer) { free(pointer) } @_cdecl("swjs_call_host_function") -public func _call_host_function( +internal func _call_host_function( _ hostFuncRef: JavaScriptHostFuncRef, _ argv: UnsafePointer, _ argc: Int32, _ callbackFuncRef: JavaScriptObjectRef) { diff --git a/Sources/JavaScriptKit/JSObject.swift b/Sources/JavaScriptKit/JSObjectRef.swift similarity index 71% rename from Sources/JavaScriptKit/JSObject.swift rename to Sources/JavaScriptKit/JSObjectRef.swift index 1dcf16736..a783e9065 100644 --- a/Sources/JavaScriptKit/JSObject.swift +++ b/Sources/JavaScriptKit/JSObjectRef.swift @@ -1,11 +1,23 @@ import _CJavaScriptKit @dynamicMemberLookup -public class JSObjectRef: Equatable { - let id: UInt32 - init(id: UInt32) { +public class JSObjectRef { + + public typealias ID = UInt32 + + // MARK: - Properties + + public let id: ID + + // MARK: - Initialization + + deinit { _destroy_ref(id) } + + public init(id: ID) { self.id = id } + + // MARK: - Methods @_disfavoredOverload public subscript(dynamicMember name: String) -> ((JSValueConvertible...) -> JSValue)? { @@ -43,16 +55,29 @@ public class JSObjectRef: Equatable { setJSValue(this: self, index: Int32(index), value: value) } - static let _JS_Predef_Value_Global: UInt32 = 0 - public static let global = JSObjectRef(id: _JS_Predef_Value_Global) + public func jsValue() -> JSValue { + .object(self) + } +} + +// MARK: - Constants - deinit { _destroy_ref(id) } +public extension JSObjectRef { + + internal static let _JS_Predef_Value_Global: UInt32 = 0 + + static let global = JSObjectRef(id: _JS_Predef_Value_Global) +} + +// MARK: - Equatable +extension JSObjectRef: Equatable { + public static func == (lhs: JSObjectRef, rhs: JSObjectRef) -> Bool { return lhs.id == rhs.id } - - public func jsValue() -> JSValue { - .object(self) - } } + +// MARK: - Identifiable + +extension JSObjectRef: Identifiable { } diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift index 3a359cf4a..b7f119253 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -1,6 +1,7 @@ import _CJavaScriptKit public enum JSValue: Equatable { + case boolean(Bool) case string(String) case number(Double) @@ -8,38 +9,43 @@ public enum JSValue: Equatable { case null case undefined case function(JSFunctionRef) +} + +// MARK: - Accessors - public var boolean: Bool? { +public extension JSValue { + + var boolean: Bool? { switch self { case let .boolean(boolean): return boolean default: return nil } } - public var string: String? { + var string: String? { switch self { case let .string(string): return string default: return nil } } - public var number: Double? { + var number: Double? { switch self { case let .number(number): return number default: return nil } } - public var object: JSObjectRef? { + var object: JSObjectRef? { switch self { case let .object(object): return object default: return nil } } - public var array: JSArrayRef? { + var array: JSArrayRef? { object.flatMap { JSArrayRef($0) } } - public var isNull: Bool { return self == .null } - public var isUndefined: Bool { return self == .undefined } - public var function: JSFunctionRef? { + var isNull: Bool { return self == .null } + var isUndefined: Bool { return self == .undefined } + var function: JSFunctionRef? { switch self { case let .function(function): return function default: return nil @@ -47,25 +53,32 @@ public enum JSValue: Equatable { } } + extension JSValue { public static func function(_ body: @escaping ([JSValue]) -> JSValue) -> JSValue { .function(JSFunctionRef.from(body)) } } +// MARK: - ExpressibleByStringLiteral + extension JSValue: ExpressibleByStringLiteral { public init(stringLiteral value: String) { self = .string(value) } } -extension JSValue: ExpressibleByIntegerLiteral { - public init(integerLiteral value: Double) { +// MARK: - ExpressibleByFloatLiteral + +extension JSValue: ExpressibleByFloatLiteral { + public init(floatLiteral value: Double) { self = .number(value) } } -public func getJSValue(this: JSObjectRef, name: String) -> JSValue { +// MARK: - Functions + +internal func getJSValue(this: JSObjectRef, name: String) -> JSValue { var rawValue = RawJSValue() _get_prop(this.id, name, Int32(name.count), &rawValue.kind, @@ -73,14 +86,13 @@ public func getJSValue(this: JSObjectRef, name: String) -> JSValue { return rawValue.jsValue() } -public func setJSValue(this: JSObjectRef, name: String, value: JSValue) { +internal func setJSValue(this: JSObjectRef, name: String, value: JSValue) { value.withRawJSValue { rawValue in _set_prop(this.id, name, Int32(name.count), rawValue.kind, rawValue.payload1, rawValue.payload2, rawValue.payload3) } } - -public func getJSValue(this: JSObjectRef, index: Int32) -> JSValue { +internal func getJSValue(this: JSObjectRef, index: Int32) -> JSValue { var rawValue = RawJSValue() _get_subscript(this.id, index, &rawValue.kind, @@ -89,7 +101,7 @@ public func getJSValue(this: JSObjectRef, index: Int32) -> JSValue { } -public func setJSValue(this: JSObjectRef, index: Int32, value: JSValue) { +internal func setJSValue(this: JSObjectRef, index: Int32, value: JSValue) { value.withRawJSValue { rawValue in _set_subscript(this.id, index, rawValue.kind, diff --git a/Sources/JavaScriptKit/JSValueConvertible.swift b/Sources/JavaScriptKit/JSValueConvertible.swift index 4b6811d2f..f11becdb6 100644 --- a/Sources/JavaScriptKit/JSValueConvertible.swift +++ b/Sources/JavaScriptKit/JSValueConvertible.swift @@ -57,8 +57,6 @@ extension JSObjectRef: JSValueConvertible { // from `JSFunctionRef` } -private let Object = JSObjectRef.global.Object.function! - extension Dictionary where Value: JSValueConvertible, Key == String { public func jsValue() -> JSValue { Swift.Dictionary.jsValue(self)() @@ -67,16 +65,12 @@ extension Dictionary where Value: JSValueConvertible, Key == String { extension Dictionary: JSValueConvertible where Value == JSValueConvertible, Key == String { public func jsValue() -> JSValue { - let object = Object.new() - for (key, value) in self { - object.set(key, value.jsValue()) - } - return .object(object) + let object = JSObject() + self.forEach { object[$0.key] = $0.value.jsValue() } + return .object(object.jsObject) } } -private let Array = JSObjectRef.global.Array.function! - extension Array where Element: JSValueConvertible { public func jsValue() -> JSValue { Swift.Array.jsValue(self)() @@ -85,24 +79,20 @@ extension Array where Element: JSValueConvertible { extension Array: JSValueConvertible where Element == JSValueConvertible { public func jsValue() -> JSValue { - let array = Array.new(count) - for (index, element) in self.enumerated() { - array[index] = element.jsValue() - } - return .object(array) + return .object(JSArray(self).jsObject) } } extension RawJSValue: JSValueConvertible { public func jsValue() -> JSValue { switch kind { - case JavaScriptValueKind_Invalid: + case .invalid: fatalError() - case JavaScriptValueKind_Boolean: + case .boolean: return .boolean(payload1 != 0) - case JavaScriptValueKind_Number: + case .number: return .number(payload3) - case JavaScriptValueKind_String: + case .string: // +1 for null terminator let buffer = malloc(Int(payload2 + 1))!.assumingMemoryBound(to: UInt8.self) defer { free(buffer) } @@ -110,16 +100,16 @@ extension RawJSValue: JSValueConvertible { buffer[Int(payload2)] = 0 let string = String(decodingCString: UnsafePointer(buffer), as: UTF8.self) return .string(string) - case JavaScriptValueKind_Object: + case .object: return .object(JSObjectRef(id: UInt32(payload1))) - case JavaScriptValueKind_Null: + case .null: return .null - case JavaScriptValueKind_Undefined: + case .undefined: return .undefined - case JavaScriptValueKind_Function: + case .function: return .function(JSFunctionRef(id: UInt32(payload1))) - default: - fatalError("unreachable") + @unknown default: + fatalError("Unreachable") } } } @@ -132,35 +122,35 @@ extension JSValue { var payload3: JavaScriptPayload3 = 0 switch self { case let .boolean(boolValue): - kind = JavaScriptValueKind_Boolean + kind = .boolean payload1 = boolValue ? 1 : 0 payload2 = 0 case let .number(numberValue): - kind = JavaScriptValueKind_Number + kind = .number payload1 = 0 payload2 = 0 payload3 = numberValue case var .string(stringValue): - kind = JavaScriptValueKind_String + kind = .string return stringValue.withUTF8 { bufferPtr in let ptrValue = UInt32(UInt(bitPattern: bufferPtr.baseAddress!)) let rawValue = RawJSValue(kind: kind, payload1: JavaScriptPayload1(ptrValue), payload2: JavaScriptPayload2(bufferPtr.count), payload3: 0) return body(rawValue) } case let .object(ref): - kind = JavaScriptValueKind_Object + kind = .object payload1 = JavaScriptPayload1(ref.id) payload2 = 0 case .null: - kind = JavaScriptValueKind_Null + kind = .null payload1 = 0 payload2 = 0 case .undefined: - kind = JavaScriptValueKind_Undefined + kind = .undefined payload1 = 0 payload2 = 0 case let .function(functionRef): - kind = JavaScriptValueKind_Function + kind = .function payload1 = JavaScriptPayload1(functionRef.id) payload2 = 0 } diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index b488f49af..4b10e9b2c 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -6,15 +6,15 @@ typedef unsigned int JavaScriptObjectRef; typedef unsigned int JavaScriptHostFuncRef; -typedef enum { - JavaScriptValueKind_Invalid = -1, - JavaScriptValueKind_Boolean = 0, - JavaScriptValueKind_String = 1, - JavaScriptValueKind_Number = 2, - JavaScriptValueKind_Object = 3, - JavaScriptValueKind_Null = 4, - JavaScriptValueKind_Undefined = 5, - JavaScriptValueKind_Function = 6, +typedef enum __attribute__((enum_extensibility(closed))) { + JavaScriptValueKindInvalid = -1, + JavaScriptValueKindBoolean = 0, + JavaScriptValueKindString = 1, + JavaScriptValueKindNumber = 2, + JavaScriptValueKindObject = 3, + JavaScriptValueKindNull = 4, + JavaScriptValueKindUndefined = 5, + JavaScriptValueKindFunction = 6, } JavaScriptValueKind; typedef unsigned JavaScriptPayload1; From ff138141b26cae7cbfefed18506217df71bc7400 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 3 Jun 2020 15:01:49 -0500 Subject: [PATCH 02/38] Added `JSPromise` --- Sources/JavaScriptKit/JS Types/JSArray.swift | 2 +- Sources/JavaScriptKit/JS Types/JSBluetooth.swift | 4 +++- Sources/JavaScriptKit/JS Types/JSPromise.swift | 8 ++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Sources/JavaScriptKit/JS Types/JSArray.swift b/Sources/JavaScriptKit/JS Types/JSArray.swift index 4727ef01c..b5475a75a 100644 --- a/Sources/JavaScriptKit/JS Types/JSArray.swift +++ b/Sources/JavaScriptKit/JS Types/JSArray.swift @@ -24,7 +24,7 @@ public final class JSArray: JSType { } public convenience init(_ collection: C) where C: Collection, C.Element == JSValueConvertible { - self.init(collection.lazy.map({ $0.jsValue() })) + self.init(collection.map({ $0.jsValue() })) } } diff --git a/Sources/JavaScriptKit/JS Types/JSBluetooth.swift b/Sources/JavaScriptKit/JS Types/JSBluetooth.swift index d185ca8ce..7af3bd08a 100644 --- a/Sources/JavaScriptKit/JS Types/JSBluetooth.swift +++ b/Sources/JavaScriptKit/JS Types/JSBluetooth.swift @@ -27,7 +27,9 @@ public final class JSBluetooth: JSType { guard let function = jsObject.requestDevice.function else { assertionFailure("Nil \(#function)"); return } - let result = function() + let promise = JSPromise() + + let result = function(promise) } diff --git a/Sources/JavaScriptKit/JS Types/JSPromise.swift b/Sources/JavaScriptKit/JS Types/JSPromise.swift index d022c1206..edcbf574b 100644 --- a/Sources/JavaScriptKit/JS Types/JSPromise.swift +++ b/Sources/JavaScriptKit/JS Types/JSPromise.swift @@ -18,4 +18,12 @@ public final class JSPromise: JSType { // validate if promise type } + public init() { + self.jsObject = Self.classObject.new() + } +} + +internal extension JSPromise { + + static let classObject = JSObjectRef.global.Promise.function! } From 94c7dbbf6f1f3ad6c02f0a45fd7eb4c2d74d63e7 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 3 Jun 2020 17:38:50 -0500 Subject: [PATCH 03/38] Working on `JSPromise` --- Sources/JavaScriptKit/JS Types/JSArray.swift | 9 -- .../JavaScriptKit/JS Types/JSBluetooth.swift | 30 ++++-- .../JS Types/JSBluetoothDevice.swift | 22 +++++ .../JavaScriptKit/JS Types/JSBoolean.swift | 29 ++++++ Sources/JavaScriptKit/JS Types/JSError.swift | 23 +++++ Sources/JavaScriptKit/JS Types/JSObject.swift | 9 -- .../JavaScriptKit/JS Types/JSPromise.swift | 95 ++++++++++++++++++- Sources/JavaScriptKit/JS Types/JSType.swift | 18 +++- 8 files changed, 201 insertions(+), 34 deletions(-) create mode 100644 Sources/JavaScriptKit/JS Types/JSBluetoothDevice.swift create mode 100644 Sources/JavaScriptKit/JS Types/JSBoolean.swift create mode 100644 Sources/JavaScriptKit/JS Types/JSError.swift diff --git a/Sources/JavaScriptKit/JS Types/JSArray.swift b/Sources/JavaScriptKit/JS Types/JSArray.swift index b5475a75a..0d78778b2 100644 --- a/Sources/JavaScriptKit/JS Types/JSArray.swift +++ b/Sources/JavaScriptKit/JS Types/JSArray.swift @@ -51,15 +51,6 @@ extension JSArray: ExpressibleByArrayLiteral { } } -// MARK: - CustomStringConvertible - -extension JSArray: CustomStringConvertible { - - public var description: String { - return toString() ?? "" - } -} - // MARK: - Sequence extension JSArray: Sequence { diff --git a/Sources/JavaScriptKit/JS Types/JSBluetooth.swift b/Sources/JavaScriptKit/JS Types/JSBluetooth.swift index 7af3bd08a..5d2bb3292 100644 --- a/Sources/JavaScriptKit/JS Types/JSBluetooth.swift +++ b/Sources/JavaScriptKit/JS Types/JSBluetooth.swift @@ -22,17 +22,35 @@ public final class JSBluetooth: JSType { // MARK: - Methods - public func requestDevice() { + /// Returns a Promise to a BluetoothDevice object with the specified options. + /// If there is no chooser UI, this method returns the first device matching the criteria. + /// + /// - Returns: A Promise to a `BluetoothDevice` object. + public func requestDevice(//filters: [] = [], + //services: [] = [], + acceptAllDevices: Bool = true) -> JSPromise { + + enum Option: String { + case filters + case optionalServices + case acceptAllDevices + } + + // Bluetooth.requestDevice([options]) + // .then(function(bluetoothDevice) { ... }) guard let function = jsObject.requestDevice.function - else { assertionFailure("Nil \(#function)"); return } + else { fatalError("Invalid function \(jsObject.requestDevice)") } - let promise = JSPromise() + // FIXME: Improve, support all options + let options = JSObject() + options[Option.acceptAllDevices.rawValue] = acceptAllDevices.jsValue() - let result = function(promise) + let result = function(options.jsValue()) + guard let promise = result.object.flatMap({ JSPromise($0) }) + else { fatalError("Invalid object \(result)") } + return promise } - - } diff --git a/Sources/JavaScriptKit/JS Types/JSBluetoothDevice.swift b/Sources/JavaScriptKit/JS Types/JSBluetoothDevice.swift new file mode 100644 index 000000000..3adc0ac81 --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSBluetoothDevice.swift @@ -0,0 +1,22 @@ +// +// JSBluetoothDevice.swift +// +// +// Created by Alsey Coleman Miller on 6/3/20. +// + +/// JavaScript Bluetooth Device object. +public final class JSBluetoothDevice: JSType { + + // MARK: - Properties + + public let jsObject: JSObjectRef + + // MARK: - Initialization + + public init(_ jsObject: JSObjectRef) { + self.jsObject = jsObject + } + + +} diff --git a/Sources/JavaScriptKit/JS Types/JSBoolean.swift b/Sources/JavaScriptKit/JS Types/JSBoolean.swift new file mode 100644 index 000000000..b3b61b02d --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSBoolean.swift @@ -0,0 +1,29 @@ +// +// File.swift +// +// +// Created by Alsey Coleman Miller on 6/3/20. +// + +/// The Boolean object is an object wrapper for a boolean value. +public final class JSBoolean: JSType { + + // MARK: - Properties + + public let jsObject: JSObjectRef + + // MARK: - Initialization + + public init(_ jsObject: JSObjectRef) { + self.jsObject = jsObject + } + + public init(value: Bool) { + self.jsObject = Self.classObject.new(value.jsValue()) + } +} + +internal extension JSBoolean { + + static let classObject = JSObjectRef.global.Boolean.function! +} diff --git a/Sources/JavaScriptKit/JS Types/JSError.swift b/Sources/JavaScriptKit/JS Types/JSError.swift new file mode 100644 index 000000000..1b7a1626a --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSError.swift @@ -0,0 +1,23 @@ +// +// JSError +// +// +// Created by Alsey Coleman Miller on 6/3/20. +// + +public extension JSValue { + + /// Cast error to JavaScript value. + init(error: Error) { + if let value = error as? JSValueConvertible { + // convert to JavaScript value if supported. + self = value.jsValue() + } else if let stringConvertible = error as? CustomStringConvertible { + // use decription for error + self = stringConvertible.description.jsValue() + } else { + // default to printing description + self = String(reflecting: error).jsValue() + } + } +} diff --git a/Sources/JavaScriptKit/JS Types/JSObject.swift b/Sources/JavaScriptKit/JS Types/JSObject.swift index 187fe8b0c..f4c8fd919 100644 --- a/Sources/JavaScriptKit/JS Types/JSObject.swift +++ b/Sources/JavaScriptKit/JS Types/JSObject.swift @@ -56,12 +56,3 @@ extension JSObject: ExpressibleByDictionaryLiteral { self.init(elements) } } - -// MARK: - CustomStringConvertible - -extension JSObject: CustomStringConvertible { - - public var description: String { - return toString() ?? "" - } -} diff --git a/Sources/JavaScriptKit/JS Types/JSPromise.swift b/Sources/JavaScriptKit/JS Types/JSPromise.swift index edcbf574b..95ad8fd03 100644 --- a/Sources/JavaScriptKit/JS Types/JSPromise.swift +++ b/Sources/JavaScriptKit/JS Types/JSPromise.swift @@ -5,7 +5,12 @@ // Created by Alsey Coleman Miller on 6/3/20. // -public final class JSPromise: JSType { +/** + JavaScript Promise + + A Promise is a proxy for a value not necessarily known when the promise is created. It allows you to associate handlers with an asynchronous action's eventual success value or failure reason. This lets asynchronous methods return values like synchronous methods: instead of immediately returning the final value, the asynchronous method returns a promise to supply the value at some point in the future. + */ +public final class JSPromise: JSType { // MARK: - Properties @@ -18,12 +23,92 @@ public final class JSPromise: JSType { // validate if promise type } - public init() { - self.jsObject = Self.classObject.new() + /** + Initializes a Promise with the specified executor. + + - parameter executor: A function to be executed by the Promise. The executor is custom code that ties an outcome to a promise. You, the programmer, write the executor. + */ + public init(executor block: @escaping ((Success) -> (), (Error) -> ()) -> ()) { + let executor = JSFunctionRef.from { (arguments) in + let resolutionFunc = arguments[0].function + let rejectionFunc = arguments[1].function + block({ resolutionFunc?($0.jsValue()) }, { rejectionFunc?(JSValue(error: $0)) }) + return .undefined + } + self.jsObject = JSPromiseClassObject.new(executor) + } + + /** + Initializes a Promise with the specified executor. + + - parameter executor: A function to be executed by the Promise. The executor is custom code that ties an outcome to a promise. You, the programmer, write the executor. + */ + public convenience init(executor block: @escaping ((Result) -> ()) -> ()) { + self.init { (resolution, rejection) in + block({ + switch $0 { + case let .success(value): + resolution(value) + case let .failure(error): + rejection(error) + } + }) + } + } + + // MARK: - Properties + + /// Promise State + public var state: State { + guard let value = jsObject.state.string.flatMap({ State(rawValue: $0) }) + else { fatalError("Invalid state: \(jsObject.state)") } + return value + } + + /// Promise result value. + public var result: JSValue { + return jsObject.result + } + + // MARK: - Methods + + public func then(_ completion: (Success) -> ()) { + + } + + public func `catch`(_ completion: (Error) -> ()) { + + + } + + public func finally() { + + } } -internal extension JSPromise { +private let JSPromiseClassObject = JSObjectRef.global.Promise.function! + +// MARK: - Supporting Types + +public extension JSPromise { - static let classObject = JSObjectRef.global.Promise.function! + enum State: String { + + /// Initial state, neither fulfilled nor rejected. + case pending + + /// The operation completed successfully. + case fulfilled + + /// the operation failed. + case rejected + } +} + +extension JSPromise.State: JSValueConstructible { + + public static func construct(from value: JSValue) -> JSPromise.State? { + return value.string.flatMap { JSPromise.State(rawValue: $0) } + } } diff --git a/Sources/JavaScriptKit/JS Types/JSType.swift b/Sources/JavaScriptKit/JS Types/JSType.swift index 3e5cb48ab..878f4159b 100644 --- a/Sources/JavaScriptKit/JS Types/JSType.swift +++ b/Sources/JavaScriptKit/JS Types/JSType.swift @@ -6,23 +6,31 @@ // /// JavaScript Type (Class) -public protocol JSType: JSValueConvertible { +public protocol JSType: JSValueConvertible, JSValueConstructible, CustomStringConvertible { init?(_ jsObject: JSObjectRef) var jsObject: JSObjectRef { get } } -public extension JSType { +internal extension JSType { func toString() -> String? { return jsObject.toString.function?().string } } -// MARK: - JSValueConvertible - public extension JSType { - func jsValue() -> JSValue { return .object(jsObject) } + static func construct(from value: JSValue) -> Self? { + return value.object.flatMap { Self.init($0) } + } + + func jsValue() -> JSValue { + return .object(jsObject) + } + + var description: String { + return toString() ?? "" + } } From 4714de8c7227b41263473d8216752523512bfa5a Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 3 Jun 2020 18:46:13 -0500 Subject: [PATCH 04/38] Working on `JSPromise` --- .../JavaScriptKit/JS Types/JSBluetooth.swift | 5 ++- .../JS Types/JSBluetoothDevice.swift | 10 +++++ .../JavaScriptKit/JS Types/JSBoolean.swift | 3 +- Sources/JavaScriptKit/JS Types/JSObject.swift | 2 +- .../JavaScriptKit/JS Types/JSPromise.swift | 41 ++++++++++++++++--- .../JavaScriptKit/JSValueConvertible.swift | 14 ++++++- 6 files changed, 64 insertions(+), 11 deletions(-) diff --git a/Sources/JavaScriptKit/JS Types/JSBluetooth.swift b/Sources/JavaScriptKit/JS Types/JSBluetooth.swift index 5d2bb3292..98bbd2e23 100644 --- a/Sources/JavaScriptKit/JS Types/JSBluetooth.swift +++ b/Sources/JavaScriptKit/JS Types/JSBluetooth.swift @@ -28,7 +28,7 @@ public final class JSBluetooth: JSType { /// - Returns: A Promise to a `BluetoothDevice` object. public func requestDevice(//filters: [] = [], //services: [] = [], - acceptAllDevices: Bool = true) -> JSPromise { + /*acceptAllDevices: Bool = true*/) -> JSPromise { enum Option: String { case filters @@ -44,7 +44,8 @@ public final class JSBluetooth: JSType { // FIXME: Improve, support all options let options = JSObject() - options[Option.acceptAllDevices.rawValue] = acceptAllDevices.jsValue() + options[Option.acceptAllDevices.rawValue] = JSBoolean(true).jsValue() + options[Option.optionalServices.rawValue] = ["device_information"].jsValue() let result = function(options.jsValue()) diff --git a/Sources/JavaScriptKit/JS Types/JSBluetoothDevice.swift b/Sources/JavaScriptKit/JS Types/JSBluetoothDevice.swift index 3adc0ac81..90f2c1bd3 100644 --- a/Sources/JavaScriptKit/JS Types/JSBluetoothDevice.swift +++ b/Sources/JavaScriptKit/JS Types/JSBluetoothDevice.swift @@ -18,5 +18,15 @@ public final class JSBluetoothDevice: JSType { self.jsObject = jsObject } + // MARK: - Accessors + + public lazy var id: String = self.jsObject.get("id").string! + + public lazy var name: String = self.jsObject.name.string! + } + +// MARK: - Identifiable + +extension JSBluetoothDevice: Identifiable { } diff --git a/Sources/JavaScriptKit/JS Types/JSBoolean.swift b/Sources/JavaScriptKit/JS Types/JSBoolean.swift index b3b61b02d..f63c9c901 100644 --- a/Sources/JavaScriptKit/JS Types/JSBoolean.swift +++ b/Sources/JavaScriptKit/JS Types/JSBoolean.swift @@ -18,7 +18,7 @@ public final class JSBoolean: JSType { self.jsObject = jsObject } - public init(value: Bool) { + public init(_ value: Bool) { self.jsObject = Self.classObject.new(value.jsValue()) } } @@ -27,3 +27,4 @@ internal extension JSBoolean { static let classObject = JSObjectRef.global.Boolean.function! } + diff --git a/Sources/JavaScriptKit/JS Types/JSObject.swift b/Sources/JavaScriptKit/JS Types/JSObject.swift index f4c8fd919..ac673ef45 100644 --- a/Sources/JavaScriptKit/JS Types/JSObject.swift +++ b/Sources/JavaScriptKit/JS Types/JSObject.swift @@ -22,7 +22,7 @@ public final class JSObject: JSType { public init() { self.jsObject = Self.classObject.new() - assert(Self.isObject(jsObject)) + //assert(Self.isObject(jsObject)) } public convenience init(_ elements: [(key: String, value: JSValue)]) { diff --git a/Sources/JavaScriptKit/JS Types/JSPromise.swift b/Sources/JavaScriptKit/JS Types/JSPromise.swift index 95ad8fd03..5f3c16f11 100644 --- a/Sources/JavaScriptKit/JS Types/JSPromise.swift +++ b/Sources/JavaScriptKit/JS Types/JSPromise.swift @@ -33,7 +33,7 @@ public final class JSPromise: JSType { let resolutionFunc = arguments[0].function let rejectionFunc = arguments[1].function block({ resolutionFunc?($0.jsValue()) }, { rejectionFunc?(JSValue(error: $0)) }) - return .undefined + return .null } self.jsObject = JSPromiseClassObject.new(executor) } @@ -56,7 +56,7 @@ public final class JSPromise: JSType { } } - // MARK: - Properties + // MARK: - Accessors /// Promise State public var state: State { @@ -72,18 +72,49 @@ public final class JSPromise: JSType { // MARK: - Methods - public func then(_ completion: (Success) -> ()) { + /** + The `then()` method returns a Promise. It takes up to two arguments: callback functions for the success and failure cases of the Promise. + + - Parameter onFulfilled: A Function called if the Promise is fulfilled. This function has one argument, the fulfillment value. If it is not a function, it is internally replaced with an "Identity" function (it returns the received argument). + + - Parameter onRejected: A Function called if the Promise is rejected. This function has one argument, the rejection reason. If it is not a function, it is internally replaced with a "Thrower" function (it throws an error it received as argument). + + - Returns: Once a Promise is fulfilled or rejected, the respective handler function (onFulfilled or onRejected) will be called asynchronously (scheduled in the current thread loop). The behaviour of the handler function follows a specific set of rules. + + - Note: If a handler function: + - returns a value, the promise returned by then gets resolved with the returned value as its value. + - doesn't return anything, the promise returned by then gets resolved with an undefined value. + - throws an error, the promise returned by then gets rejected with the thrown error as its value. + - returns an already fulfilled promise, the promise returned by then gets fulfilled with that promise's value as its value. + - returns an already rejected promise, the promise returned by then gets rejected with that promise's value as its value. + - returns another pending promise object, the resolution/rejection of the promise returned by then will be subsequent to the resolution/rejection of the promise returned by the handler. Also, the resolved value of the promise returned by then will be the same as the resolved value of the promise returned by the handler. + */ + public func then/**/(onFulfilled: @escaping (Success) -> () /* onRejected: ((Error) -> ())? */) /* -> JSPromise */ { + let success = JSFunctionRef.from { (arguments) in + if let value = arguments.first.flatMap({ Success.construct(from: $0) }) { + onFulfilled(value) + } + return .null + } + /* + let errorFunction = onRejected.flatMap { (onRejected) in + JSFunctionRef.from { (arguments) in + // TODO: Initialize error + return .undefined + } + }*/ + let result = jsObject.then.function?(success, JSValue.null) //, errorFunction) } public func `catch`(_ completion: (Error) -> ()) { - + jsObject.catch.function?() } public func finally() { - + jsObject.finally.function?() } } diff --git a/Sources/JavaScriptKit/JSValueConvertible.swift b/Sources/JavaScriptKit/JSValueConvertible.swift index f11becdb6..9c0e3d316 100644 --- a/Sources/JavaScriptKit/JSValueConvertible.swift +++ b/Sources/JavaScriptKit/JSValueConvertible.swift @@ -57,6 +57,18 @@ extension JSObjectRef: JSValueConvertible { // from `JSFunctionRef` } +extension Optional: JSValueConvertible where Wrapped: JSValueConvertible { + + public func jsValue() -> JSValue { + switch self { + case .none: + return .null + case let .some(value): + return value.jsValue() + } + } +} + extension Dictionary where Value: JSValueConvertible, Key == String { public func jsValue() -> JSValue { Swift.Dictionary.jsValue(self)() @@ -108,8 +120,6 @@ extension RawJSValue: JSValueConvertible { return .undefined case .function: return .function(JSFunctionRef(id: UInt32(payload1))) - @unknown default: - fatalError("Unreachable") } } } From b5c8fa37c80143af917d2bfe3b622786cb274efd Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 3 Jun 2020 19:27:38 -0500 Subject: [PATCH 05/38] Added `JSError` --- Sources/JavaScriptKit/JS Types/JSError.swift | 42 ++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/Sources/JavaScriptKit/JS Types/JSError.swift b/Sources/JavaScriptKit/JS Types/JSError.swift index 1b7a1626a..b73d64699 100644 --- a/Sources/JavaScriptKit/JS Types/JSError.swift +++ b/Sources/JavaScriptKit/JS Types/JSError.swift @@ -5,6 +5,47 @@ // Created by Alsey Coleman Miller on 6/3/20. // +/// JavaScript Error +/// +/// Error objects are thrown when runtime errors occur. +/// The Error object can also be used as a base object for user-defined exceptions. +public class JSError: JSType, Error { + + // MARK: - Properties + + public let jsObject: JSObjectRef + + // MARK: - Initialization + + public init(_ jsObject: JSObjectRef) { + self.jsObject = jsObject + } + + public init() { + // TODO: Implement initializer + self.jsObject = Self.classObject.new() + } + + // MARK: - Accessors + + /// The name property represents a name for the type of error. The initial value is "Error". + public lazy var name: String = self.jsObject.name.string ?? "" + + /// The message property is a human-readable description of the error. + public lazy var message: String = self.jsObject.message.string ?? "" +} + +internal extension JSError { + + static let classObject = JSObjectRef.global.Error.function! +} + +// MARK: - CustomStringConvertible + +extension JSError: CustomStringConvertible { } + +// MARK: - Extensions + public extension JSValue { /// Cast error to JavaScript value. @@ -21,3 +62,4 @@ public extension JSValue { } } } + From 1a44d94cb5fa5d2e3fa4478ee3c532b6a6146439 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 3 Jun 2020 19:27:58 -0500 Subject: [PATCH 06/38] Updated `JSType` --- Sources/JavaScriptKit/JS Types/JSArray.swift | 4 ++++ Sources/JavaScriptKit/JS Types/JSBoolean.swift | 12 ++++++++++++ Sources/JavaScriptKit/JS Types/JSObject.swift | 4 ++++ Sources/JavaScriptKit/JS Types/JSType.swift | 5 ++++- 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Sources/JavaScriptKit/JS Types/JSArray.swift b/Sources/JavaScriptKit/JS Types/JSArray.swift index 0d78778b2..a0c7ea67a 100644 --- a/Sources/JavaScriptKit/JS Types/JSArray.swift +++ b/Sources/JavaScriptKit/JS Types/JSArray.swift @@ -42,6 +42,10 @@ internal extension JSArray { @available(*, renamed: "JSArray") public typealias JSArrayRef = JSArray +// MARK: - CustomStringConvertible + +extension JSArray: CustomStringConvertible { } + // MARK: - ExpressibleByArrayLiteral extension JSArray: ExpressibleByArrayLiteral { diff --git a/Sources/JavaScriptKit/JS Types/JSBoolean.swift b/Sources/JavaScriptKit/JS Types/JSBoolean.swift index f63c9c901..6682d9f60 100644 --- a/Sources/JavaScriptKit/JS Types/JSBoolean.swift +++ b/Sources/JavaScriptKit/JS Types/JSBoolean.swift @@ -28,3 +28,15 @@ internal extension JSBoolean { static let classObject = JSObjectRef.global.Boolean.function! } +// MARK: - CustomStringConvertible + +extension JSBoolean: CustomStringConvertible { } + +// MARK: - ExpressibleByBooleanLiteral + +extension JSBoolean: ExpressibleByBooleanLiteral { + + public init(booleanLiteral value: Bool) { + self.init(value) + } +} diff --git a/Sources/JavaScriptKit/JS Types/JSObject.swift b/Sources/JavaScriptKit/JS Types/JSObject.swift index ac673ef45..deadf6e21 100644 --- a/Sources/JavaScriptKit/JS Types/JSObject.swift +++ b/Sources/JavaScriptKit/JS Types/JSObject.swift @@ -48,6 +48,10 @@ public extension JSObject { } } +// MARK: - CustomStringConvertible + +extension JSObject: CustomStringConvertible { } + // MARK: - ExpressibleByDictionaryLiteral extension JSObject: ExpressibleByDictionaryLiteral { diff --git a/Sources/JavaScriptKit/JS Types/JSType.swift b/Sources/JavaScriptKit/JS Types/JSType.swift index 878f4159b..20b3bd183 100644 --- a/Sources/JavaScriptKit/JS Types/JSType.swift +++ b/Sources/JavaScriptKit/JS Types/JSType.swift @@ -6,7 +6,7 @@ // /// JavaScript Type (Class) -public protocol JSType: JSValueConvertible, JSValueConstructible, CustomStringConvertible { +public protocol JSType: JSValueConvertible, JSValueConstructible { init?(_ jsObject: JSObjectRef) @@ -29,6 +29,9 @@ public extension JSType { func jsValue() -> JSValue { return .object(jsObject) } +} + +public extension JSType where Self: CustomStringConvertible { var description: String { return toString() ?? "" From c88b849a6b3e1f8b7042b19837ec91738f6806ae Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 3 Jun 2020 19:50:14 -0500 Subject: [PATCH 07/38] Working on `JSBluetooth` --- Sources/JavaScriptKit/JS Types/JSBluetooth.swift | 9 ++++++--- Sources/JavaScriptKit/JS Types/JSBoolean.swift | 2 +- Sources/JavaScriptKit/JS Types/JSError.swift | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Sources/JavaScriptKit/JS Types/JSBluetooth.swift b/Sources/JavaScriptKit/JS Types/JSBluetooth.swift index 98bbd2e23..bd04aeca3 100644 --- a/Sources/JavaScriptKit/JS Types/JSBluetooth.swift +++ b/Sources/JavaScriptKit/JS Types/JSBluetooth.swift @@ -28,7 +28,9 @@ public final class JSBluetooth: JSType { /// - Returns: A Promise to a `BluetoothDevice` object. public func requestDevice(//filters: [] = [], //services: [] = [], - /*acceptAllDevices: Bool = true*/) -> JSPromise { + acceptAllDevices: Bool = true) -> JSPromise { + + JSObjectRef.global.console.object?.log.function?("\(#file) \(String(reflecting: type(of: self))) \(#function) \(#line)") enum Option: String { case filters @@ -44,14 +46,15 @@ public final class JSBluetooth: JSType { // FIXME: Improve, support all options let options = JSObject() - options[Option.acceptAllDevices.rawValue] = JSBoolean(true).jsValue() + options[Option.acceptAllDevices.rawValue] = JSBoolean(acceptAllDevices).jsValue() options[Option.optionalServices.rawValue] = ["device_information"].jsValue() let result = function(options.jsValue()) + JSObjectRef.global.console.object?.log.function?("\(#file) \(String(reflecting: type(of: self))) \(#function) \(#line)") guard let promise = result.object.flatMap({ JSPromise($0) }) else { fatalError("Invalid object \(result)") } - + JSObjectRef.global.console.object?.log.function?("\(#file) \(String(reflecting: type(of: self))) \(#function) \(#line)") return promise } } diff --git a/Sources/JavaScriptKit/JS Types/JSBoolean.swift b/Sources/JavaScriptKit/JS Types/JSBoolean.swift index 6682d9f60..5887bb5c0 100644 --- a/Sources/JavaScriptKit/JS Types/JSBoolean.swift +++ b/Sources/JavaScriptKit/JS Types/JSBoolean.swift @@ -36,7 +36,7 @@ extension JSBoolean: CustomStringConvertible { } extension JSBoolean: ExpressibleByBooleanLiteral { - public init(booleanLiteral value: Bool) { + public convenience init(booleanLiteral value: Bool) { self.init(value) } } diff --git a/Sources/JavaScriptKit/JS Types/JSError.swift b/Sources/JavaScriptKit/JS Types/JSError.swift index b73d64699..a7d213c73 100644 --- a/Sources/JavaScriptKit/JS Types/JSError.swift +++ b/Sources/JavaScriptKit/JS Types/JSError.swift @@ -17,7 +17,7 @@ public class JSError: JSType, Error { // MARK: - Initialization - public init(_ jsObject: JSObjectRef) { + public required init?(_ jsObject: JSObjectRef) { self.jsObject = jsObject } From 128a862c9042ad09383c9b163146da27de62ac6c Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 3 Jun 2020 20:08:04 -0500 Subject: [PATCH 08/38] Working on `JSBluetooth` --- Sources/JavaScriptKit/JS Types/JSBluetooth.swift | 8 ++------ Sources/JavaScriptKit/JS Types/JSPromise.swift | 7 +++++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Sources/JavaScriptKit/JS Types/JSBluetooth.swift b/Sources/JavaScriptKit/JS Types/JSBluetooth.swift index bd04aeca3..7f1d387e5 100644 --- a/Sources/JavaScriptKit/JS Types/JSBluetooth.swift +++ b/Sources/JavaScriptKit/JS Types/JSBluetooth.swift @@ -30,8 +30,6 @@ public final class JSBluetooth: JSType { //services: [] = [], acceptAllDevices: Bool = true) -> JSPromise { - JSObjectRef.global.console.object?.log.function?("\(#file) \(String(reflecting: type(of: self))) \(#function) \(#line)") - enum Option: String { case filters case optionalServices @@ -47,14 +45,12 @@ public final class JSBluetooth: JSType { // FIXME: Improve, support all options let options = JSObject() options[Option.acceptAllDevices.rawValue] = JSBoolean(acceptAllDevices).jsValue() - options[Option.optionalServices.rawValue] = ["device_information"].jsValue() - let result = function(options.jsValue()) - JSObjectRef.global.console.object?.log.function?("\(#file) \(String(reflecting: type(of: self))) \(#function) \(#line)") + let result = function.apply(this: jsObject, arguments: options) guard let promise = result.object.flatMap({ JSPromise($0) }) else { fatalError("Invalid object \(result)") } - JSObjectRef.global.console.object?.log.function?("\(#file) \(String(reflecting: type(of: self))) \(#function) \(#line)") + return promise } } diff --git a/Sources/JavaScriptKit/JS Types/JSPromise.swift b/Sources/JavaScriptKit/JS Types/JSPromise.swift index 5f3c16f11..58cb7ad34 100644 --- a/Sources/JavaScriptKit/JS Types/JSPromise.swift +++ b/Sources/JavaScriptKit/JS Types/JSPromise.swift @@ -91,11 +91,14 @@ public final class JSPromise: JSType { */ public func then/**/(onFulfilled: @escaping (Success) -> () /* onRejected: ((Error) -> ())? */) /* -> JSPromise */ { + guard let function = jsObject.then.function + else { fatalError("Invalid function \(jsObject.requestDevice)") } + let success = JSFunctionRef.from { (arguments) in if let value = arguments.first.flatMap({ Success.construct(from: $0) }) { onFulfilled(value) } - return .null + return .undefined } /* let errorFunction = onRejected.flatMap { (onRejected) in @@ -104,7 +107,7 @@ public final class JSPromise: JSType { return .undefined } }*/ - let result = jsObject.then.function?(success, JSValue.null) //, errorFunction) + let result = function.apply(this: jsObject, argumentList: [success.jsValue()]) } public func `catch`(_ completion: (Error) -> ()) { From 2f0eae8941b42db060cac6611b0c1cc427f6cc87 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 3 Jun 2020 20:09:29 -0500 Subject: [PATCH 09/38] Updated example --- .../Sources/JavaScriptKitExample/main.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift b/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift index 2ddec8ad4..d0f9b767c 100644 --- a/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift +++ b/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift @@ -2,6 +2,7 @@ import JavaScriptKit let alert = JSObjectRef.global.alert.function! let document = JSObjectRef.global.document.object! +let bluetooth = JSBluetooth.shared! let divElement = document.createElement!("div").object! divElement.innerText = "Hello, world" @@ -11,7 +12,14 @@ _ = body.appendChild!(divElement) let buttonElement = document.createElement!("button").object! buttonElement.innerText = "Click me!" buttonElement.onclick = .function { _ in + JSObjectRef.global.console.object?.log.function?("\(#file) \(#function) \(#line)") alert("Swift is running on browser!") + bluetooth.requestDevice().then { + JSObjectRef.global.console.object?.log.function?("\($0)") + alert("Got device \($0)") + } + JSObjectRef.global.console.object?.log.function?("\(#file) \(#function) \(#line)") + return .undefined } _ = body.appendChild!(buttonElement) From b75da558874dd3dc3a2f0472f8d7018457c70d2a Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 3 Jun 2020 20:21:57 -0500 Subject: [PATCH 10/38] Updated `JSBluetoothDevice` --- .../JavaScriptKit/JS Types/JSBluetoothDevice.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Sources/JavaScriptKit/JS Types/JSBluetoothDevice.swift b/Sources/JavaScriptKit/JS Types/JSBluetoothDevice.swift index 90f2c1bd3..cbbd7ece4 100644 --- a/Sources/JavaScriptKit/JS Types/JSBluetoothDevice.swift +++ b/Sources/JavaScriptKit/JS Types/JSBluetoothDevice.swift @@ -22,9 +22,16 @@ public final class JSBluetoothDevice: JSType { public lazy var id: String = self.jsObject.get("id").string! - public lazy var name: String = self.jsObject.name.string! - + public lazy var name: String? = self.jsObject.name.string +} + +// MARK: - CustomStringConvertible + +extension JSBluetoothDevice: CustomStringConvertible { + public var description: String { + return "JSBluetoothDevice(id: \(id), name: \(name ?? "nil"))" + } } // MARK: - Identifiable From 3f47ae9d0efa26d0d93c1d0073261b35b64e3788 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 3 Jun 2020 21:11:03 -0500 Subject: [PATCH 11/38] Added `JSConsole` --- .../Sources/JavaScriptKitExample/main.swift | 7 +- .../JavaScriptKit/JS Types/JSConsole.swift | 71 +++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 Sources/JavaScriptKit/JS Types/JSConsole.swift diff --git a/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift b/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift index d0f9b767c..3ab9cf4f1 100644 --- a/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift +++ b/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift @@ -12,13 +12,14 @@ _ = body.appendChild!(divElement) let buttonElement = document.createElement!("button").object! buttonElement.innerText = "Click me!" buttonElement.onclick = .function { _ in - JSObjectRef.global.console.object?.log.function?("\(#file) \(#function) \(#line)") + JSConsole.debug("\(#file) \(#function) \(#line)") alert("Swift is running on browser!") + JSConsole.log("Requesting device") bluetooth.requestDevice().then { - JSObjectRef.global.console.object?.log.function?("\($0)") + JSConsole.info("\($0)") alert("Got device \($0)") } - JSObjectRef.global.console.object?.log.function?("\(#file) \(#function) \(#line)") + JSConsole.debug("\(#file) \(#function) \(#line)") return .undefined } diff --git a/Sources/JavaScriptKit/JS Types/JSConsole.swift b/Sources/JavaScriptKit/JS Types/JSConsole.swift new file mode 100644 index 000000000..615d1d6c3 --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSConsole.swift @@ -0,0 +1,71 @@ +// +// JSConsole.swift +// +// +// Created by Alsey Coleman Miller on 6/3/20. +// + +public final class JSConsole: JSType { + + // MARK: - Properties + + public let jsObject: JSObjectRef + + // MARK: - Initialization + + public init(_ jsObject: JSObjectRef) { + self.jsObject = jsObject + } + + // MARK: - Methods + + /** + The Console method `log()` outputs a message to the web console. The message may be a single string (with optional substitution values), or it may be any one or more JavaScript objects. + */ + public static func log(_ message: String) { + logFunction(message) + } + + /** + The `console.info()` method outputs an informational message to the Web Console. In Firefox, a small "i" icon is displayed next to these items in the Web Console's log. + */ + public static func info(_ message: String) { + infoFunction(message) + } + + /** + The console method `debug()` outputs a message to the web console at the "debug" log level. The message is only displayed to the user if the console is configured to display debug output. + */ + public static func debug(_ message: String) { + debugFunction(message) + } + + /** + Outputs an error message to the Web Console. + */ + public static func error(_ message: String) { + errorFunction(message) + } + + /** + The `console.assert()` method writes an error message to the console if the assertion is false. If the assertion is true, nothing happens. + */ + public static func assert(_ condition: @autoclosure () -> (Bool), _ message: String) { + assertFunction(condition(), message) + } +} + +internal extension JSConsole { + + static let classObject = JSObjectRef.global.console.object! + + static let logFunction = classObject.log.function! + + static let infoFunction = classObject.info.function! + + static let debugFunction = classObject.debug.function! + + static let errorFunction = classObject.error.function! + + static let assertFunction = classObject.assert.function! +} From cde8d76e68b5cca395cd0145e6e85b4a22cdc48d Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 3 Jun 2020 21:30:40 -0500 Subject: [PATCH 12/38] Added `JSBluetoothRemoteGATTServer` --- .../JavaScriptKit/JS Types/JSBluetooth.swift | 4 +- .../JS Types/JSBluetoothDevice.swift | 5 +++ .../JSBluetoothRemoteGATTServer.swift | 42 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 Sources/JavaScriptKit/JS Types/JSBluetoothRemoteGATTServer.swift diff --git a/Sources/JavaScriptKit/JS Types/JSBluetooth.swift b/Sources/JavaScriptKit/JS Types/JSBluetooth.swift index 7f1d387e5..1002da44f 100644 --- a/Sources/JavaScriptKit/JS Types/JSBluetooth.swift +++ b/Sources/JavaScriptKit/JS Types/JSBluetooth.swift @@ -5,7 +5,9 @@ // Created by Alsey Coleman Miller on 6/3/20. // -/// JavaScript Bluetooth object +/// JavaScript Bluetooth interface +/// +/// - SeeAlso: [Web Bluetooth API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API) public final class JSBluetooth: JSType { // MARK: - Properties diff --git a/Sources/JavaScriptKit/JS Types/JSBluetoothDevice.swift b/Sources/JavaScriptKit/JS Types/JSBluetoothDevice.swift index cbbd7ece4..f99eddcd6 100644 --- a/Sources/JavaScriptKit/JS Types/JSBluetoothDevice.swift +++ b/Sources/JavaScriptKit/JS Types/JSBluetoothDevice.swift @@ -20,9 +20,14 @@ public final class JSBluetoothDevice: JSType { // MARK: - Accessors + /// A string that uniquely identifies a device. public lazy var id: String = self.jsObject.get("id").string! + /// A string that provices a human-readable name for the device. public lazy var name: String? = self.jsObject.name.string + + /// Interface of the Web Bluetooth API represents a GATT Server on a remote device. + public lazy var gatt = self.jsObject.gatt.object.flatMap({ JSBluetoothRemoteGATTServer($0) })! } // MARK: - CustomStringConvertible diff --git a/Sources/JavaScriptKit/JS Types/JSBluetoothRemoteGATTServer.swift b/Sources/JavaScriptKit/JS Types/JSBluetoothRemoteGATTServer.swift new file mode 100644 index 000000000..cd1a19556 --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSBluetoothRemoteGATTServer.swift @@ -0,0 +1,42 @@ +// +// JSBluetoothRemoteGATTServer.swift +// +// +// Created by Alsey Coleman Miller on 6/3/20. +// + +/// Represents a GATT Server on a remote device. +// https://developer.mozilla.org/en-US/docs/Web/API/BluetoothRemoteGATTServer +public final class JSBluetoothRemoteGATTServer: JSType { + + // MARK: - Properties + + public let jsObject: JSObjectRef + + // MARK: - Initialization + + public init(_ jsObject: JSObjectRef) { + self.jsObject = jsObject + } + + // MARK: - Accessors + + public var isConnected: Bool { + return jsObject.connected.boolean ?? false + } + + // MARK: - Methods + + public func connect() -> JSPromise { + guard let function = jsObject.connect.function + else { fatalError("Missing function \(#function)") } + let result = function.apply(this: jsObject) + guard let promise = result.object.flatMap({ JSPromise($0) }) + else { fatalError("Invalid object \(result)") } + return promise + } + + public func disconnect() { + jsObject.disconnect.function?.apply(this: jsObject) + } +} From 4fb0f75e730aea1fa56878ec4f9842681185b2b3 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 3 Jun 2020 23:40:11 -0500 Subject: [PATCH 13/38] Updated `JSPromise` --- .../JSBluetoothRemoteGATTServer.swift | 2 +- .../JavaScriptKit/JS Types/JSPromise.swift | 173 +++++++++++++++--- 2 files changed, 147 insertions(+), 28 deletions(-) diff --git a/Sources/JavaScriptKit/JS Types/JSBluetoothRemoteGATTServer.swift b/Sources/JavaScriptKit/JS Types/JSBluetoothRemoteGATTServer.swift index cd1a19556..650942bb6 100644 --- a/Sources/JavaScriptKit/JS Types/JSBluetoothRemoteGATTServer.swift +++ b/Sources/JavaScriptKit/JS Types/JSBluetoothRemoteGATTServer.swift @@ -37,6 +37,6 @@ public final class JSBluetoothRemoteGATTServer: JSType { } public func disconnect() { - jsObject.disconnect.function?.apply(this: jsObject) + let _ = jsObject.disconnect.function?.apply(this: jsObject) } } diff --git a/Sources/JavaScriptKit/JS Types/JSPromise.swift b/Sources/JavaScriptKit/JS Types/JSPromise.swift index 58cb7ad34..92b94345e 100644 --- a/Sources/JavaScriptKit/JS Types/JSPromise.swift +++ b/Sources/JavaScriptKit/JS Types/JSPromise.swift @@ -59,7 +59,7 @@ public final class JSPromise: JSType { // MARK: - Accessors /// Promise State - public var state: State { + public var state: JSPromiseState { guard let value = jsObject.state.string.flatMap({ State(rawValue: $0) }) else { fatalError("Invalid state: \(jsObject.state)") } return value @@ -89,60 +89,179 @@ public final class JSPromise: JSType { - returns an already rejected promise, the promise returned by then gets rejected with that promise's value as its value. - returns another pending promise object, the resolution/rejection of the promise returned by then will be subsequent to the resolution/rejection of the promise returned by the handler. Also, the resolved value of the promise returned by then will be the same as the resolved value of the promise returned by the handler. */ - public func then/**/(onFulfilled: @escaping (Success) -> () /* onRejected: ((Error) -> ())? */) /* -> JSPromise */ { + @discardableResult + internal func _then(onFulfilled: @escaping (Success) -> (JSValue), + onRejected: @escaping (JSError) -> () = defaultRejected) -> JSValue { guard let function = jsObject.then.function else { fatalError("Invalid function \(jsObject.requestDevice)") } let success = JSFunctionRef.from { (arguments) in if let value = arguments.first.flatMap({ Success.construct(from: $0) }) { - onFulfilled(value) + return onFulfilled(value) + } else { + JSConsole.error("Unable to load success type \(String(reflecting: Success.self)) from ", arguments.first) + return .undefined } - return .undefined } - /* - let errorFunction = onRejected.flatMap { (onRejected) in - JSFunctionRef.from { (arguments) in - // TODO: Initialize error - return .undefined + + let errorFunction = JSFunctionRef.from { (arguments) in + if let value = arguments.first.flatMap({ JSError.construct(from: $0) }) { + onRejected(value) + } else { + JSConsole.error("Unable to load error from ", arguments.first) } - }*/ - let result = function.apply(this: jsObject, argumentList: [success.jsValue()]) + return .undefined + } + + return function.apply(this: jsObject, argumentList: [success.jsValue(), errorFunction.jsValue()]) } - public func `catch`(_ completion: (Error) -> ()) { + /** + The `then()` method returns a Promise. It takes up to two arguments: callback functions for the success and failure cases of the Promise. + + - Parameter onFulfilled: A Function called if the Promise is fulfilled. This function has one argument, the fulfillment value. If it is not a function, it is internally replaced with an "Identity" function (it returns the received argument). + + - Parameter onRejected: A Function called if the Promise is rejected. This function has one argument, the rejection reason. If it is not a function, it is internally replaced with a "Thrower" function (it throws an error it received as argument). + + - Returns: Once a Promise is fulfilled or rejected, the respective handler function (onFulfilled or onRejected) will be called asynchronously (scheduled in the current thread loop). The behaviour of the handler function follows a specific set of rules. + + - Note: If a handler function: + - returns a value, the promise returned by then gets resolved with the returned value as its value. + - doesn't return anything, the promise returned by then gets resolved with an undefined value. + - throws an error, the promise returned by then gets rejected with the thrown error as its value. + - returns an already fulfilled promise, the promise returned by then gets fulfilled with that promise's value as its value. + - returns an already rejected promise, the promise returned by then gets rejected with that promise's value as its value. + - returns another pending promise object, the resolution/rejection of the promise returned by then will be subsequent to the resolution/rejection of the promise returned by the handler. Also, the resolved value of the promise returned by then will be the same as the resolved value of the promise returned by the handler. + */ + public func then(onFulfilled: @escaping (Success) -> (), + onRejected: @escaping (JSError) -> ()) -> JSPromise { + let result = _then(onFulfilled: { + onFulfilled($0) + return .undefined + }, onRejected: onRejected) + guard let promise = result.object.flatMap({ JSPromise($0) }) + else { fatalError("Invalid object \(result)") } + return promise + } + + /** + The `then()` method returns a Promise. It takes up to two arguments: callback functions for the success and failure cases of the Promise. + + - Parameter onFulfilled: A Function called if the Promise is fulfilled. This function has one argument, the fulfillment value. If it is not a function, it is internally replaced with an "Identity" function (it returns the received argument). + + - Parameter onRejected: A Function called if the Promise is rejected. This function has one argument, the rejection reason. If it is not a function, it is internally replaced with a "Thrower" function (it throws an error it received as argument). + + - Returns: Once a Promise is fulfilled or rejected, the respective handler function (onFulfilled or onRejected) will be called asynchronously (scheduled in the current thread loop). The behaviour of the handler function follows a specific set of rules. + + - Note: If a handler function: + - returns a value, the promise returned by then gets resolved with the returned value as its value. + - doesn't return anything, the promise returned by then gets resolved with an undefined value. + - throws an error, the promise returned by then gets rejected with the thrown error as its value. + - returns an already fulfilled promise, the promise returned by then gets fulfilled with that promise's value as its value. + - returns an already rejected promise, the promise returned by then gets rejected with that promise's value as its value. + - returns another pending promise object, the resolution/rejection of the promise returned by then will be subsequent to the resolution/rejection of the promise returned by the handler. Also, the resolved value of the promise returned by then will be the same as the resolved value of the promise returned by the handler. + */ + public func then(onFulfilled: @escaping (Success) -> (JSPromise), + onRejected: @escaping (JSError) -> ()) -> JSPromise where T: JSType { + let result = _then(onFulfilled: { + return onFulfilled($0).jsValue() + }, onRejected: onRejected) + guard let promise = result.object.flatMap({ JSPromise($0) }) + else { fatalError("Invalid object \(result)") } + return promise + } + + /** + The `then()` method returns a Promise. It takes up to two arguments: callback functions for the success and failure cases of the Promise. + + - Parameter onFulfilled: A Function called if the Promise is fulfilled. This function has one argument, the fulfillment value. If it is not a function, it is internally replaced with an "Identity" function (it returns the received argument). + + - Parameter onRejected: A Function called if the Promise is rejected. This function has one argument, the rejection reason. If it is not a function, it is internally replaced with a "Thrower" function (it throws an error it received as argument). + + - Returns: Once a Promise is fulfilled or rejected, the respective handler function (onFulfilled or onRejected) will be called asynchronously (scheduled in the current thread loop). The behaviour of the handler function follows a specific set of rules. + + - Note: If a handler function: + - returns a value, the promise returned by then gets resolved with the returned value as its value. + - doesn't return anything, the promise returned by then gets resolved with an undefined value. + - throws an error, the promise returned by then gets rejected with the thrown error as its value. + - returns an already fulfilled promise, the promise returned by then gets fulfilled with that promise's value as its value. + - returns an already rejected promise, the promise returned by then gets rejected with that promise's value as its value. + - returns another pending promise object, the resolution/rejection of the promise returned by then will be subsequent to the resolution/rejection of the promise returned by the handler. Also, the resolved value of the promise returned by then will be the same as the resolved value of the promise returned by the handler. + */ + public func then(onFulfilled: @escaping (Success) -> ()) -> JSPromise { + let result = _then(onFulfilled: { + onFulfilled($0) + return .undefined + }) + guard let promise = result.object.flatMap({ JSPromise($0) }) + else { fatalError("Invalid object \(result)") } + return promise + } + + /** + The `then()` method returns a Promise. It takes up to two arguments: callback functions for the success and failure cases of the Promise. + + - Parameter onFulfilled: A Function called if the Promise is fulfilled. This function has one argument, the fulfillment value. If it is not a function, it is internally replaced with an "Identity" function (it returns the received argument). + + - Parameter onRejected: A Function called if the Promise is rejected. This function has one argument, the rejection reason. If it is not a function, it is internally replaced with a "Thrower" function (it throws an error it received as argument). + + - Returns: Once a Promise is fulfilled or rejected, the respective handler function (onFulfilled or onRejected) will be called asynchronously (scheduled in the current thread loop). The behaviour of the handler function follows a specific set of rules. + + - Note: If a handler function: + - returns a value, the promise returned by then gets resolved with the returned value as its value. + - doesn't return anything, the promise returned by then gets resolved with an undefined value. + - throws an error, the promise returned by then gets rejected with the thrown error as its value. + - returns an already fulfilled promise, the promise returned by then gets fulfilled with that promise's value as its value. + - returns an already rejected promise, the promise returned by then gets rejected with that promise's value as its value. + - returns another pending promise object, the resolution/rejection of the promise returned by then will be subsequent to the resolution/rejection of the promise returned by the handler. Also, the resolved value of the promise returned by then will be the same as the resolved value of the promise returned by the handler. + */ + public func then(onFulfilled: @escaping (Success) -> (JSPromise)) -> JSPromise where T: JSType { + + let result = _then(onFulfilled: { + return onFulfilled($0).jsValue() + }) + guard let promise = result.object.flatMap({ JSPromise($0) }) + else { fatalError("Invalid object \(result)") } + return promise + } + + public func `catch`(_ completion: (JSError) -> ()) { jsObject.catch.function?() } public func finally() { - jsObject.finally.function?() } } private let JSPromiseClassObject = JSObjectRef.global.Promise.function! +internal let defaultRejected: (JSError) -> () = { JSConsole.error("Uncaught promise error ", $0) } + // MARK: - Supporting Types public extension JSPromise { - enum State: String { - - /// Initial state, neither fulfilled nor rejected. - case pending - - /// The operation completed successfully. - case fulfilled - - /// the operation failed. - case rejected - } + typealias State = JSPromiseState +} + +public enum JSPromiseState: String { + + /// Initial state, neither fulfilled nor rejected. + case pending + + /// The operation completed successfully. + case fulfilled + + /// the operation failed. + case rejected } -extension JSPromise.State: JSValueConstructible { +extension JSPromiseState: JSValueConstructible { - public static func construct(from value: JSValue) -> JSPromise.State? { - return value.string.flatMap { JSPromise.State(rawValue: $0) } + public static func construct(from value: JSValue) -> JSPromiseState? { + return value.string.flatMap { JSPromiseState(rawValue: $0) } } } From 66dd688bff7474fba1c073a175847ecf2fe2c1c7 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Wed, 3 Jun 2020 23:46:42 -0500 Subject: [PATCH 14/38] Updated logging --- .../Sources/JavaScriptKitExample/main.swift | 16 ++++++--- .../JavaScriptKit/JS Types/JSConsole.swift | 33 +++++++++++++------ 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift b/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift index 3ab9cf4f1..0db1a81eb 100644 --- a/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift +++ b/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift @@ -5,19 +5,27 @@ let document = JSObjectRef.global.document.object! let bluetooth = JSBluetooth.shared! let divElement = document.createElement!("div").object! -divElement.innerText = "Hello, world" +divElement.innerText = "Swift Bluetooth Web App" let body = document.body.object! _ = body.appendChild!(divElement) let buttonElement = document.createElement!("button").object! -buttonElement.innerText = "Click me!" +buttonElement.innerText = "Scan for Bluetooth devices" buttonElement.onclick = .function { _ in + JSConsole.info("Swift is running on browser!") JSConsole.debug("\(#file) \(#function) \(#line)") alert("Swift is running on browser!") - JSConsole.log("Requesting device") + JSConsole.log("Requesting any Bluetooth Device...") bluetooth.requestDevice().then { - JSConsole.info("\($0)") + JSConsole.info($0) + JSConsole.debug("\(#file) \(#function) \(#line)") alert("Got device \($0)") + JSConsole.log("Connecting to GATT Server...") + $0.gatt.connect() + }.then { + JSConsole.info($0) + JSConsole.debug("\(#file) \(#function) \(#line)") + alert("Connected") } JSConsole.debug("\(#file) \(#function) \(#line)") return .undefined diff --git a/Sources/JavaScriptKit/JS Types/JSConsole.swift b/Sources/JavaScriptKit/JS Types/JSConsole.swift index 615d1d6c3..063794312 100644 --- a/Sources/JavaScriptKit/JS Types/JSConsole.swift +++ b/Sources/JavaScriptKit/JS Types/JSConsole.swift @@ -22,36 +22,36 @@ public final class JSConsole: JSType { /** The Console method `log()` outputs a message to the web console. The message may be a single string (with optional substitution values), or it may be any one or more JavaScript objects. */ - public static func log(_ message: String) { - logFunction(message) + public static func log(_ arguments: JSValueConvertible...) { + logFunction.dynamicallyCall(withArguments: arguments) } /** The `console.info()` method outputs an informational message to the Web Console. In Firefox, a small "i" icon is displayed next to these items in the Web Console's log. */ - public static func info(_ message: String) { - infoFunction(message) + public static func info(_ arguments: JSValueConvertible...) { + infoFunction.dynamicallyCall(withArguments: arguments) } /** The console method `debug()` outputs a message to the web console at the "debug" log level. The message is only displayed to the user if the console is configured to display debug output. */ - public static func debug(_ message: String) { - debugFunction(message) + public static func debug(_ arguments: JSValueConvertible...) { + debugFunction.dynamicallyCall(withArguments: arguments) } /** Outputs an error message to the Web Console. */ - public static func error(_ message: String) { - errorFunction(message) + public static func error(_ arguments: JSValueConvertible...) { + errorFunction.dynamicallyCall(withArguments: arguments) } /** The `console.assert()` method writes an error message to the console if the assertion is false. If the assertion is true, nothing happens. */ - public static func assert(_ condition: @autoclosure () -> (Bool), _ message: String) { - assertFunction(condition(), message) + public static func assert(_ condition: @autoclosure () -> (Bool), _ arguments: JSValueConvertible...) { + assertFunction.dynamicallyCall(withArguments: [condition()] + arguments) } } @@ -69,3 +69,16 @@ internal extension JSConsole { static let assertFunction = classObject.assert.function! } +/* +private extension JSConsole { + + /// Console should print objects as their description and not + static func print(_ value: JSValueConvertible) -> JSValueConvertible { + if let value = value as? CustomStringConvertible { + return value.description + } else { + return value + } + } +} +*/ From e14b6aca150a566d41ae76e3ef62f77184d61d1e Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 4 Jun 2020 00:06:13 -0500 Subject: [PATCH 15/38] Updated `JSPromise` --- .../JavaScriptKit/JS Types/JSPromise.swift | 24 ++++++++++++++----- Sources/JavaScriptKit/JSFunctionRef.swift | 5 +++- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Sources/JavaScriptKit/JS Types/JSPromise.swift b/Sources/JavaScriptKit/JS Types/JSPromise.swift index 92b94345e..826858ea4 100644 --- a/Sources/JavaScriptKit/JS Types/JSPromise.swift +++ b/Sources/JavaScriptKit/JS Types/JSPromise.swift @@ -94,13 +94,13 @@ public final class JSPromise: JSType { onRejected: @escaping (JSError) -> () = defaultRejected) -> JSValue { guard let function = jsObject.then.function - else { fatalError("Invalid function \(jsObject.requestDevice)") } + else { fatalError("Invalid function \(#function)") } let success = JSFunctionRef.from { (arguments) in if let value = arguments.first.flatMap({ Success.construct(from: $0) }) { return onFulfilled(value) } else { - JSConsole.error("Unable to load success type \(String(reflecting: Success.self)) from ", arguments.first) + JSConsole.error("Unable to load success type \(String(reflecting: Success.self)) from ", arguments.first ?? "nil") return .undefined } } @@ -109,7 +109,7 @@ public final class JSPromise: JSType { if let value = arguments.first.flatMap({ JSError.construct(from: $0) }) { onRejected(value) } else { - JSConsole.error("Unable to load error from ", arguments.first) + JSConsole.error("Unable to load error from ", arguments.first ?? "nil") } return .undefined } @@ -227,12 +227,24 @@ public final class JSPromise: JSType { return promise } - public func `catch`(_ completion: (JSError) -> ()) { - jsObject.catch.function?() + public func `catch`(_ completion: @escaping (JSError) -> ()) { + guard let function = jsObject.catch.function + else { fatalError("Invalid function \(#function)") } + let errorFunction = JSFunctionRef.from { (arguments) in + if let value = arguments.first.flatMap({ JSError.construct(from: $0) }) { + completion(value) + } else { + JSConsole.error("Unable to load error from ", arguments.first ?? "nil") + } + return .undefined + } + function.apply(this: jsObject, arguments: errorFunction) } public func finally() { - jsObject.finally.function?() + guard let function = jsObject.finally.function + else { fatalError("Invalid function \(#function)") } + function.apply(this: jsObject) } } diff --git a/Sources/JavaScriptKit/JSFunctionRef.swift b/Sources/JavaScriptKit/JSFunctionRef.swift index 70180522a..9de20a82f 100644 --- a/Sources/JavaScriptKit/JSFunctionRef.swift +++ b/Sources/JavaScriptKit/JSFunctionRef.swift @@ -20,9 +20,12 @@ public class JSFunctionRef: JSObjectRef { return result.jsValue() } + @discardableResult public func apply(this: JSObjectRef, arguments: JSValueConvertible...) -> JSValue { - apply(this: this, argumentList: arguments) + return apply(this: this, argumentList: arguments) } + + @discardableResult public func apply(this: JSObjectRef, argumentList: [JSValueConvertible]) -> JSValue { let result = argumentList.withRawJSValues { rawValues in rawValues.withUnsafeBufferPointer { bufferPointer -> RawJSValue in From e4f402559d214cf2d4a26890af5408a7e9bdcb68 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 4 Jun 2020 00:06:29 -0500 Subject: [PATCH 16/38] Updated `JSConsole` --- .../Sources/JavaScriptKitExample/main.swift | 18 ++++++----- .../JavaScriptKit/JS Types/JSConsole.swift | 31 ++++++++++--------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift b/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift index 0db1a81eb..115346412 100644 --- a/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift +++ b/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift @@ -16,16 +16,18 @@ buttonElement.onclick = .function { _ in JSConsole.debug("\(#file) \(#function) \(#line)") alert("Swift is running on browser!") JSConsole.log("Requesting any Bluetooth Device...") - bluetooth.requestDevice().then { - JSConsole.info($0) - JSConsole.debug("\(#file) \(#function) \(#line)") - alert("Got device \($0)") + bluetooth.requestDevice().then { (device: JSBluetoothDevice) -> (JSPromise) in + JSConsole.info(device) + JSConsole.debug("\(#file) \(#function) \(#line) \(device)") + alert("Got device \(device)") JSConsole.log("Connecting to GATT Server...") - $0.gatt.connect() - }.then { - JSConsole.info($0) - JSConsole.debug("\(#file) \(#function) \(#line)") + return device.gatt.connect() + }.then { (server: JSBluetoothRemoteGATTServer) -> () in + JSConsole.info(server) + JSConsole.debug("\(#file) \(#function) \(#line) \(server)") alert("Connected") + }.catch { (error: JSError) in + alert("Error: \(error)") } JSConsole.debug("\(#file) \(#function) \(#line)") return .undefined diff --git a/Sources/JavaScriptKit/JS Types/JSConsole.swift b/Sources/JavaScriptKit/JS Types/JSConsole.swift index 063794312..984ce3c64 100644 --- a/Sources/JavaScriptKit/JS Types/JSConsole.swift +++ b/Sources/JavaScriptKit/JS Types/JSConsole.swift @@ -22,36 +22,36 @@ public final class JSConsole: JSType { /** The Console method `log()` outputs a message to the web console. The message may be a single string (with optional substitution values), or it may be any one or more JavaScript objects. */ - public static func log(_ arguments: JSValueConvertible...) { - logFunction.dynamicallyCall(withArguments: arguments) + public static func log(_ arguments: Any...) { + logFunction.dynamicallyCall(withArguments: arguments.map(print)) } /** The `console.info()` method outputs an informational message to the Web Console. In Firefox, a small "i" icon is displayed next to these items in the Web Console's log. */ - public static func info(_ arguments: JSValueConvertible...) { - infoFunction.dynamicallyCall(withArguments: arguments) + public static func info(_ arguments: Any...) { + infoFunction.dynamicallyCall(withArguments: arguments.map(print)) } /** The console method `debug()` outputs a message to the web console at the "debug" log level. The message is only displayed to the user if the console is configured to display debug output. */ - public static func debug(_ arguments: JSValueConvertible...) { - debugFunction.dynamicallyCall(withArguments: arguments) + public static func debug(_ arguments: Any...) { + debugFunction.dynamicallyCall(withArguments: arguments.map(print)) } /** Outputs an error message to the Web Console. */ - public static func error(_ arguments: JSValueConvertible...) { - errorFunction.dynamicallyCall(withArguments: arguments) + public static func error(_ arguments: Any...) { + errorFunction.dynamicallyCall(withArguments: arguments.map(print)) } /** The `console.assert()` method writes an error message to the console if the assertion is false. If the assertion is true, nothing happens. */ public static func assert(_ condition: @autoclosure () -> (Bool), _ arguments: JSValueConvertible...) { - assertFunction.dynamicallyCall(withArguments: [condition()] + arguments) + assertFunction.dynamicallyCall(withArguments: [condition()] + arguments.map(print)) } } @@ -69,16 +69,19 @@ internal extension JSConsole { static let assertFunction = classObject.assert.function! } -/* + private extension JSConsole { /// Console should print objects as their description and not - static func print(_ value: JSValueConvertible) -> JSValueConvertible { - if let value = value as? CustomStringConvertible { + static func print(_ value: Any) -> JSValueConvertible { + if let value = value as? JSType { + return value + } else if let value = value as? JSValueConvertible { + return value + } else if let value = value as? CustomStringConvertible { return value.description } else { - return value + return "\(value)" } } } -*/ From ea55aa5b2b53f03412f1e8c7bea2c10a0b5fd61e Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 4 Jun 2020 00:25:04 -0500 Subject: [PATCH 17/38] Fixed `JSType.toString()` --- Sources/JavaScriptKit/JS Types/JSType.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JavaScriptKit/JS Types/JSType.swift b/Sources/JavaScriptKit/JS Types/JSType.swift index 20b3bd183..17f639a31 100644 --- a/Sources/JavaScriptKit/JS Types/JSType.swift +++ b/Sources/JavaScriptKit/JS Types/JSType.swift @@ -16,7 +16,7 @@ public protocol JSType: JSValueConvertible, JSValueConstructible { internal extension JSType { func toString() -> String? { - return jsObject.toString.function?().string + return jsObject.toString.function?.apply(this: jsObject).string } } From e002eb8aaca6b80331f7a4757ef982e2019faa66 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 4 Jun 2020 00:30:24 -0500 Subject: [PATCH 18/38] Updated `JSPromise` --- .../JavaScriptKit/JS Types/JSPromise.swift | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/Sources/JavaScriptKit/JS Types/JSPromise.swift b/Sources/JavaScriptKit/JS Types/JSPromise.swift index 826858ea4..50a1bdbea 100644 --- a/Sources/JavaScriptKit/JS Types/JSPromise.swift +++ b/Sources/JavaScriptKit/JS Types/JSPromise.swift @@ -91,7 +91,7 @@ public final class JSPromise: JSType { */ @discardableResult internal func _then(onFulfilled: @escaping (Success) -> (JSValue), - onRejected: @escaping (JSError) -> () = defaultRejected) -> JSValue { + onRejected: @escaping (JSError) -> ()) -> JSValue { guard let function = jsObject.then.function else { fatalError("Invalid function \(#function)") } @@ -117,6 +117,24 @@ public final class JSPromise: JSType { return function.apply(this: jsObject, argumentList: [success.jsValue(), errorFunction.jsValue()]) } + @discardableResult + internal func _then(onFulfilled: @escaping (Success) -> (JSValue)) -> JSValue { + + guard let function = jsObject.then.function + else { fatalError("Invalid function \(#function)") } + + let success = JSFunctionRef.from { (arguments) in + if let value = arguments.first.flatMap({ Success.construct(from: $0) }) { + return onFulfilled(value) + } else { + JSConsole.error("Unable to load success type \(String(reflecting: Success.self)) from ", arguments.first ?? "nil") + return .undefined + } + } + + return function.apply(this: jsObject, argumentList: [success.jsValue()]) + } + /** The `then()` method returns a Promise. It takes up to two arguments: callback functions for the success and failure cases of the Promise. @@ -138,7 +156,7 @@ public final class JSPromise: JSType { onRejected: @escaping (JSError) -> ()) -> JSPromise { let result = _then(onFulfilled: { onFulfilled($0) - return .undefined + return .null }, onRejected: onRejected) guard let promise = result.object.flatMap({ JSPromise($0) }) else { fatalError("Invalid object \(result)") } @@ -193,7 +211,7 @@ public final class JSPromise: JSType { public func then(onFulfilled: @escaping (Success) -> ()) -> JSPromise { let result = _then(onFulfilled: { onFulfilled($0) - return .undefined + return .null }) guard let promise = result.object.flatMap({ JSPromise($0) }) else { fatalError("Invalid object \(result)") } @@ -250,8 +268,6 @@ public final class JSPromise: JSType { private let JSPromiseClassObject = JSObjectRef.global.Promise.function! -internal let defaultRejected: (JSError) -> () = { JSConsole.error("Uncaught promise error ", $0) } - // MARK: - Supporting Types public extension JSPromise { From 195b5e293af8be5509b9b1c08c053f0945290ade Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 4 Jun 2020 01:41:23 -0500 Subject: [PATCH 19/38] Updated logging --- .../JavaScriptKit/JS Types/JSBluetooth.swift | 9 +++++-- .../JS Types/JSBluetoothDevice.swift | 6 +++-- .../JavaScriptKit/JS Types/JSBoolean.swift | 17 ++++++++++++ .../JavaScriptKit/JS Types/JSConsole.swift | 7 +++++ Sources/JavaScriptKit/Utilities.swift | 26 +++++++++++++++++++ 5 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 Sources/JavaScriptKit/Utilities.swift diff --git a/Sources/JavaScriptKit/JS Types/JSBluetooth.swift b/Sources/JavaScriptKit/JS Types/JSBluetooth.swift index 1002da44f..3d099ce17 100644 --- a/Sources/JavaScriptKit/JS Types/JSBluetooth.swift +++ b/Sources/JavaScriptKit/JS Types/JSBluetooth.swift @@ -16,7 +16,7 @@ public final class JSBluetooth: JSType { // MARK: - Initialization - public init(_ jsObject: JSObjectRef) { + public init?(_ jsObject: JSObjectRef) { self.jsObject = jsObject } @@ -42,7 +42,7 @@ public final class JSBluetooth: JSType { // .then(function(bluetoothDevice) { ... }) guard let function = jsObject.requestDevice.function - else { fatalError("Invalid function \(jsObject.requestDevice)") } + else { fatalError("Invalid function \(#function)") } // FIXME: Improve, support all options let options = JSObject() @@ -56,3 +56,8 @@ public final class JSBluetooth: JSType { return promise } } + +internal extension JSBluetooth { + + static let classObject = JSObjectRef.global.Bluetooth.function! +} diff --git a/Sources/JavaScriptKit/JS Types/JSBluetoothDevice.swift b/Sources/JavaScriptKit/JS Types/JSBluetoothDevice.swift index f99eddcd6..cacaec9f7 100644 --- a/Sources/JavaScriptKit/JS Types/JSBluetoothDevice.swift +++ b/Sources/JavaScriptKit/JS Types/JSBluetoothDevice.swift @@ -14,7 +14,7 @@ public final class JSBluetoothDevice: JSType { // MARK: - Initialization - public init(_ jsObject: JSObjectRef) { + public init?(_ jsObject: JSObjectRef) { self.jsObject = jsObject } @@ -24,7 +24,9 @@ public final class JSBluetoothDevice: JSType { public lazy var id: String = self.jsObject.get("id").string! /// A string that provices a human-readable name for the device. - public lazy var name: String? = self.jsObject.name.string + public var name: String? { + return self.jsObject.name.string + } /// Interface of the Web Bluetooth API represents a GATT Server on a remote device. public lazy var gatt = self.jsObject.gatt.object.flatMap({ JSBluetoothRemoteGATTServer($0) })! diff --git a/Sources/JavaScriptKit/JS Types/JSBoolean.swift b/Sources/JavaScriptKit/JS Types/JSBoolean.swift index 5887bb5c0..a9c992715 100644 --- a/Sources/JavaScriptKit/JS Types/JSBoolean.swift +++ b/Sources/JavaScriptKit/JS Types/JSBoolean.swift @@ -23,6 +23,23 @@ public final class JSBoolean: JSType { } } +// MARK: - RawRepresentable + +extension JSBoolean: RawRepresentable { + + public convenience init(rawValue: Bool) { + self.init(rawValue) + } + + public var rawValue: Bool { + guard let function = jsObject.valueOf.function + else { fatalError("Invalid function \(#function)") } + return function.apply(this: jsObject).boolean ?? false + } +} + +// MARK: - Constants + internal extension JSBoolean { static let classObject = JSObjectRef.global.Boolean.function! diff --git a/Sources/JavaScriptKit/JS Types/JSConsole.swift b/Sources/JavaScriptKit/JS Types/JSConsole.swift index 984ce3c64..2c14a8803 100644 --- a/Sources/JavaScriptKit/JS Types/JSConsole.swift +++ b/Sources/JavaScriptKit/JS Types/JSConsole.swift @@ -53,6 +53,13 @@ public final class JSConsole: JSType { public static func assert(_ condition: @autoclosure () -> (Bool), _ arguments: JSValueConvertible...) { assertFunction.dynamicallyCall(withArguments: [condition()] + arguments.map(print)) } + + /** + The `console.assert()` method writes an error message to the console if the assertion is false. If the assertion is true, nothing happens. + */ + public static func assert(_ condition: @autoclosure () -> (JSBoolean), _ arguments: JSValueConvertible...) { + assert(condition().rawValue, arguments) + } } internal extension JSConsole { diff --git a/Sources/JavaScriptKit/Utilities.swift b/Sources/JavaScriptKit/Utilities.swift new file mode 100644 index 000000000..3480b1b54 --- /dev/null +++ b/Sources/JavaScriptKit/Utilities.swift @@ -0,0 +1,26 @@ +// +// Utilities.swift +// +// +// Created by Alsey Coleman Miller on 6/4/20. +// + +/// Unconditionally prints a given message and stops execution. +internal func fatalError(_ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) -> Never { + let message = message() + JSConsole.error("Fatal error: \(message)") + Swift.fatalError(message, file: file, line: line) +} + +/// Performs a traditional C-style assert with an optional message. +internal func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) { + let condition = condition() + let message = message() + JSConsole.assert(condition, "Assertion failure: \(message)") + Swift.assert(condition, message, file: file, line: line) +} + +/// Performs a traditional C-style assert with an optional message. +internal func assert(_ condition: @autoclosure () -> JSBoolean, _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) { + assert(condition().rawValue, message(), file: file, line: line) +} From f1be2de602329412e186a9407edfb430fa8d919f Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 4 Jun 2020 02:06:29 -0500 Subject: [PATCH 20/38] Added `JSBluetooth.isAvailable` --- .../JavaScriptKit/JS Types/JSBluetooth.swift | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Sources/JavaScriptKit/JS Types/JSBluetooth.swift b/Sources/JavaScriptKit/JS Types/JSBluetooth.swift index 3d099ce17..adb2a7127 100644 --- a/Sources/JavaScriptKit/JS Types/JSBluetooth.swift +++ b/Sources/JavaScriptKit/JS Types/JSBluetooth.swift @@ -22,6 +22,17 @@ public final class JSBluetooth: JSType { public static var shared: JSBluetooth? { return JSNavigator.shared?.bluetooth } + // MARK: - Accessors + + public var isAvailable: JSPromise { + guard let function = jsObject.getAvailability.function + else { fatalError("Invalid function \(#function)") } + let result = function.apply(this: jsObject) + guard let promise = result.object.flatMap({ JSPromise($0) }) + else { fatalError("Invalid object \(result)") } + return promise + } + // MARK: - Methods /// Returns a Promise to a BluetoothDevice object with the specified options. @@ -46,7 +57,8 @@ public final class JSBluetooth: JSType { // FIXME: Improve, support all options let options = JSObject() - options[Option.acceptAllDevices.rawValue] = JSBoolean(acceptAllDevices).jsValue() + options[Option.acceptAllDevices.rawValue] = acceptAllDevices.jsValue() + options[Option.optionalServices.rawValue] = ["device_information"].jsValue() let result = function.apply(this: jsObject, arguments: options) @@ -56,8 +68,3 @@ public final class JSBluetooth: JSType { return promise } } - -internal extension JSBluetooth { - - static let classObject = JSObjectRef.global.Bluetooth.function! -} From 7eb458790a83f6f0a5b0ac5edec0e196bc48d74d Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 4 Jun 2020 02:06:40 -0500 Subject: [PATCH 21/38] Updated `JSPromise` --- Sources/JavaScriptKit/JS Types/JSPromise.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JavaScriptKit/JS Types/JSPromise.swift b/Sources/JavaScriptKit/JS Types/JSPromise.swift index 50a1bdbea..302a88ea5 100644 --- a/Sources/JavaScriptKit/JS Types/JSPromise.swift +++ b/Sources/JavaScriptKit/JS Types/JSPromise.swift @@ -10,7 +10,7 @@ A Promise is a proxy for a value not necessarily known when the promise is created. It allows you to associate handlers with an asynchronous action's eventual success value or failure reason. This lets asynchronous methods return values like synchronous methods: instead of immediately returning the final value, the asynchronous method returns a promise to supply the value at some point in the future. */ -public final class JSPromise: JSType { +public final class JSPromise: JSType where Success: JSValueConvertible, Success: JSValueConstructible { // MARK: - Properties From b1a06cb0fb5427d2e70d0e7b1ed248b8408db500 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 4 Jun 2020 02:07:32 -0500 Subject: [PATCH 22/38] Added `JSBluetoothRemoteGATTService` --- .../Sources/JavaScriptKitExample/main.swift | 63 ++++++++++++------- .../JSBluetoothRemoteGATTServer.swift | 20 +++++- .../JSBluetoothRemoteGATTService.swift | 36 +++++++++++ 3 files changed, 93 insertions(+), 26 deletions(-) create mode 100644 Sources/JavaScriptKit/JS Types/JSBluetoothRemoteGATTService.swift diff --git a/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift b/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift index 115346412..38561ee17 100644 --- a/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift +++ b/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift @@ -2,35 +2,50 @@ import JavaScriptKit let alert = JSObjectRef.global.alert.function! let document = JSObjectRef.global.document.object! -let bluetooth = JSBluetooth.shared! let divElement = document.createElement!("div").object! divElement.innerText = "Swift Bluetooth Web App" let body = document.body.object! _ = body.appendChild!(divElement) -let buttonElement = document.createElement!("button").object! -buttonElement.innerText = "Scan for Bluetooth devices" -buttonElement.onclick = .function { _ in - JSConsole.info("Swift is running on browser!") - JSConsole.debug("\(#file) \(#function) \(#line)") - alert("Swift is running on browser!") - JSConsole.log("Requesting any Bluetooth Device...") - bluetooth.requestDevice().then { (device: JSBluetoothDevice) -> (JSPromise) in - JSConsole.info(device) - JSConsole.debug("\(#file) \(#function) \(#line) \(device)") - alert("Got device \(device)") - JSConsole.log("Connecting to GATT Server...") - return device.gatt.connect() - }.then { (server: JSBluetoothRemoteGATTServer) -> () in - JSConsole.info(server) - JSConsole.debug("\(#file) \(#function) \(#line) \(server)") - alert("Connected") - }.catch { (error: JSError) in - alert("Error: \(error)") +if let bluetooth = JSBluetooth.shared { + bluetooth.isAvailable.then { + JSConsole.assert($0, "Bluetooth not available") } - JSConsole.debug("\(#file) \(#function) \(#line)") - return .undefined + let buttonElement = document.createElement!("button").object! + buttonElement.innerText = "Scan for Bluetooth devices" + buttonElement.onclick = .function { _ in + JSConsole.info("Swift is running on browser!") + JSConsole.debug("\(#file) \(#function) \(#line)") + alert("Swift is running on browser!") + JSConsole.log("Requesting any Bluetooth Device...") + bluetooth.requestDevice().then { (device: JSBluetoothDevice) -> (JSPromise) in + JSConsole.info(device) + JSConsole.debug("\(#file) \(#function) \(#line) \(device)") + alert("Got device \(device)") + JSConsole.log("Connecting to GATT Server...") + return device.gatt.connect() + }.then { (server: JSBluetoothRemoteGATTServer) -> (JSPromise) in + JSConsole.info(server) + JSConsole.debug("\(#file) \(#function) \(#line) \(server)") + alert("Connected") + JSConsole.log("Getting Device Information Service...") + return server.getPrimaryService("device_information") + }.then { (service: JSBluetoothRemoteGATTService) -> () in + JSConsole.info(service) + JSConsole.debug("\(#file) \(#function) \(#line) isPrimary \(service.isPrimary) uuid \(service.uuid)") + }.catch { (error: JSError) in + JSConsole.debug(#file, #function, #line) + JSConsole.error(error) + alert("Error: \(error.message)") + } + JSConsole.debug("\(#file) \(#function) \(#line)") + return .undefined + } + _ = body.appendChild!(buttonElement) +} else { + JSConsole.error("Cannot access Bluetooth API") + let divElement = document.createElement!("div").object! + divElement.innerText = "Bluetooth Web API not enabled" + _ = body.appendChild!(divElement) } - -_ = body.appendChild!(buttonElement) diff --git a/Sources/JavaScriptKit/JS Types/JSBluetoothRemoteGATTServer.swift b/Sources/JavaScriptKit/JS Types/JSBluetoothRemoteGATTServer.swift index 650942bb6..0dce69baf 100644 --- a/Sources/JavaScriptKit/JS Types/JSBluetoothRemoteGATTServer.swift +++ b/Sources/JavaScriptKit/JS Types/JSBluetoothRemoteGATTServer.swift @@ -15,7 +15,7 @@ public final class JSBluetoothRemoteGATTServer: JSType { // MARK: - Initialization - public init(_ jsObject: JSObjectRef) { + public init?(_ jsObject: JSObjectRef) { self.jsObject = jsObject } @@ -27,6 +27,7 @@ public final class JSBluetoothRemoteGATTServer: JSType { // MARK: - Methods + /// Causes the script execution environment to connect to this device. public func connect() -> JSPromise { guard let function = jsObject.connect.function else { fatalError("Missing function \(#function)") } @@ -36,7 +37,22 @@ public final class JSBluetoothRemoteGATTServer: JSType { return promise } + /// Causes the script execution environment to disconnect from this device. public func disconnect() { - let _ = jsObject.disconnect.function?.apply(this: jsObject) + guard let function = jsObject.disconnect.function + else { fatalError("Missing function \(#function)") } + function.apply(this: jsObject) + } + + /// Returns a promise to the primary BluetoothGATTService offered by the bluetooth device for a specified BluetoothServiceUUID. + /// + /// - Parameter uuid: A Bluetooth service universally unique identifier for a specified device. + public func getPrimaryService(_ uuid: String) -> JSPromise { + guard let function = jsObject.getPrimaryService.function + else { fatalError("Missing function \(#function)") } + let result = function.apply(this: jsObject, arguments: uuid) + guard let value = result.object.flatMap({ JSPromise($0) }) + else { fatalError("Invalid object \(result)") } + return value } } diff --git a/Sources/JavaScriptKit/JS Types/JSBluetoothRemoteGATTService.swift b/Sources/JavaScriptKit/JS Types/JSBluetoothRemoteGATTService.swift new file mode 100644 index 000000000..78df84d3b --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSBluetoothRemoteGATTService.swift @@ -0,0 +1,36 @@ +// +// JSBluetoothRemoteGATTService.swift +// +// +// Created by Alsey Coleman Miller on 6/4/20. +// + +/** + JavaScript Bluetooth GATT Service + + The [`BluetoothRemoteGATTService`](https://developer.mozilla.org/en-US/docs/Web/API/BluetoothRemoteGATTService) interface of the [Web Bluetooth API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API) represents a service provided by a GATT server, including a device, a list of referenced services, and a list of the characteristics of this service. + */ +public final class JSBluetoothRemoteGATTService: JSType { + + // MARK: - Properties + + public let jsObject: JSObjectRef + + // MARK: - Initialization + + public init?(_ jsObject: JSObjectRef) { + self.jsObject = jsObject + } + + // MARK: - Accessors + + public lazy var isPrimary: Bool = jsObject.isPrimary.boolean ?? false + + public lazy var uuid: String = jsObject.uuid.string ?? "" + + // MARK: - Methods + + +} + + From 235a4fe28fe01d5ffc088d10390f52a64f84b33e Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 4 Jun 2020 02:21:12 -0500 Subject: [PATCH 23/38] Updated `JSPromise` --- Sources/JavaScriptKit/JS Types/JSError.swift | 20 ------------------- .../JavaScriptKit/JS Types/JSPromise.swift | 6 +++--- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/Sources/JavaScriptKit/JS Types/JSError.swift b/Sources/JavaScriptKit/JS Types/JSError.swift index a7d213c73..85703b5f7 100644 --- a/Sources/JavaScriptKit/JS Types/JSError.swift +++ b/Sources/JavaScriptKit/JS Types/JSError.swift @@ -43,23 +43,3 @@ internal extension JSError { // MARK: - CustomStringConvertible extension JSError: CustomStringConvertible { } - -// MARK: - Extensions - -public extension JSValue { - - /// Cast error to JavaScript value. - init(error: Error) { - if let value = error as? JSValueConvertible { - // convert to JavaScript value if supported. - self = value.jsValue() - } else if let stringConvertible = error as? CustomStringConvertible { - // use decription for error - self = stringConvertible.description.jsValue() - } else { - // default to printing description - self = String(reflecting: error).jsValue() - } - } -} - diff --git a/Sources/JavaScriptKit/JS Types/JSPromise.swift b/Sources/JavaScriptKit/JS Types/JSPromise.swift index 302a88ea5..cc7f56582 100644 --- a/Sources/JavaScriptKit/JS Types/JSPromise.swift +++ b/Sources/JavaScriptKit/JS Types/JSPromise.swift @@ -28,11 +28,11 @@ public final class JSPromise: JSType where Success: JSValueConvertible, - parameter executor: A function to be executed by the Promise. The executor is custom code that ties an outcome to a promise. You, the programmer, write the executor. */ - public init(executor block: @escaping ((Success) -> (), (Error) -> ()) -> ()) { + public init(executor block: @escaping ((Success) -> (), (JSError) -> ()) -> ()) { let executor = JSFunctionRef.from { (arguments) in let resolutionFunc = arguments[0].function let rejectionFunc = arguments[1].function - block({ resolutionFunc?($0.jsValue()) }, { rejectionFunc?(JSValue(error: $0)) }) + block({ resolutionFunc?($0.jsValue()) }, { rejectionFunc?($0.jsValue()) }) return .null } self.jsObject = JSPromiseClassObject.new(executor) @@ -43,7 +43,7 @@ public final class JSPromise: JSType where Success: JSValueConvertible, - parameter executor: A function to be executed by the Promise. The executor is custom code that ties an outcome to a promise. You, the programmer, write the executor. */ - public convenience init(executor block: @escaping ((Result) -> ()) -> ()) { + public convenience init(executor block: @escaping ((Result) -> ()) -> ()) { self.init { (resolution, rejection) in block({ switch $0 { From 31e87ef1efd776a45c208cb215062df0e21f0536 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 4 Jun 2020 02:21:25 -0500 Subject: [PATCH 24/38] Renamed `JSDecoder` --- .../{JSValueDecoder.swift => JSDecoder.swift} | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) rename Sources/JavaScriptKit/{JSValueDecoder.swift => JSDecoder.swift} (96%) diff --git a/Sources/JavaScriptKit/JSValueDecoder.swift b/Sources/JavaScriptKit/JSDecoder.swift similarity index 96% rename from Sources/JavaScriptKit/JSValueDecoder.swift rename to Sources/JavaScriptKit/JSDecoder.swift index 0bf9a426b..990e2433f 100644 --- a/Sources/JavaScriptKit/JSValueDecoder.swift +++ b/Sources/JavaScriptKit/JSDecoder.swift @@ -1,3 +1,22 @@ +/// JavaScript Decoder +public struct JSDecoder { + + // MARK: - Properties + + public var userInfo: [CodingUserInfoKey: Any] = [:] + + // MARK: - Initialization + + public init() { } + + // MARK: - Methods + + public func decode(_ type: T.Type = T.self, from value: JSValue) throws -> T where T: Decodable { + let decoder = _Decoder(referencing: value, userInfo: userInfo) + return try T(from: decoder) + } +} + private struct _Decoder: Decoder { fileprivate let node: JSValue @@ -232,17 +251,3 @@ extension _Decoder: SingleValueDecodingContainer { return try primitive(node) ?? type.init(from: self) } } - -public class JSValueDecoder { - - public init() {} - - public func decode( - _ type: T.Type = T.self, - from value: JSValue, - userInfo: [CodingUserInfoKey: Any] = [:] - ) throws -> T where T: Decodable { - let decoder = _Decoder(referencing: value, userInfo: userInfo) - return try T(from: decoder) - } -} From 8d5a12bbe0f62c51b6c72276a3048f7613e9b127 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 4 Jun 2020 03:25:39 -0500 Subject: [PATCH 25/38] Fixed `JSConsole.assert()` --- Sources/JavaScriptKit/JS Types/JSConsole.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/JavaScriptKit/JS Types/JSConsole.swift b/Sources/JavaScriptKit/JS Types/JSConsole.swift index 2c14a8803..28a9839e6 100644 --- a/Sources/JavaScriptKit/JS Types/JSConsole.swift +++ b/Sources/JavaScriptKit/JS Types/JSConsole.swift @@ -58,7 +58,7 @@ public final class JSConsole: JSType { The `console.assert()` method writes an error message to the console if the assertion is false. If the assertion is true, nothing happens. */ public static func assert(_ condition: @autoclosure () -> (JSBoolean), _ arguments: JSValueConvertible...) { - assert(condition().rawValue, arguments) + assertFunction.dynamicallyCall(withArguments: [condition()] + arguments.map(print)) } } From 23d930b55537500fd8a9d23969a47429fc6c4147 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 4 Jun 2020 21:44:56 -0500 Subject: [PATCH 26/38] Added `JSBluetooth.devices` --- .../JavaScriptKit/Extensions/CodingKey.swift | 29 +++++++ .../JavaScriptKit/JS Types/JSBluetooth.swift | 76 ++++++++++++++----- Sources/JavaScriptKit/JSDecoder.swift | 6 +- .../JavaScriptKit/JSValueConstructible.swift | 7 ++ .../JavaScriptKit/JSValueConvertible.swift | 20 +++-- 5 files changed, 113 insertions(+), 25 deletions(-) create mode 100644 Sources/JavaScriptKit/Extensions/CodingKey.swift diff --git a/Sources/JavaScriptKit/Extensions/CodingKey.swift b/Sources/JavaScriptKit/Extensions/CodingKey.swift new file mode 100644 index 000000000..71737854e --- /dev/null +++ b/Sources/JavaScriptKit/Extensions/CodingKey.swift @@ -0,0 +1,29 @@ +// +// CodingKey.swift +// +// +// Created by Alsey Coleman Miller on 5/30/20. +// + +internal extension Sequence where Element == CodingKey { + + /// KVC path string for current coding path. + var path: String { + return reduce("", { $0 + "\($0.isEmpty ? "" : ".")" + $1.stringValue }) + } +} + +internal extension CodingKey { + + static var sanitizedName: String { + + let rawName = String(reflecting: self) + var elements = rawName.split(separator: ".") + guard elements.count > 2 + else { return rawName } + elements.removeFirst() + // FIXME: Needs Foundation for String extensions + //elements.removeAll { $0.contains("(unknown context") } + return elements.reduce("", { $0 + ($0.isEmpty ? "" : ".") + $1 }) + } +} diff --git a/Sources/JavaScriptKit/JS Types/JSBluetooth.swift b/Sources/JavaScriptKit/JS Types/JSBluetooth.swift index adb2a7127..e2ec6365b 100644 --- a/Sources/JavaScriptKit/JS Types/JSBluetooth.swift +++ b/Sources/JavaScriptKit/JS Types/JSBluetooth.swift @@ -24,6 +24,9 @@ public final class JSBluetooth: JSType { // MARK: - Accessors + /** + Returns a Promise that resolved to a Boolean indicating whether the user-agent has the ability to support Bluetooth. Some user-agents let the user configure an option that affects what is returned by this value. If this option is set, that is the value returned by this method. + */ public var isAvailable: JSPromise { guard let function = jsObject.getAvailability.function else { fatalError("Invalid function \(#function)") } @@ -33,38 +36,77 @@ public final class JSBluetooth: JSType { return promise } + /** + Returns a `Promise` that resolved to an array of `BluetoothDevice` which the origin already obtained permission for via a call to `Bluetooth.requestDevice()`. + */ + public var devices: JSPromise<[JSBluetoothDevice]> { + guard let function = jsObject.getDevices.function + else { fatalError("Invalid function \(#function)") } + let result = function.apply(this: jsObject) + guard let promise = result.object.flatMap({ JSPromise<[JSBluetoothDevice]>($0) }) + else { fatalError("Invalid object \(result)") } + return promise + } + // MARK: - Methods /// Returns a Promise to a BluetoothDevice object with the specified options. /// If there is no chooser UI, this method returns the first device matching the criteria. /// /// - Returns: A Promise to a `BluetoothDevice` object. - public func requestDevice(//filters: [] = [], - //services: [] = [], - acceptAllDevices: Bool = true) -> JSPromise { - - enum Option: String { - case filters - case optionalServices - case acceptAllDevices - } + public func requestDevice() -> JSPromise { + let options = RequestDeviceOptions(filters: nil, optionalServices: nil, acceptAllDevices: true) + return requestDevice(options: options) + } + + /// Returns a Promise to a BluetoothDevice object with the specified options. + /// If there is no chooser UI, this method returns the first device matching the criteria. + /// + /// - Returns: A Promise to a `BluetoothDevice` object. + internal func requestDevice(options: RequestDeviceOptions) -> JSPromise { // Bluetooth.requestDevice([options]) // .then(function(bluetoothDevice) { ... }) guard let function = jsObject.requestDevice.function else { fatalError("Invalid function \(#function)") } - - // FIXME: Improve, support all options - let options = JSObject() - options[Option.acceptAllDevices.rawValue] = acceptAllDevices.jsValue() - options[Option.optionalServices.rawValue] = ["device_information"].jsValue() - let result = function.apply(this: jsObject, arguments: options) - guard let promise = result.object.flatMap({ JSPromise($0) }) else { fatalError("Invalid object \(result)") } - return promise } } + +// MARK: - Supporting Types + +public extension JSBluetooth { + + struct ScanFilter: Equatable, Hashable, Codable { + + public var services: [String]? + + public var name: String? + + public var namePrefix: String? + + public init(services: [String]? = nil, + name: String? = nil, + namePrefix: String? = nil) { + self.services = services + self.name = name + self.namePrefix = namePrefix + } + } +} + +internal extension JSBluetooth { + + struct RequestDeviceOptions: Encodable, JSValueConvertible { + + var filters: [ScanFilter]? + + var optionalServices: [String]? + + var acceptAllDevices: Bool? + } +} diff --git a/Sources/JavaScriptKit/JSDecoder.swift b/Sources/JavaScriptKit/JSDecoder.swift index 990e2433f..900a15387 100644 --- a/Sources/JavaScriptKit/JSDecoder.swift +++ b/Sources/JavaScriptKit/JSDecoder.swift @@ -3,7 +3,11 @@ public struct JSDecoder { // MARK: - Properties - public var userInfo: [CodingUserInfoKey: Any] = [:] + /// Any contextual information set by the user for encoding. + public var userInfo = [CodingUserInfoKey : Any]() + + /// Logging handler + public var log: ((String) -> ())? // MARK: - Initialization diff --git a/Sources/JavaScriptKit/JSValueConstructible.swift b/Sources/JavaScriptKit/JSValueConstructible.swift index 2a312bf65..948139d50 100644 --- a/Sources/JavaScriptKit/JSValueConstructible.swift +++ b/Sources/JavaScriptKit/JSValueConstructible.swift @@ -85,3 +85,10 @@ extension UInt64: JSValueConstructible { value.number.map(Self.init) } } + +extension Array: JSValueConstructible where Element: JSValueConstructible { + + public static func construct(from value: JSValue) -> Self? { + return value.array?.compactMap { Element.construct(from: $0) } + } +} diff --git a/Sources/JavaScriptKit/JSValueConvertible.swift b/Sources/JavaScriptKit/JSValueConvertible.swift index 9c0e3d316..654085a5b 100644 --- a/Sources/JavaScriptKit/JSValueConvertible.swift +++ b/Sources/JavaScriptKit/JSValueConvertible.swift @@ -83,13 +83,7 @@ extension Dictionary: JSValueConvertible where Value == JSValueConvertible, Key } } -extension Array where Element: JSValueConvertible { - public func jsValue() -> JSValue { - Swift.Array.jsValue(self)() - } -} - -extension Array: JSValueConvertible where Element == JSValueConvertible { +extension Array: JSValueConvertible where Element: JSValueConvertible { public func jsValue() -> JSValue { return .object(JSArray(self).jsObject) } @@ -192,3 +186,15 @@ extension Array where Element: JSValueConvertible { Swift.Array.withRawJSValues(self)(body) } } + +extension JSValueConvertible where Self: Encodable { + + func jsValue() -> JSValue { + let encoder = JSEncoder() + do { return try encoder.encode(self) } + catch { + JSConsole.error(error) + return .undefined + } + } +} From 435652195baca146a222a031a3dc7a0d9864544d Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 4 Jun 2020 21:45:20 -0500 Subject: [PATCH 27/38] Added `JSEncoder` --- Sources/JavaScriptKit/JSEncoder.swift | 460 ++++++++++++++++++++++++++ 1 file changed, 460 insertions(+) create mode 100644 Sources/JavaScriptKit/JSEncoder.swift diff --git a/Sources/JavaScriptKit/JSEncoder.swift b/Sources/JavaScriptKit/JSEncoder.swift new file mode 100644 index 000000000..a5bb45ee8 --- /dev/null +++ b/Sources/JavaScriptKit/JSEncoder.swift @@ -0,0 +1,460 @@ +// +// JSEncoder.swift +// +// +// Created by Alsey Coleman Miller on 6/4/20. +// + +#if arch(wasm32) +import SwiftFoundation +#else +import Foundation +#endif + +/// JavaScript Encoder +public struct JSEncoder { + + // MARK: - Properties + + /// Any contextual information set by the user for encoding. + public var userInfo = [CodingUserInfoKey : Any]() + + /// Logging handler + public var log: ((String) -> ())? + + // MARK: - Initialization + + public init() { } + + // MARK: - Methods + + public func encode(_ encodable: T) throws -> JSValue { + + log?("Will encode \(T.self)") + + if let value = encodable as? JSValueConvertible { + return value.jsValue() + } else { + /* + let encoder = Encoder( + userInfo: userInfo, + log: log + ) + try encodable.encode(to: encoder) + return encoder.object.jsValue() + */ + fatalError() + } + } +} +/* +// MARK: - Encoder + +internal extension JSEncoder { + + final class Encoder: Swift.Encoder { + + // MARK: - Properties + + /// The path of coding keys taken to get to this point in encoding. + fileprivate(set) var codingPath: [CodingKey] + + /// Any contextual information set by the user for encoding. + let userInfo: [CodingUserInfoKey : Any] + + /// Logger + let log: ((String) -> ())? + + /// Output + internal let object = JSObject() + + // MARK: - Initialization + + fileprivate init(codingPath: [CodingKey] = [], + userInfo: [CodingUserInfoKey : Any], + log: ((String) -> ())?) { + + self.codingPath = codingPath + self.userInfo = userInfo + self.log = log + } + + // MARK: - Encoder + + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { + + log?("Requested container keyed by \(type.sanitizedName) for path \"\(codingPath.path)\"") + let keyedContainer = JSKeyedEncodingContainer(referencing: self) + return KeyedEncodingContainer(keyedContainer) + } + + func unkeyedContainer() -> UnkeyedEncodingContainer { + + log?("Requested unkeyed container for path \"\(codingPath.path)\"") + return JSUnkeyedEncodingContainer(referencing: self) + } + + func singleValueContainer() -> SingleValueEncodingContainer { + + log?("Requested single value container for path \"\(codingPath.path)\"") + return JSSingleValueEncodingContainer(referencing: self) + } + } +} + +internal extension JSEncoder.Encoder { + + @inline(__always) + func box (_ value: T) -> JSValue { + return value.jsValue() + } + + func boxEncodable (_ value: T) throws -> JSValue { + + if let data = value as? Data { + return data + } else if let uuid = value as? UUID { + return boxUUID(uuid) + } else if let date = value as? Date { + return boxDate(date) + } else if let tlvEncodable = value as? JSValueConvertible { + return tlvEncodable.jsValue() + } else { + // encode using Encodable, should push new container. + try value.encode(to: self) + let nestedContainer = stack.pop() + return nestedContainer.data + } + } +} + +private extension JSEncoder.Encoder { + + func boxData(_ data: Data) -> JSValue { + return uuid.uuidString + } + + func boxUUID(_ uuid: UUID) -> JSValue { + return uuid.uuidString + } + + func boxDate(_ date: Date) -> Data { + + switch options.dateFormatting { + case .secondsSince1970: + return boxDouble(date.timeIntervalSince1970) + case .millisecondsSince1970: + return boxDouble(date.timeIntervalSince1970 * 1000) + case .iso8601: + guard #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) + else { fatalError("ISO8601DateFormatter is unavailable on this platform.") } + return boxDate(date, using: TLVDateFormatting.iso8601Formatter) + case let .formatted(formatter): + return boxDate(date, using: formatter) + } + } + + func boxDate (_ date: Date, using formatter: T) -> Data { + return box(formatter.string(from: date)) + } +} + +// MARK: - KeyedEncodingContainerProtocol + +internal struct JSKeyedEncodingContainer : KeyedEncodingContainerProtocol { + + typealias Key = K + + // MARK: - Properties + + /// A reference to the encoder we're writing to. + let encoder: JSEncoder.Encoder + + /// The path of coding keys taken to get to this point in encoding. + let codingPath: [CodingKey] + + // MARK: - Initialization + + init(referencing encoder: JSEncoder.Encoder) { + + self.encoder = encoder + self.codingPath = encoder.codingPath + } + + // MARK: - Methods + + func encodeNil(forKey key: K) throws { + // do nothing + } + + func encode(_ value: Bool, forKey key: K) throws { + try encodeRaw(value, forKey: key) + } + + func encode(_ value: Int, forKey key: K) throws { + try encodeRaw(value, forKey: key) + } + + func encode(_ value: Int8, forKey key: K) throws { + try encodeRaw(value, forKey: key) + } + + func encode(_ value: Int16, forKey key: K) throws { + try encodeRaw(value, forKey: key) + } + + func encode(_ value: Int32, forKey key: K) throws { + try encodeRaw(value, forKey: key) + } + + func encode(_ value: Int64, forKey key: K) throws { + try encodeRaw(value, forKey: key) + } + + func encode(_ value: UInt, forKey key: K) throws { + try encodeRaw(value, forKey: key) + } + + func encode(_ value: UInt8, forKey key: K) throws { + try encodeRaw(value, forKey: key) + } + + func encode(_ value: UInt16, forKey key: K) throws { + try encodeRaw(value, forKey: key) + } + + func encode(_ value: UInt32, forKey key: K) throws { + try encodeRaw(value, forKey: key) + } + + func encode(_ value: UInt64, forKey key: K) throws { + try encodeRaw(value, forKey: key) + } + + func encode(_ value: Float, forKey key: K) throws { + try encodeRaw(value, forKey: key) + } + + func encode(_ value: Double, forKey key: K) throws { + try encodeRaw(value, forKey: key) + } + + func encode(_ value: String, forKey key: K) throws { + self.encoder.codingPath.append(key) + defer { self.encoder.codingPath.removeLast() } + let data = try encoder.boxString(value) + try setValue(value, data: data, for: key) + } + + func encode (_ value: T, forKey key: K) throws { + + self.encoder.codingPath.append(key) + defer { self.encoder.codingPath.removeLast() } + encoder.log?("Will encode \(T.self) at path \"\(encoder.codingPath.path)\"") + try encoder.writeEncodable(value) + } + + func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: K) -> KeyedEncodingContainer where NestedKey : CodingKey { + fatalError() + } + + func nestedUnkeyedContainer(forKey key: K) -> UnkeyedEncodingContainer { + fatalError() + } + + func superEncoder() -> Encoder { + fatalError() + } + + func superEncoder(forKey key: K) -> Encoder { + fatalError() + } + + // MARK: Private Methods + + private func encodeRaw (_ value: T, forKey key: K) throws { + + self.encoder.codingPath.append(key) + defer { self.encoder.codingPath.removeLast() } + try setValue(value, value.jsValue(), for: key) + } + + private func setValue (_ value: T, _ encodedValue: JSValue, for key: Key) throws { + encoder.log?("Will encode \(T.self) at path \"\(encoder.codingPath.path)\"") + self.encoder.write(data) + } +} + +// MARK: - SingleValueEncodingContainer + +internal final class JSSingleValueEncodingContainer: SingleValueEncodingContainer { + + // MARK: - Properties + + /// A reference to the encoder we're writing to. + let encoder: JSEncoder.Encoder + + /// The path of coding keys taken to get to this point in encoding. + let codingPath: [CodingKey] + + /// Whether the data has been written + private var didWrite = false + + // MARK: - Initialization + + init(referencing encoder: JSEncoder.Encoder) { + + self.encoder = encoder + self.codingPath = encoder.codingPath + } + + // MARK: - Methods + + func encodeNil() throws { + // do nothing + } + + func encode(_ value: Bool) throws { write(encoder.box(value)) } + + func encode(_ value: String) throws { try write(encoder.boxString(value)) } + + func encode(_ value: Double) throws { write(encoder.boxDouble(value)) } + + func encode(_ value: Float) throws { write(encoder.boxFloat(value)) } + + func encode(_ value: Int) throws { write(encoder.boxNumeric(Int32(value))) } + + func encode(_ value: Int8) throws { write(encoder.box(value)) } + + func encode(_ value: Int16) throws { write(encoder.boxNumeric(value)) } + + func encode(_ value: Int32) throws { write(encoder.boxNumeric(value)) } + + func encode(_ value: Int64) throws { write(encoder.boxNumeric(value)) } + + func encode(_ value: UInt) throws { write(encoder.boxNumeric(UInt32(value))) } + + func encode(_ value: UInt8) throws { write(encoder.box(value)) } + + func encode(_ value: UInt16) throws { write(encoder.boxNumeric(value)) } + + func encode(_ value: UInt32) throws { write(encoder.boxNumeric(value)) } + + func encode(_ value: UInt64) throws { write(encoder.boxNumeric(value)) } + + func encode (_ value: T) throws { + precondition(didWrite == false, "Data already written") + try encoder.writeEncodable(value) + self.didWrite = true + } + + // MARK: - Private Methods + + private func write(_ data: Data) { + + precondition(didWrite == false, "Data already written") + self.encoder.write(data) + self.didWrite = true + } +} + +// MARK: - UnkeyedEncodingContainer + +internal final class JSUnkeyedEncodingContainer: UnkeyedEncodingContainer { + + // MARK: - Properties + + /// A reference to the encoder we're writing to. + let encoder: JSEncoder.Encoder + + /// The path of coding keys taken to get to this point in encoding. + let codingPath: [CodingKey] + + private var countOffset: Int? + + /// The number of elements encoded into the container. + private(set) var count: Int = 0 + + // MARK: - Initialization + + deinit { + // update count byte + self.countOffset.flatMap { self.encoder.data[$0] = UInt8(self.count) } + } + + init(referencing encoder: JSEncoder.Encoder) { + self.encoder = encoder + self.codingPath = encoder.codingPath + let formatting = encoder.formatting.array + switch formatting { + case .lengthPrefix: + // write count byte + self.countOffset = self.encoder.data.count + self.encoder.write(Data([0])) + case .remainder: + self.countOffset = nil + } + } + + // MARK: - Methods + + func encodeNil() throws { + throw EncodingError.invalidValue(Optional.self, EncodingError.Context(codingPath: self.codingPath, debugDescription: "Cannot encode nil in an array")) + } + + func encode(_ value: Bool) throws { append(encoder.box(value)) } + + func encode(_ value: String) throws { try append(encoder.boxString(value)) } + + func encode(_ value: Double) throws { append(encoder.boxNumeric(value.bitPattern)) } + + func encode(_ value: Float) throws { append(encoder.boxNumeric(value.bitPattern)) } + + func encode(_ value: Int) throws { append(encoder.boxNumeric(Int32(value))) } + + func encode(_ value: Int8) throws { append(encoder.box(value)) } + + func encode(_ value: Int16) throws { append(encoder.boxNumeric(value)) } + + func encode(_ value: Int32) throws { append(encoder.boxNumeric(value)) } + + func encode(_ value: Int64) throws { append(encoder.boxNumeric(value)) } + + func encode(_ value: UInt) throws { append(encoder.boxNumeric(UInt32(value))) } + + func encode(_ value: UInt8) throws { append(encoder.box(value)) } + + func encode(_ value: UInt16) throws { append(encoder.boxNumeric(value)) } + + func encode(_ value: UInt32) throws { append(encoder.boxNumeric(value)) } + + func encode(_ value: UInt64) throws { append(encoder.boxNumeric(value)) } + + func encode (_ value: T) throws { + assert(count < Int(UInt8.max), "Cannot encode more than \(UInt8.max) elements") + try encoder.writeEncodable(value) + count += 1 + } + + func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey { + fatalError() + } + + func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + fatalError() + } + + func superEncoder() -> Encoder { + fatalError() + } + + // MARK: - Private Methods + + private func append(_ data: Data) { + assert(count < Int(UInt8.max), "Cannot encode more than \(UInt8.max) elements") + // write element data + encoder.write(data) + count += 1 + } +} +*/ From 4ed88b34aef923e9112e48d2f754d7cdfb991e39 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 4 Jun 2020 21:45:31 -0500 Subject: [PATCH 28/38] Added `JSObject.keys` --- Sources/JavaScriptKit/JS Types/JSObject.swift | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Sources/JavaScriptKit/JS Types/JSObject.swift b/Sources/JavaScriptKit/JS Types/JSObject.swift index deadf6e21..97293939a 100644 --- a/Sources/JavaScriptKit/JS Types/JSObject.swift +++ b/Sources/JavaScriptKit/JS Types/JSObject.swift @@ -29,6 +29,17 @@ public final class JSObject: JSType { self.init() elements.forEach { self[$0.key] = $0.value } } + + // MARK: - Accessors + + public subscript (key: String) -> JSValue { + get { jsObject.get(key) } + set { jsObject.set(key, newValue) } + } + + public var keys: Set { + return Set(jsObject.keys.function?.apply(this: jsObject).array.flatMap { $0.compactMap { $0.string } } ?? []) + } } internal extension JSObject { @@ -40,14 +51,6 @@ internal extension JSObject { } } -public extension JSObject { - - subscript (key: String) -> JSValue { - get { jsObject.get(key) } - set { jsObject.set(key, newValue) } - } -} - // MARK: - CustomStringConvertible extension JSObject: CustomStringConvertible { } From fdef8c24acdcd80ee26825c46c6722cd04f5e977 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 4 Jun 2020 21:45:59 -0500 Subject: [PATCH 29/38] Updated dependencies --- .gitignore | 3 +++ Package.swift | 8 +++++++- Package@swift-5.2.swift | 8 +++++++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index c1866f55e..79ca422f5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ node_modules /Packages /*.xcodeproj xcuserdata/ + +*.resolved +.swiftpm diff --git a/Package.swift b/Package.swift index 7330d4a6c..d447a1772 100644 --- a/Package.swift +++ b/Package.swift @@ -7,10 +7,16 @@ let package = Package( products: [ .library(name: "JavaScriptKit", targets: ["JavaScriptKit"]) ], + dependencies: [ + .package(url: "https://github.com/PureSwift/SwiftFoundation.git", .branch("feature/swift5")) + ], targets: [ .target( name: "JavaScriptKit", - dependencies: ["_CJavaScriptKit"], + dependencies: [ + "_CJavaScriptKit", + "SwiftFoundation" + ], linkerSettings: [ .unsafeFlags( [ diff --git a/Package@swift-5.2.swift b/Package@swift-5.2.swift index 2509ff9dd..8068372d8 100644 --- a/Package@swift-5.2.swift +++ b/Package@swift-5.2.swift @@ -7,10 +7,16 @@ let package = Package( products: [ .library(name: "JavaScriptKit", targets: ["JavaScriptKit"]) ], + dependencies: [ + .package(url: "https://github.com/PureSwift/SwiftFoundation.git", .branch("feature/swift5")) + ], targets: [ .target( name: "JavaScriptKit", - dependencies: ["_CJavaScriptKit"] + dependencies: [ + "_CJavaScriptKit", + "SwiftFoundation" + ] ), .target(name: "_CJavaScriptKit"), .testTarget( From a3093b9147c30144b50b712ed5014196b78edb00 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 4 Jun 2020 23:30:37 -0500 Subject: [PATCH 30/38] Added `JSDate` --- .../Sources/JavaScriptKitExample/main.swift | 3 + Sources/JavaScriptKit/Foundation/Date.swift | 105 ++++++++++++++++++ .../JavaScriptKit/JS Types/JSBoolean.swift | 2 +- .../JavaScriptKit/JS Types/JSConsole.swift | 3 +- Sources/JavaScriptKit/JS Types/JSDate.swift | 70 ++++++++++++ .../JavaScriptKit/JS Types/JSNavigator.swift | 2 +- 6 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 Sources/JavaScriptKit/Foundation/Date.swift create mode 100644 Sources/JavaScriptKit/JS Types/JSDate.swift diff --git a/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift b/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift index 38561ee17..29641e814 100644 --- a/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift +++ b/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift @@ -1,3 +1,4 @@ +import SwiftFoundation import JavaScriptKit let alert = JSObjectRef.global.alert.function! @@ -8,6 +9,8 @@ divElement.innerText = "Swift Bluetooth Web App" let body = document.body.object! _ = body.appendChild!(divElement) +JSConsole.log("Date:", Date()) + if let bluetooth = JSBluetooth.shared { bluetooth.isAvailable.then { JSConsole.assert($0, "Bluetooth not available") diff --git a/Sources/JavaScriptKit/Foundation/Date.swift b/Sources/JavaScriptKit/Foundation/Date.swift new file mode 100644 index 000000000..588c952cf --- /dev/null +++ b/Sources/JavaScriptKit/Foundation/Date.swift @@ -0,0 +1,105 @@ +// +// Date.swift +// JavaScriptKit +// +// Created by Alsey Coleman Miller on 6/4/20. +// + +import SwiftFoundation + +#if arch(wasm32) + +// MARK: - Time + +internal extension JSDate { + + /// The interval between 00:00:00 UTC on 1 January 2001 and the current date and time. + static var timeIntervalSinceReferenceDate: SwiftFoundation.TimeInterval { + return JSDate.now - Date.timeIntervalBetween1970AndReferenceDate + } +} + +public extension SwiftFoundation.Date { + + /// Returns a `Date` initialized to the current date and time. + init() { + self.init(timeIntervalSinceReferenceDate: JSDate.timeIntervalSinceReferenceDate) + } + + /// Returns a `Date` initialized relative to the current date and time by a given number of seconds. + init(timeIntervalSinceNow: SwiftFoundation.TimeInterval) { + self.init(timeIntervalSinceReferenceDate: timeIntervalSinceNow + JSDate.timeIntervalSinceReferenceDate) + } + + /** + The time interval between the date and the current date and time. + + If the date is earlier than the current date and time, the this property’s value is negative. + + - SeeAlso: `timeIntervalSince(_:)` + - SeeAlso: `timeIntervalSince1970` + - SeeAlso: `timeIntervalSinceReferenceDate` + */ + var timeIntervalSinceNow: SwiftFoundation.TimeInterval { + return self.timeIntervalSinceReferenceDate - JSDate.timeIntervalSinceReferenceDate + } + + /** + The interval between the date object and 00:00:00 UTC on 1 January 1970. + + This property’s value is negative if the date object is earlier than 00:00:00 UTC on 1 January 1970. + + - SeeAlso: `timeIntervalSince(_:)` + - SeeAlso: `timeIntervalSinceNow` + - SeeAlso: `timeIntervalSinceReferenceDate` + */ + var timeIntervalSince1970: SwiftFoundation.TimeInterval { + return self.timeIntervalSinceReferenceDate + Date.timeIntervalBetween1970AndReferenceDate + } +} + +public extension SwiftFoundation.Date { + + /// The interval between 00:00:00 UTC on 1 January 2001 and the current date and time. + static var _timeIntervalSinceReferenceDate: SwiftFoundation.TimeInterval { + // FIXME: Compiler error + return JSDate.timeIntervalSinceReferenceDate + } +} + +// MARK: - CustomStringConvertible + +extension SwiftFoundation.Date: CustomStringConvertible { + + public var description: String { + return JSDate(self).description + } +} + +// MARK: - CustomDebugStringConvertible + +extension SwiftFoundation.Date: CustomDebugStringConvertible { + + public var debugDescription: String { + return description + } +} + +#endif + +// MARK: - JS Value + +public extension JSDate { + + convenience init(_ date: SwiftFoundation.Date) { + self.init(timeInterval: date.timeIntervalSince1970) + } +} + +extension SwiftFoundation.Date: JSValueConvertible { + + public func jsValue() -> JSValue { + let date = JSDate() + return date.jsValue() + } +} diff --git a/Sources/JavaScriptKit/JS Types/JSBoolean.swift b/Sources/JavaScriptKit/JS Types/JSBoolean.swift index a9c992715..0d79a472b 100644 --- a/Sources/JavaScriptKit/JS Types/JSBoolean.swift +++ b/Sources/JavaScriptKit/JS Types/JSBoolean.swift @@ -14,7 +14,7 @@ public final class JSBoolean: JSType { // MARK: - Initialization - public init(_ jsObject: JSObjectRef) { + public init?(_ jsObject: JSObjectRef) { self.jsObject = jsObject } diff --git a/Sources/JavaScriptKit/JS Types/JSConsole.swift b/Sources/JavaScriptKit/JS Types/JSConsole.swift index 28a9839e6..2aecbc31a 100644 --- a/Sources/JavaScriptKit/JS Types/JSConsole.swift +++ b/Sources/JavaScriptKit/JS Types/JSConsole.swift @@ -5,6 +5,7 @@ // Created by Alsey Coleman Miller on 6/3/20. // +/// JavaScript Console public final class JSConsole: JSType { // MARK: - Properties @@ -13,7 +14,7 @@ public final class JSConsole: JSType { // MARK: - Initialization - public init(_ jsObject: JSObjectRef) { + public init?(_ jsObject: JSObjectRef) { self.jsObject = jsObject } diff --git a/Sources/JavaScriptKit/JS Types/JSDate.swift b/Sources/JavaScriptKit/JS Types/JSDate.swift new file mode 100644 index 000000000..f43c76575 --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSDate.swift @@ -0,0 +1,70 @@ +// +// JSDate.swift +// JavaScriptKit +// +// Created by Alsey Coleman Miller on 6/4/20. +// + +/** + JavaScript Date + + A JavaScript date is fundamentally specified as the number of milliseconds that have elapsed since midnight on January 1, 1970, UTC. This date and time is the same as the UNIX epoch, which is the predominant base value for computer-recorded date and time values. + */ +public final class JSDate: JSType { + + // MARK: - Properties + + public let jsObject: JSObjectRef + + // MARK: - Initialization + + public init?(_ jsObject: JSObjectRef) { + self.jsObject = jsObject + } + + /** + Creates a JavaScript Date instance that represents a single moment in time in a platform-independent format. Date objects contain a Number that represents milliseconds since 1 January 1970 UTC. + + When no parameters are provided, the newly-created Date object represents the current date and time as of the time of instantiation. + */ + public init() { + self.jsObject = Self.classObject.new() + } + + /** + Creates a JavaScript Date instance that represents a single moment in time in a platform-independent format. Date objects contain a Number that represents milliseconds since 1 January 1970 UTC. + + - Parameter timeInterval: An integer value representing the number of milliseconds since January 1, 1970, 00:00:00 UTC (the ECMAScript epoch, equivalent to the UNIX epoch), with leap seconds ignored. Keep in mind that most UNIX Timestamp functions are only accurate to the nearest second. + */ + public init(timeInterval: Double) { + self.jsObject = Self.classObject.new(timeInterval) + } + + /** + Creates a JavaScript Date instance that represents a single moment in time in a platform-independent format. Date objects contain a Number that represents milliseconds since 1 January 1970 UTC. + + - Parameter _string: A string value representing a date, specified in a format recognized by the Date.parse() method. (These formats are IETF-compliant RFC 2822 timestamps, and also strings in a version of ISO8601.) + */ + public init(_ dateString: String) { + self.jsObject = Self.classObject.new(dateString) + } + + /// The static `Date.now()` method returns the number of milliseconds elapsed since January 1, 1970 00:00:00 UTC. + /// + /// - Returns: A Number representing the milliseconds elapsed since the UNIX epoch. + public static var now: Double { + guard let function = classObject.now.function, + let timeInterval = function().number + else { fatalError() } + return timeInterval + } +} + +internal extension JSDate { + + static let classObject = JSObjectRef.global.Date.function! +} + +// MARK: - CustomStringConvertible + +extension JSDate: CustomStringConvertible { } diff --git a/Sources/JavaScriptKit/JS Types/JSNavigator.swift b/Sources/JavaScriptKit/JS Types/JSNavigator.swift index c30a282d0..34d4d612e 100644 --- a/Sources/JavaScriptKit/JS Types/JSNavigator.swift +++ b/Sources/JavaScriptKit/JS Types/JSNavigator.swift @@ -14,7 +14,7 @@ public final class JSNavigator: JSType { // MARK: - Initialization - public init(_ jsObject: JSObjectRef) { + public init?(_ jsObject: JSObjectRef) { self.jsObject = jsObject // TODO: validate JS class } From cfc85cee7987f01b3d0191f52e4fac5ec35e80ef Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 4 Jun 2020 23:52:09 -0500 Subject: [PATCH 31/38] Updated assertions --- .../{Utilities.swift => Assert.swift} | 14 +++++++++++++- Sources/JavaScriptKit/JS Types/JSBoolean.swift | 5 ++--- 2 files changed, 15 insertions(+), 4 deletions(-) rename Sources/JavaScriptKit/{Utilities.swift => Assert.swift} (75%) diff --git a/Sources/JavaScriptKit/Utilities.swift b/Sources/JavaScriptKit/Assert.swift similarity index 75% rename from Sources/JavaScriptKit/Utilities.swift rename to Sources/JavaScriptKit/Assert.swift index 3480b1b54..e9365ebeb 100644 --- a/Sources/JavaScriptKit/Utilities.swift +++ b/Sources/JavaScriptKit/Assert.swift @@ -1,5 +1,5 @@ // -// Utilities.swift +// Assert.swift // // // Created by Alsey Coleman Miller on 6/4/20. @@ -24,3 +24,15 @@ internal func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosu internal func assert(_ condition: @autoclosure () -> JSBoolean, _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) { assert(condition().rawValue, message(), file: file, line: line) } + +internal extension Optional { + + func assert(_ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) -> Wrapped { + switch self { + case .none: + fatalError(message(), file: file, line: line) + case let .some(value): + return value + } + } +} diff --git a/Sources/JavaScriptKit/JS Types/JSBoolean.swift b/Sources/JavaScriptKit/JS Types/JSBoolean.swift index 0d79a472b..4186ea8c1 100644 --- a/Sources/JavaScriptKit/JS Types/JSBoolean.swift +++ b/Sources/JavaScriptKit/JS Types/JSBoolean.swift @@ -32,9 +32,8 @@ extension JSBoolean: RawRepresentable { } public var rawValue: Bool { - guard let function = jsObject.valueOf.function - else { fatalError("Invalid function \(#function)") } - return function.apply(this: jsObject).boolean ?? false + let function = jsObject.valueOf.function.assert("Invalid function \(#function)") + return function.apply(this: jsObject).boolean.assert() } } From 9fbb5e75c2a94950f2779b44e221a95092825407 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 4 Jun 2020 23:57:18 -0500 Subject: [PATCH 32/38] Updated `JSDate` --- .../Sources/JavaScriptKitExample/main.swift | 4 +- Sources/JavaScriptKit/Foundation/Date.swift | 23 +++++++++- Sources/JavaScriptKit/JS Types/JSDate.swift | 42 +++++++++++++++++-- 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift b/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift index 29641e814..b24421a4d 100644 --- a/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift +++ b/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift @@ -9,7 +9,9 @@ divElement.innerText = "Swift Bluetooth Web App" let body = document.body.object! _ = body.appendChild!(divElement) -JSConsole.log("Date:", Date()) +let date = Date() +JSConsole.info("Date:", date) +JSConsole.log(date.description) if let bluetooth = JSBluetooth.shared { bluetooth.isAvailable.then { diff --git a/Sources/JavaScriptKit/Foundation/Date.swift b/Sources/JavaScriptKit/Foundation/Date.swift index 588c952cf..c91328bdd 100644 --- a/Sources/JavaScriptKit/Foundation/Date.swift +++ b/Sources/JavaScriptKit/Foundation/Date.swift @@ -72,7 +72,7 @@ public extension SwiftFoundation.Date { extension SwiftFoundation.Date: CustomStringConvertible { public var description: String { - return JSDate(self).description + return JSDate(self).toUTCString() } } @@ -96,6 +96,15 @@ public extension JSDate { } } +public extension SwiftFoundation.Date { + + init(_ date: JSDate) { + self.init(timeIntervalSince1970: date.rawValue) + } +} + +// MARK: - JSValueConvertible + extension SwiftFoundation.Date: JSValueConvertible { public func jsValue() -> JSValue { @@ -103,3 +112,15 @@ extension SwiftFoundation.Date: JSValueConvertible { return date.jsValue() } } + +// MARK: - JSValueConstructible + +extension SwiftFoundation.Date: JSValueConstructible { + + public static func construct(from value: JSValue) -> Date? { + return value.object + .flatMap { JSDate($0) } + .flatMap { Date($0) } + ?? value.number.flatMap { Date(timeIntervalSince1970: $0) } + } +} diff --git a/Sources/JavaScriptKit/JS Types/JSDate.swift b/Sources/JavaScriptKit/JS Types/JSDate.swift index f43c76575..8a7a56c0e 100644 --- a/Sources/JavaScriptKit/JS Types/JSDate.swift +++ b/Sources/JavaScriptKit/JS Types/JSDate.swift @@ -53,18 +53,52 @@ public final class JSDate: JSType { /// /// - Returns: A Number representing the milliseconds elapsed since the UNIX epoch. public static var now: Double { - guard let function = classObject.now.function, - let timeInterval = function().number - else { fatalError() } - return timeInterval + let function = classObject.now.function.assert() + return function().number.assert() + } + + // MARK: - Accessors + + /// Converts a date to a string following the ISO 8601 Extended Format. + public func toISOString() -> String { + let function = jsObject.toISOString.function.assert() + return function.apply(this: jsObject).string.assert() + } + + /// Returns a string representing the Date using `toISOString()`. Intended for use by JSON.stringify(). + public func toJSON() -> String { + let function = jsObject.toJSON.function.assert() + return function.apply(this: jsObject).string.assert() + } + + /// Converts a date to a string using the UTC timezone. + public func toUTCString() -> String { + let function = jsObject.toUTCString.function.assert() + return function.apply(this: jsObject).string.assert() } } +// MARK: - Constants + internal extension JSDate { static let classObject = JSObjectRef.global.Date.function! } +// MARK: - RawRepresentable + +extension JSDate: RawRepresentable { + + public convenience init(rawValue: Double) { + self.init(timeInterval: rawValue) + } + + public var rawValue: Double { + let function = jsObject.valueOf.function.assert("Invalid function \(#function)") + return function.apply(this: jsObject).number.assert() + } +} + // MARK: - CustomStringConvertible extension JSDate: CustomStringConvertible { } From 8f05e88fa427af5ca09abc395e825c47e4e842ad Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 5 Jun 2020 00:53:55 -0500 Subject: [PATCH 33/38] Fixed `JSEncoder` --- .../Sources/JavaScriptKitExample/main.swift | 3 + Sources/JavaScriptKit/JS Types/JSArray.swift | 8 +- Sources/JavaScriptKit/JSEncoder.swift | 259 +++++++++++------- .../JavaScriptKit/JSValueConvertible.swift | 12 + 4 files changed, 170 insertions(+), 112 deletions(-) diff --git a/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift b/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift index b24421a4d..a0e3f15f4 100644 --- a/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift +++ b/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift @@ -16,6 +16,9 @@ JSConsole.log(date.description) if let bluetooth = JSBluetooth.shared { bluetooth.isAvailable.then { JSConsole.assert($0, "Bluetooth not available") + }.catch { (error: JSError) in + JSConsole.debug(#file, #function, #line) + JSConsole.error(error) } let buttonElement = document.createElement!("button").object! buttonElement.innerText = "Scan for Bluetooth devices" diff --git a/Sources/JavaScriptKit/JS Types/JSArray.swift b/Sources/JavaScriptKit/JS Types/JSArray.swift index a0c7ea67a..123d7c025 100644 --- a/Sources/JavaScriptKit/JS Types/JSArray.swift +++ b/Sources/JavaScriptKit/JS Types/JSArray.swift @@ -18,11 +18,6 @@ public final class JSArray: JSType { assert(Self.isArray(jsObject)) } - public convenience init(_ collection: C) where C: Collection, C.Element == JSValue { - self.init(count: collection.count) - collection.enumerated().forEach { self[$0.offset] = $0.element } - } - public convenience init(_ collection: C) where C: Collection, C.Element == JSValueConvertible { self.init(collection.map({ $0.jsValue() })) } @@ -33,7 +28,8 @@ internal extension JSArray { static let classObject = JSObjectRef.global.Array.function! static func isArray(_ object: JSObjectRef) -> Bool { - classObject.isArray.function?(object).boolean ?? false + let function = classObject.isArray.function.assert() + return function(object).boolean.assert() } } diff --git a/Sources/JavaScriptKit/JSEncoder.swift b/Sources/JavaScriptKit/JSEncoder.swift index a5bb45ee8..8af2bf690 100644 --- a/Sources/JavaScriptKit/JSEncoder.swift +++ b/Sources/JavaScriptKit/JSEncoder.swift @@ -5,11 +5,7 @@ // Created by Alsey Coleman Miller on 6/4/20. // -#if arch(wasm32) import SwiftFoundation -#else -import Foundation -#endif /// JavaScript Encoder public struct JSEncoder { @@ -28,26 +24,21 @@ public struct JSEncoder { // MARK: - Methods - public func encode(_ encodable: T) throws -> JSValue { + public func encode(_ value: T) throws -> JSValue { log?("Will encode \(T.self)") - if let value = encodable as? JSValueConvertible { - return value.jsValue() - } else { - /* - let encoder = Encoder( - userInfo: userInfo, - log: log - ) - try encodable.encode(to: encoder) - return encoder.object.jsValue() - */ - fatalError() - } + let encoder = Encoder( + userInfo: userInfo, + log: log + ) + + try value.encode(to: encoder) + assert(encoder.stack.containers.count == 1) + return encoder.stack.root.jsValue } } -/* + // MARK: - Encoder internal extension JSEncoder { @@ -65,8 +56,7 @@ internal extension JSEncoder { /// Logger let log: ((String) -> ())? - /// Output - internal let object = JSObject() + private(set) var stack: Stack // MARK: - Initialization @@ -74,6 +64,7 @@ internal extension JSEncoder { userInfo: [CodingUserInfoKey : Any], log: ((String) -> ())?) { + self.stack = Stack() self.codingPath = codingPath self.userInfo = userInfo self.log = log @@ -84,24 +75,31 @@ internal extension JSEncoder { func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { log?("Requested container keyed by \(type.sanitizedName) for path \"\(codingPath.path)\"") - let keyedContainer = JSKeyedEncodingContainer(referencing: self) + let object = JSObject() + self.stack.push(object) + let keyedContainer = JSKeyedEncodingContainer(referencing: self, wrapping: object.jsObject) return KeyedEncodingContainer(keyedContainer) } func unkeyedContainer() -> UnkeyedEncodingContainer { log?("Requested unkeyed container for path \"\(codingPath.path)\"") - return JSUnkeyedEncodingContainer(referencing: self) + let array = JSArray() + self.stack.push(array) + return JSUnkeyedEncodingContainer(referencing: self, wrapping: array.jsObject) } func singleValueContainer() -> SingleValueEncodingContainer { log?("Requested single value container for path \"\(codingPath.path)\"") - return JSSingleValueEncodingContainer(referencing: self) + let container = Container(.undefined) + return JSSingleValueEncodingContainer(referencing: self, wrapping: container) } } } +// MARK: - Boxing Values + internal extension JSEncoder.Encoder { @inline(__always) @@ -112,18 +110,18 @@ internal extension JSEncoder.Encoder { func boxEncodable (_ value: T) throws -> JSValue { if let data = value as? Data { - return data - } else if let uuid = value as? UUID { - return boxUUID(uuid) + return boxData(data) } else if let date = value as? Date { return boxDate(date) + } else if let uuid = value as? UUID { + return boxUUID(uuid) } else if let tlvEncodable = value as? JSValueConvertible { return tlvEncodable.jsValue() } else { // encode using Encodable, should push new container. try value.encode(to: self) let nestedContainer = stack.pop() - return nestedContainer.data + return nestedContainer.jsValue } } } @@ -131,15 +129,18 @@ internal extension JSEncoder.Encoder { private extension JSEncoder.Encoder { func boxData(_ data: Data) -> JSValue { - return uuid.uuidString + // TODO: Support different Data formatting + //return JSArray(data).jsValue() + return data.base64EncodedString().jsValue() } func boxUUID(_ uuid: UUID) -> JSValue { - return uuid.uuidString + // TODO: Support different UUID formatting + return uuid.uuidString.jsValue() } - func boxDate(_ date: Date) -> Data { - + func boxDate(_ date: Date) -> JSValue { + /* switch options.dateFormatting { case .secondsSince1970: return boxDouble(date.timeIntervalSince1970) @@ -148,14 +149,64 @@ private extension JSEncoder.Encoder { case .iso8601: guard #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) else { fatalError("ISO8601DateFormatter is unavailable on this platform.") } - return boxDate(date, using: TLVDateFormatting.iso8601Formatter) + return boxDate(date, using: JSDateFormatting.iso8601Formatter) case let .formatted(formatter): return boxDate(date, using: formatter) - } + }*/ + return JSDate(date).jsValue() } - + /* func boxDate (_ date: Date, using formatter: T) -> Data { return box(formatter.string(from: date)) + }*/ +} + +// MARK: - Stack + +internal extension JSEncoder.Encoder { + + struct Stack { + + private(set) var containers = [Container]() + + fileprivate init() { } + + var top: Container { + guard let container = containers.last + else { fatalError("Empty container stack.") } + return container + } + + var root: Container { + guard let container = containers.first + else { fatalError("Empty container stack.") } + return container + } + + mutating func push(_ container: Container) { + containers.append(container) + } + + mutating func push(_ value: JSValueConvertible) { + containers.append(.init(value.jsValue())) + } + + @discardableResult + mutating func pop() -> Container { + guard let container = containers.popLast() + else { fatalError("Empty container stack.") } + return container + } + } +} + +internal extension JSEncoder.Encoder { + + final class Container { + fileprivate(set) var jsValue: JSValue + fileprivate init(_ jsValue: JSValue) { + self.jsValue = jsValue + } } } @@ -173,18 +224,25 @@ internal struct JSKeyedEncodingContainer : KeyedEncodingContaine /// The path of coding keys taken to get to this point in encoding. let codingPath: [CodingKey] + /// A reference to the container we're writing to. + let container: JSObjectRef + // MARK: - Initialization - init(referencing encoder: JSEncoder.Encoder) { + init(referencing encoder: JSEncoder.Encoder, + wrapping container: JSObjectRef) { self.encoder = encoder self.codingPath = encoder.codingPath + self.container = container } // MARK: - Methods func encodeNil(forKey key: K) throws { - // do nothing + self.encoder.codingPath.append(key) + defer { self.encoder.codingPath.removeLast() } + try setValue(.null, Any?.self, for: key) } func encode(_ value: Bool, forKey key: K) throws { @@ -240,18 +298,15 @@ internal struct JSKeyedEncodingContainer : KeyedEncodingContaine } func encode(_ value: String, forKey key: K) throws { - self.encoder.codingPath.append(key) - defer { self.encoder.codingPath.removeLast() } - let data = try encoder.boxString(value) - try setValue(value, data: data, for: key) + try encodeRaw(value, forKey: key) } func encode (_ value: T, forKey key: K) throws { self.encoder.codingPath.append(key) defer { self.encoder.codingPath.removeLast() } - encoder.log?("Will encode \(T.self) at path \"\(encoder.codingPath.path)\"") - try encoder.writeEncodable(value) + let encodedValue = try encoder.boxEncodable(value) + try setValue(encodedValue, T.self, for: key) } func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: K) -> KeyedEncodingContainer where NestedKey : CodingKey { @@ -276,12 +331,13 @@ internal struct JSKeyedEncodingContainer : KeyedEncodingContaine self.encoder.codingPath.append(key) defer { self.encoder.codingPath.removeLast() } - try setValue(value, value.jsValue(), for: key) + let encodedValue = encoder.box(value) + try setValue(encodedValue, T.self, for: key) } - private func setValue (_ value: T, _ encodedValue: JSValue, for key: Key) throws { - encoder.log?("Will encode \(T.self) at path \"\(encoder.codingPath.path)\"") - self.encoder.write(data) + private func setValue (_ encodedValue: JSValue, _ type: T.Type, for key: Key) throws { + encoder.log?("Will encode \(type) at path \"\(encoder.codingPath.path)\"") + container.set(key.stringValue, encodedValue) } } @@ -297,63 +353,66 @@ internal final class JSSingleValueEncodingContainer: SingleValueEncodingContaine /// The path of coding keys taken to get to this point in encoding. let codingPath: [CodingKey] + /// A reference to the container we're writing to. + let container: JSEncoder.Encoder.Container + /// Whether the data has been written private var didWrite = false // MARK: - Initialization - init(referencing encoder: JSEncoder.Encoder) { + init(referencing encoder: JSEncoder.Encoder, + wrapping container: JSEncoder.Encoder.Container) { self.encoder = encoder self.codingPath = encoder.codingPath + self.container = container } // MARK: - Methods func encodeNil() throws { - // do nothing + write(.null) } func encode(_ value: Bool) throws { write(encoder.box(value)) } - func encode(_ value: String) throws { try write(encoder.boxString(value)) } + func encode(_ value: String) throws { write(encoder.box(value)) } - func encode(_ value: Double) throws { write(encoder.boxDouble(value)) } + func encode(_ value: Double) throws { write(encoder.box(value)) } - func encode(_ value: Float) throws { write(encoder.boxFloat(value)) } + func encode(_ value: Float) throws { write(encoder.box(value)) } - func encode(_ value: Int) throws { write(encoder.boxNumeric(Int32(value))) } + func encode(_ value: Int) throws { write(encoder.box(value)) } func encode(_ value: Int8) throws { write(encoder.box(value)) } - func encode(_ value: Int16) throws { write(encoder.boxNumeric(value)) } + func encode(_ value: Int16) throws { write(encoder.box(value)) } - func encode(_ value: Int32) throws { write(encoder.boxNumeric(value)) } + func encode(_ value: Int32) throws { write(encoder.box(value)) } - func encode(_ value: Int64) throws { write(encoder.boxNumeric(value)) } + func encode(_ value: Int64) throws { write(encoder.box(value)) } - func encode(_ value: UInt) throws { write(encoder.boxNumeric(UInt32(value))) } + func encode(_ value: UInt) throws { write(encoder.box(value)) } func encode(_ value: UInt8) throws { write(encoder.box(value)) } - func encode(_ value: UInt16) throws { write(encoder.boxNumeric(value)) } + func encode(_ value: UInt16) throws { write(encoder.box(value)) } - func encode(_ value: UInt32) throws { write(encoder.boxNumeric(value)) } + func encode(_ value: UInt32) throws { write(encoder.box(value)) } - func encode(_ value: UInt64) throws { write(encoder.boxNumeric(value)) } + func encode(_ value: UInt64) throws { write(encoder.box(value)) } func encode (_ value: T) throws { - precondition(didWrite == false, "Data already written") - try encoder.writeEncodable(value) - self.didWrite = true + write(try encoder.boxEncodable(value)) } // MARK: - Private Methods - private func write(_ data: Data) { + private func write(_ value: JSValue) { precondition(didWrite == false, "Data already written") - self.encoder.write(data) + self.container.jsValue = value self.didWrite = true } } @@ -369,71 +428,59 @@ internal final class JSUnkeyedEncodingContainer: UnkeyedEncodingContainer { /// The path of coding keys taken to get to this point in encoding. let codingPath: [CodingKey] - - private var countOffset: Int? - - /// The number of elements encoded into the container. - private(set) var count: Int = 0 - // MARK: - Initialization + /// A reference to the container we're writing to. + let container: JSObjectRef - deinit { - // update count byte - self.countOffset.flatMap { self.encoder.data[$0] = UInt8(self.count) } - } + // MARK: - Initialization - init(referencing encoder: JSEncoder.Encoder) { + init(referencing encoder: JSEncoder.Encoder, + wrapping container: JSObjectRef) { + self.encoder = encoder self.codingPath = encoder.codingPath - let formatting = encoder.formatting.array - switch formatting { - case .lengthPrefix: - // write count byte - self.countOffset = self.encoder.data.count - self.encoder.write(Data([0])) - case .remainder: - self.countOffset = nil - } + self.container = container } // MARK: - Methods + /// The number of elements encoded into the container. + private(set) var count: Int = 0 + func encodeNil() throws { - throw EncodingError.invalidValue(Optional.self, EncodingError.Context(codingPath: self.codingPath, debugDescription: "Cannot encode nil in an array")) + append(.null) } func encode(_ value: Bool) throws { append(encoder.box(value)) } - func encode(_ value: String) throws { try append(encoder.boxString(value)) } + func encode(_ value: String) throws { append(encoder.box(value)) } - func encode(_ value: Double) throws { append(encoder.boxNumeric(value.bitPattern)) } + func encode(_ value: Double) throws { append(encoder.box(value)) } - func encode(_ value: Float) throws { append(encoder.boxNumeric(value.bitPattern)) } + func encode(_ value: Float) throws { append(encoder.box(value.bitPattern)) } - func encode(_ value: Int) throws { append(encoder.boxNumeric(Int32(value))) } + func encode(_ value: Int) throws { append(encoder.box(value)) } func encode(_ value: Int8) throws { append(encoder.box(value)) } - func encode(_ value: Int16) throws { append(encoder.boxNumeric(value)) } + func encode(_ value: Int16) throws { append(encoder.box(value)) } - func encode(_ value: Int32) throws { append(encoder.boxNumeric(value)) } + func encode(_ value: Int32) throws { append(encoder.box(value)) } - func encode(_ value: Int64) throws { append(encoder.boxNumeric(value)) } + func encode(_ value: Int64) throws { append(encoder.box(value)) } - func encode(_ value: UInt) throws { append(encoder.boxNumeric(UInt32(value))) } + func encode(_ value: UInt) throws { append(encoder.box(value)) } func encode(_ value: UInt8) throws { append(encoder.box(value)) } - func encode(_ value: UInt16) throws { append(encoder.boxNumeric(value)) } + func encode(_ value: UInt16) throws { append(encoder.box(value)) } - func encode(_ value: UInt32) throws { append(encoder.boxNumeric(value)) } + func encode(_ value: UInt32) throws { append(encoder.box(value)) } - func encode(_ value: UInt64) throws { append(encoder.boxNumeric(value)) } + func encode(_ value: UInt64) throws { append(encoder.box(value)) } func encode (_ value: T) throws { - assert(count < Int(UInt8.max), "Cannot encode more than \(UInt8.max) elements") - try encoder.writeEncodable(value) - count += 1 + append(try encoder.boxEncodable(value)) } func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer where NestedKey : CodingKey { @@ -450,11 +497,11 @@ internal final class JSUnkeyedEncodingContainer: UnkeyedEncodingContainer { // MARK: - Private Methods - private func append(_ data: Data) { - assert(count < Int(UInt8.max), "Cannot encode more than \(UInt8.max) elements") - // write element data - encoder.write(data) - count += 1 + private func append(_ value: JSValue) { + + // write + let index = self.count + defer { self.count += 1 } + self.container.set(index, value) } } -*/ diff --git a/Sources/JavaScriptKit/JSValueConvertible.swift b/Sources/JavaScriptKit/JSValueConvertible.swift index 654085a5b..81d139d95 100644 --- a/Sources/JavaScriptKit/JSValueConvertible.swift +++ b/Sources/JavaScriptKit/JSValueConvertible.swift @@ -28,6 +28,10 @@ extension Int32: JSValueConvertible { public func jsValue() -> JSValue { .number(Double(self)) } } +extension Int64: JSValueConvertible { + public func jsValue() -> JSValue { .number(Double(self)) } +} + extension UInt: JSValueConvertible { public func jsValue() -> JSValue { .number(Double(self)) } } @@ -40,6 +44,14 @@ extension UInt16: JSValueConvertible { public func jsValue() -> JSValue { .number(Double(self)) } } +extension UInt32: JSValueConvertible { + public func jsValue() -> JSValue { .number(Double(self)) } +} + +extension UInt64: JSValueConvertible { + public func jsValue() -> JSValue { .number(Double(self)) } +} + extension Float: JSValueConvertible { public func jsValue() -> JSValue { .number(Double(self)) } } From c976028d6e78460090c3e66e0f3c016a92066690 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 5 Jun 2020 01:09:50 -0500 Subject: [PATCH 34/38] Fixed `CodingKey.sanitizedName` --- Sources/JavaScriptKit/Extensions/CodingKey.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/JavaScriptKit/Extensions/CodingKey.swift b/Sources/JavaScriptKit/Extensions/CodingKey.swift index 71737854e..608eb081a 100644 --- a/Sources/JavaScriptKit/Extensions/CodingKey.swift +++ b/Sources/JavaScriptKit/Extensions/CodingKey.swift @@ -22,8 +22,7 @@ internal extension CodingKey { guard elements.count > 2 else { return rawName } elements.removeFirst() - // FIXME: Needs Foundation for String extensions - //elements.removeAll { $0.contains("(unknown context") } + elements.removeAll { $0.hasPrefix("(unknown context") } return elements.reduce("", { $0 + ($0.isEmpty ? "" : ".") + $1 }) } } From 6a1528fca0f2cab0f451194904e1f0f739234c24 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 5 Jun 2020 07:41:57 -0500 Subject: [PATCH 35/38] Fixed `JSArray` --- Sources/JavaScriptKit/JS Types/JSArray.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/JavaScriptKit/JS Types/JSArray.swift b/Sources/JavaScriptKit/JS Types/JSArray.swift index 123d7c025..81901f8ac 100644 --- a/Sources/JavaScriptKit/JS Types/JSArray.swift +++ b/Sources/JavaScriptKit/JS Types/JSArray.swift @@ -19,7 +19,8 @@ public final class JSArray: JSType { } public convenience init(_ collection: C) where C: Collection, C.Element == JSValueConvertible { - self.init(collection.map({ $0.jsValue() })) + self.init(count: collection.count) + collection.enumerated().forEach { self[$0.offset] = $0.element.jsValue() } } } From 699a1f0c1d926b046a51ca59a9d7d6fffa5a6444 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 5 Jun 2020 07:42:40 -0500 Subject: [PATCH 36/38] Fixed `JSEncoder` --- Sources/JavaScriptKit/JSEncoder.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Sources/JavaScriptKit/JSEncoder.swift b/Sources/JavaScriptKit/JSEncoder.swift index 8af2bf690..afe18522f 100644 --- a/Sources/JavaScriptKit/JSEncoder.swift +++ b/Sources/JavaScriptKit/JSEncoder.swift @@ -93,6 +93,8 @@ internal extension JSEncoder { log?("Requested single value container for path \"\(codingPath.path)\"") let container = Container(.undefined) + self.stack.push(container) + defer { assert(container.jsValue != .undefined, "Did not set value") } return JSSingleValueEncodingContainer(referencing: self, wrapping: container) } } @@ -115,8 +117,8 @@ internal extension JSEncoder.Encoder { return boxDate(date) } else if let uuid = value as? UUID { return boxUUID(uuid) - } else if let tlvEncodable = value as? JSValueConvertible { - return tlvEncodable.jsValue() + } else if let jsEncodable = value as? JSValueConvertible { + return jsEncodable.jsValue() } else { // encode using Encodable, should push new container. try value.encode(to: self) @@ -140,6 +142,7 @@ private extension JSEncoder.Encoder { } func boxDate(_ date: Date) -> JSValue { + // TODO: Support different Date formatting /* switch options.dateFormatting { case .secondsSince1970: @@ -457,7 +460,7 @@ internal final class JSUnkeyedEncodingContainer: UnkeyedEncodingContainer { func encode(_ value: Double) throws { append(encoder.box(value)) } - func encode(_ value: Float) throws { append(encoder.box(value.bitPattern)) } + func encode(_ value: Float) throws { append(encoder.box(value)) } func encode(_ value: Int) throws { append(encoder.box(value)) } From 830a969a0e2161d540970896b1cc815ce24923be Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 5 Jun 2020 08:01:24 -0500 Subject: [PATCH 37/38] Updated dependencies --- Package.swift | 2 +- Package@swift-5.2.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index d447a1772..a4f83f63a 100644 --- a/Package.swift +++ b/Package.swift @@ -8,7 +8,7 @@ let package = Package( .library(name: "JavaScriptKit", targets: ["JavaScriptKit"]) ], dependencies: [ - .package(url: "https://github.com/PureSwift/SwiftFoundation.git", .branch("feature/swift5")) + .package(url: "https://github.com/PureSwift/SwiftFoundation.git", .branch("develop")) ], targets: [ .target( diff --git a/Package@swift-5.2.swift b/Package@swift-5.2.swift index 8068372d8..af76783c2 100644 --- a/Package@swift-5.2.swift +++ b/Package@swift-5.2.swift @@ -8,7 +8,7 @@ let package = Package( .library(name: "JavaScriptKit", targets: ["JavaScriptKit"]) ], dependencies: [ - .package(url: "https://github.com/PureSwift/SwiftFoundation.git", .branch("feature/swift5")) + .package(url: "https://github.com/PureSwift/SwiftFoundation.git", .branch("develop")) ], targets: [ .target( From d56d684288915dfd27935e8616454b03fc4f5a3d Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Fri, 5 Jun 2020 08:33:57 -0500 Subject: [PATCH 38/38] Fixed `JSDate` --- Sources/JavaScriptKit/Foundation/Date.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/JavaScriptKit/Foundation/Date.swift b/Sources/JavaScriptKit/Foundation/Date.swift index c91328bdd..77c543522 100644 --- a/Sources/JavaScriptKit/Foundation/Date.swift +++ b/Sources/JavaScriptKit/Foundation/Date.swift @@ -108,7 +108,8 @@ public extension SwiftFoundation.Date { extension SwiftFoundation.Date: JSValueConvertible { public func jsValue() -> JSValue { - let date = JSDate() + let date = JSDate(self) + assert(date.rawValue == timeIntervalSince1970) return date.jsValue() } }