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/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift b/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift index 2ddec8ad4..a0e3f15f4 100644 --- a/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift +++ b/Example/JavaScriptKitExample/Sources/JavaScriptKitExample/main.swift @@ -1,17 +1,59 @@ +import SwiftFoundation import JavaScriptKit let alert = JSObjectRef.global.alert.function! let document = JSObjectRef.global.document.object! 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.onclick = .function { _ in - alert("Swift is running on browser!") -} +let date = Date() +JSConsole.info("Date:", date) +JSConsole.log(date.description) -_ = body.appendChild!(buttonElement) +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" + 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) +} diff --git a/Package.swift b/Package.swift index 7330d4a6c..a4f83f63a 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("develop")) + ], 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 new file mode 100644 index 000000000..af76783c2 --- /dev/null +++ b/Package@swift-5.2.swift @@ -0,0 +1,27 @@ +// swift-tools-version:5.2 + +import PackageDescription + +let package = Package( + name: "JavaScriptKit", + products: [ + .library(name: "JavaScriptKit", targets: ["JavaScriptKit"]) + ], + dependencies: [ + .package(url: "https://github.com/PureSwift/SwiftFoundation.git", .branch("develop")) + ], + targets: [ + .target( + name: "JavaScriptKit", + dependencies: [ + "_CJavaScriptKit", + "SwiftFoundation" + ] + ), + .target(name: "_CJavaScriptKit"), + .testTarget( + name: "JavaScriptKitTests", + dependencies: ["JavaScriptKit"] + ) + ] +) diff --git a/Sources/JavaScriptKit/Assert.swift b/Sources/JavaScriptKit/Assert.swift new file mode 100644 index 000000000..e9365ebeb --- /dev/null +++ b/Sources/JavaScriptKit/Assert.swift @@ -0,0 +1,38 @@ +// +// Assert.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) +} + +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/Extensions/CodingKey.swift b/Sources/JavaScriptKit/Extensions/CodingKey.swift new file mode 100644 index 000000000..608eb081a --- /dev/null +++ b/Sources/JavaScriptKit/Extensions/CodingKey.swift @@ -0,0 +1,28 @@ +// +// 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() + elements.removeAll { $0.hasPrefix("(unknown context") } + return elements.reduce("", { $0 + ($0.isEmpty ? "" : ".") + $1 }) + } +} diff --git a/Sources/JavaScriptKit/Foundation/Date.swift b/Sources/JavaScriptKit/Foundation/Date.swift new file mode 100644 index 000000000..77c543522 --- /dev/null +++ b/Sources/JavaScriptKit/Foundation/Date.swift @@ -0,0 +1,127 @@ +// +// 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).toUTCString() + } +} + +// 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) + } +} + +public extension SwiftFoundation.Date { + + init(_ date: JSDate) { + self.init(timeIntervalSince1970: date.rawValue) + } +} + +// MARK: - JSValueConvertible + +extension SwiftFoundation.Date: JSValueConvertible { + + public func jsValue() -> JSValue { + let date = JSDate(self) + assert(date.rawValue == timeIntervalSince1970) + 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/JSArray.swift b/Sources/JavaScriptKit/JS Types/JSArray.swift new file mode 100644 index 000000000..81901f8ac --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSArray.swift @@ -0,0 +1,98 @@ +/// 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 == JSValueConvertible { + self.init(count: collection.count) + collection.enumerated().forEach { self[$0.offset] = $0.element.jsValue() } + } +} + +internal extension JSArray { + + static let classObject = JSObjectRef.global.Array.function! + + static func isArray(_ object: JSObjectRef) -> Bool { + let function = classObject.isArray.function.assert() + return function(object).boolean.assert() + } +} + +// MARK: - Deprecated + +@available(*, renamed: "JSArray") +public typealias JSArrayRef = JSArray + +// MARK: - CustomStringConvertible + +extension JSArray: CustomStringConvertible { } + +// MARK: - ExpressibleByArrayLiteral + +extension JSArray: ExpressibleByArrayLiteral { + + public convenience init(arrayLiteral elements: JSValueConvertible...) { + self.init(elements) + } +} + +// 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..e2ec6365b --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSBluetooth.swift @@ -0,0 +1,112 @@ +// +// JSBluetooth.swift +// +// +// Created by Alsey Coleman Miller on 6/3/20. +// + +/// 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 + + public let jsObject: JSObjectRef + + // MARK: - Initialization + + public init?(_ jsObject: JSObjectRef) { + self.jsObject = jsObject + } + + public static var shared: JSBluetooth? { return JSNavigator.shared?.bluetooth } + + // 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)") } + let result = function.apply(this: jsObject) + guard let promise = result.object.flatMap({ JSPromise($0) }) + else { fatalError("Invalid object \(result)") } + 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() -> 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)") } + 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/JS Types/JSBluetoothDevice.swift b/Sources/JavaScriptKit/JS Types/JSBluetoothDevice.swift new file mode 100644 index 000000000..cacaec9f7 --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSBluetoothDevice.swift @@ -0,0 +1,46 @@ +// +// 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 + } + + // 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 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) })! +} + +// MARK: - CustomStringConvertible + +extension JSBluetoothDevice: CustomStringConvertible { + + public var description: String { + return "JSBluetoothDevice(id: \(id), name: \(name ?? "nil"))" + } +} + +// MARK: - Identifiable + +extension JSBluetoothDevice: Identifiable { } diff --git a/Sources/JavaScriptKit/JS Types/JSBluetoothRemoteGATTServer.swift b/Sources/JavaScriptKit/JS Types/JSBluetoothRemoteGATTServer.swift new file mode 100644 index 000000000..0dce69baf --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSBluetoothRemoteGATTServer.swift @@ -0,0 +1,58 @@ +// +// 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 + + /// 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)") } + let result = function.apply(this: jsObject) + guard let promise = result.object.flatMap({ JSPromise($0) }) + else { fatalError("Invalid object \(result)") } + return promise + } + + /// Causes the script execution environment to disconnect from this device. + public func disconnect() { + 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 + + +} + + diff --git a/Sources/JavaScriptKit/JS Types/JSBoolean.swift b/Sources/JavaScriptKit/JS Types/JSBoolean.swift new file mode 100644 index 000000000..4186ea8c1 --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSBoolean.swift @@ -0,0 +1,58 @@ +// +// 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()) + } +} + +// MARK: - RawRepresentable + +extension JSBoolean: RawRepresentable { + + public convenience init(rawValue: Bool) { + self.init(rawValue) + } + + public var rawValue: Bool { + let function = jsObject.valueOf.function.assert("Invalid function \(#function)") + return function.apply(this: jsObject).boolean.assert() + } +} + +// MARK: - Constants + +internal extension JSBoolean { + + static let classObject = JSObjectRef.global.Boolean.function! +} + +// MARK: - CustomStringConvertible + +extension JSBoolean: CustomStringConvertible { } + +// MARK: - ExpressibleByBooleanLiteral + +extension JSBoolean: ExpressibleByBooleanLiteral { + + public convenience init(booleanLiteral value: Bool) { + self.init(value) + } +} diff --git a/Sources/JavaScriptKit/JS Types/JSConsole.swift b/Sources/JavaScriptKit/JS Types/JSConsole.swift new file mode 100644 index 000000000..2aecbc31a --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSConsole.swift @@ -0,0 +1,95 @@ +// +// JSConsole.swift +// +// +// Created by Alsey Coleman Miller on 6/3/20. +// + +/// JavaScript Console +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(_ 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: 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: Any...) { + debugFunction.dynamicallyCall(withArguments: arguments.map(print)) + } + + /** + Outputs an error message to the Web Console. + */ + 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.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...) { + assertFunction.dynamicallyCall(withArguments: [condition()] + arguments.map(print)) + } +} + +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! +} + +private extension JSConsole { + + /// Console should print objects as their description and not + 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)" + } + } +} diff --git a/Sources/JavaScriptKit/JS Types/JSDate.swift b/Sources/JavaScriptKit/JS Types/JSDate.swift new file mode 100644 index 000000000..8a7a56c0e --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSDate.swift @@ -0,0 +1,104 @@ +// +// 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 { + 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 { } diff --git a/Sources/JavaScriptKit/JS Types/JSError.swift b/Sources/JavaScriptKit/JS Types/JSError.swift new file mode 100644 index 000000000..85703b5f7 --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSError.swift @@ -0,0 +1,45 @@ +// +// JSError +// +// +// 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 required 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 { } diff --git a/Sources/JavaScriptKit/JS Types/JSNavigator.swift b/Sources/JavaScriptKit/JS Types/JSNavigator.swift new file mode 100644 index 000000000..34d4d612e --- /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..97293939a --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSObject.swift @@ -0,0 +1,65 @@ +// +// 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 } + } + + // 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 { + + static let classObject = JSObjectRef.global.Object.function! + + static func isObject(_ object: JSObjectRef) -> Bool { + classObject.isObject.function?(object).boolean ?? false + } +} + +// MARK: - CustomStringConvertible + +extension JSObject: CustomStringConvertible { } + +// MARK: - ExpressibleByDictionaryLiteral + +extension JSObject: ExpressibleByDictionaryLiteral { + + public convenience init(dictionaryLiteral elements: (String, JSValue)...) { + self.init(elements) + } +} diff --git a/Sources/JavaScriptKit/JS Types/JSPromise.swift b/Sources/JavaScriptKit/JS Types/JSPromise.swift new file mode 100644 index 000000000..dbfceb002 --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSPromise.swift @@ -0,0 +1,295 @@ +// +// JSPromise.swift +// +// +// Created by Alsey Coleman Miller on 6/3/20. +// + +/** + 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 where Success: JSValueConvertible, Success: JSValueConstructible { + + // MARK: - Properties + + public let jsObject: JSObjectRef + + // MARK: - Initialization + + public init?(_ jsObject: JSObjectRef) { + self.jsObject = jsObject + // validate if promise type + } + + /** + 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) -> (), (JSError) -> ()) -> ()) { + let executor = JSClosure { (arguments) in + let resolutionFunc = arguments[0].function + let rejectionFunc = arguments[1].function + block({ resolutionFunc?($0.jsValue()) }, { rejectionFunc?($0.jsValue()) }) + return .null + } + 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: - Accessors + + /// Promise State + public var state: JSPromiseState { + 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 + + /** + 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. + */ + @discardableResult + internal func _then(onFulfilled: @escaping (Success) -> (JSValue), + onRejected: @escaping (JSError) -> ()) -> JSValue { + + guard let function = jsObject.then.function + else { fatalError("Invalid function \(#function)") } + + let success = JSClosure { (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 + } + } + + let errorFunction = JSClosure { (arguments) in + if let value = arguments.first.flatMap({ JSError.construct(from: $0) }) { + onRejected(value) + } else { + JSConsole.error("Unable to load error from ", arguments.first ?? "nil") + } + return .undefined + } + + 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 = JSClosure { (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. + + - 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 .null + }, 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 .null + }) + 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: @escaping (JSError) -> ()) { + guard let function = jsObject.catch.function + else { fatalError("Invalid function \(#function)") } + let errorFunction = JSClosure { (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() { + guard let function = jsObject.finally.function + else { fatalError("Invalid function \(#function)") } + function.apply(this: jsObject) + } +} + +private let JSPromiseClassObject = JSObjectRef.global.Promise.function! + +// MARK: - Supporting Types + +public extension JSPromise { + + 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 JSPromiseState: JSValueConstructible { + + public static func construct(from value: JSValue) -> JSPromiseState? { + return value.string.flatMap { JSPromiseState(rawValue: $0) } + } +} diff --git a/Sources/JavaScriptKit/JS Types/JSType.swift b/Sources/JavaScriptKit/JS Types/JSType.swift new file mode 100644 index 000000000..17f639a31 --- /dev/null +++ b/Sources/JavaScriptKit/JS Types/JSType.swift @@ -0,0 +1,39 @@ +// +// JSType.swift +// +// +// Created by Alsey Coleman Miller on 6/3/20. +// + +/// JavaScript Type (Class) +public protocol JSType: JSValueConvertible, JSValueConstructible { + + init?(_ jsObject: JSObjectRef) + + var jsObject: JSObjectRef { get } +} + +internal extension JSType { + + func toString() -> String? { + return jsObject.toString.function?.apply(this: jsObject).string + } +} + +public extension JSType { + + static func construct(from value: JSValue) -> Self? { + return value.object.flatMap { Self.init($0) } + } + + func jsValue() -> JSValue { + return .object(jsObject) + } +} + +public extension JSType where Self: CustomStringConvertible { + + var description: String { + return toString() ?? "" + } +} 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/JSValueDecoder.swift b/Sources/JavaScriptKit/JSDecoder.swift similarity index 94% rename from Sources/JavaScriptKit/JSValueDecoder.swift rename to Sources/JavaScriptKit/JSDecoder.swift index 0bf9a426b..900a15387 100644 --- a/Sources/JavaScriptKit/JSValueDecoder.swift +++ b/Sources/JavaScriptKit/JSDecoder.swift @@ -1,3 +1,26 @@ +/// JavaScript Decoder +public struct JSDecoder { + + // 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 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 +255,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) - } -} diff --git a/Sources/JavaScriptKit/JSEncoder.swift b/Sources/JavaScriptKit/JSEncoder.swift new file mode 100644 index 000000000..afe18522f --- /dev/null +++ b/Sources/JavaScriptKit/JSEncoder.swift @@ -0,0 +1,510 @@ +// +// JSEncoder.swift +// +// +// Created by Alsey Coleman Miller on 6/4/20. +// + +import SwiftFoundation + +/// 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(_ value: T) throws -> JSValue { + + log?("Will encode \(T.self)") + + 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 { + + 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) -> ())? + + private(set) var stack: Stack + + // MARK: - Initialization + + fileprivate init(codingPath: [CodingKey] = [], + userInfo: [CodingUserInfoKey : Any], + log: ((String) -> ())?) { + + self.stack = Stack() + 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 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)\"") + 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)\"") + let container = Container(.undefined) + self.stack.push(container) + defer { assert(container.jsValue != .undefined, "Did not set value") } + return JSSingleValueEncodingContainer(referencing: self, wrapping: container) + } + } +} + +// MARK: - Boxing Values + +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 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 jsEncodable = value as? JSValueConvertible { + return jsEncodable.jsValue() + } else { + // encode using Encodable, should push new container. + try value.encode(to: self) + let nestedContainer = stack.pop() + return nestedContainer.jsValue + } + } +} + +private extension JSEncoder.Encoder { + + func boxData(_ data: Data) -> JSValue { + // TODO: Support different Data formatting + //return JSArray(data).jsValue() + return data.base64EncodedString().jsValue() + } + + func boxUUID(_ uuid: UUID) -> JSValue { + // TODO: Support different UUID formatting + return uuid.uuidString.jsValue() + } + + func boxDate(_ date: Date) -> JSValue { + // TODO: Support different Date formatting + /* + 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: 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 + } + } +} + +// 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] + + /// A reference to the container we're writing to. + let container: JSObjectRef + + // MARK: - Initialization + + 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 { + 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 { + 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 { + try encodeRaw(value, forKey: key) + } + + func encode (_ value: T, forKey key: K) throws { + + self.encoder.codingPath.append(key) + defer { self.encoder.codingPath.removeLast() } + 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 { + 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() } + let encodedValue = encoder.box(value) + try setValue(encodedValue, T.self, for: key) + } + + 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) + } +} + +// 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] + + /// 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, + wrapping container: JSEncoder.Encoder.Container) { + + self.encoder = encoder + self.codingPath = encoder.codingPath + self.container = container + } + + // MARK: - Methods + + func encodeNil() throws { + write(.null) + } + + func encode(_ value: Bool) throws { write(encoder.box(value)) } + + func encode(_ value: String) throws { write(encoder.box(value)) } + + func encode(_ value: Double) throws { write(encoder.box(value)) } + + func encode(_ value: Float) throws { write(encoder.box(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.box(value)) } + + func encode(_ value: Int32) throws { write(encoder.box(value)) } + + func encode(_ value: Int64) throws { write(encoder.box(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.box(value)) } + + func encode(_ value: UInt32) throws { write(encoder.box(value)) } + + func encode(_ value: UInt64) throws { write(encoder.box(value)) } + + func encode (_ value: T) throws { + write(try encoder.boxEncodable(value)) + } + + // MARK: - Private Methods + + private func write(_ value: JSValue) { + + precondition(didWrite == false, "Data already written") + self.container.jsValue = value + 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] + + /// A reference to the container we're writing to. + let container: JSObjectRef + + // MARK: - Initialization + + init(referencing encoder: JSEncoder.Encoder, + wrapping container: JSObjectRef) { + + self.encoder = encoder + self.codingPath = encoder.codingPath + self.container = container + } + + // MARK: - Methods + + /// The number of elements encoded into the container. + private(set) var count: Int = 0 + + func encodeNil() throws { + append(.null) + } + + func encode(_ value: Bool) throws { append(encoder.box(value)) } + + func encode(_ value: String) throws { append(encoder.box(value)) } + + func encode(_ value: Double) throws { append(encoder.box(value)) } + + func encode(_ value: Float) throws { append(encoder.box(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.box(value)) } + + func encode(_ value: Int32) throws { append(encoder.box(value)) } + + func encode(_ value: Int64) throws { append(encoder.box(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.box(value)) } + + func encode(_ value: UInt32) throws { append(encoder.box(value)) } + + func encode(_ value: UInt64) throws { append(encoder.box(value)) } + + func encode (_ value: T) throws { + append(try encoder.boxEncodable(value)) + } + + 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(_ value: JSValue) { + + // write + let index = self.count + defer { self.count += 1 } + self.container.set(index, value) + } +} diff --git a/Sources/JavaScriptKit/JSFunction.swift b/Sources/JavaScriptKit/JSFunctionRef.swift similarity index 91% rename from Sources/JavaScriptKit/JSFunction.swift rename to Sources/JavaScriptKit/JSFunctionRef.swift index eecd7e13a..b5eb89832 100644 --- a/Sources/JavaScriptKit/JSFunction.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 @@ -53,11 +56,13 @@ public class JSFunctionRef: JSObjectRef { } } } - + @available(*, unavailable, message: "Please use JSClosure instead") public static func from(_ body: @escaping ([JSValue]) -> JSValue) -> JSFunctionRef { fatalError("unavailable") } + + // MARK: - JSValueConvertible public override func jsValue() -> JSValue { .function(self) @@ -88,20 +93,19 @@ public class JSClosure: JSFunctionRef { } } - @_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 709d6454d..9053c7bd2 100644 --- a/Sources/JavaScriptKit/JSObject.swift +++ b/Sources/JavaScriptKit/JSObjectRef.swift @@ -1,11 +1,23 @@ import _CJavaScriptKit @dynamicMemberLookup -public class JSObjectRef: Equatable { - internal var id: UInt32 - init(id: UInt32) { +public class JSObjectRef { + + public typealias ID = UInt32 + + // MARK: - Properties + + public internal(set) var 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 d528c3dc8..948412d39 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(JSClosure(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/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 4b6811d2f..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)) } } @@ -57,52 +69,48 @@ extension JSObjectRef: JSValueConvertible { // from `JSFunctionRef` } -private let Object = JSObjectRef.global.Object.function! - -extension Dictionary where Value: JSValueConvertible, Key == String { +extension Optional: JSValueConvertible where Wrapped: JSValueConvertible { + public func jsValue() -> JSValue { - Swift.Dictionary.jsValue(self)() + switch self { + case .none: + return .null + case let .some(value): + return value.jsValue() + } } } -extension Dictionary: JSValueConvertible where Value == JSValueConvertible, Key == String { +extension Dictionary 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) + Swift.Dictionary.jsValue(self)() } } -private let Array = JSObjectRef.global.Array.function! - -extension Array where Element: JSValueConvertible { +extension Dictionary: JSValueConvertible where Value == JSValueConvertible, Key == String { public func jsValue() -> JSValue { - Swift.Array.jsValue(self)() + let object = JSObject() + self.forEach { object[$0.key] = $0.value.jsValue() } + return .object(object.jsObject) } } -extension Array: JSValueConvertible 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 +118,14 @@ 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") } } } @@ -132,35 +138,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 } @@ -192,3 +198,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 + } + } +} 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;