vxrn 2.13.10 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/exports/prebuildWithoutExpo.mjs +15 -1
- package/dist/exports/prebuildWithoutExpo.mjs.map +1 -1
- package/dist/exports/prebuildWithoutExpo.native.js +15 -1
- package/dist/exports/prebuildWithoutExpo.native.js.map +1 -1
- package/dist/utils/nativeSourceContract.mjs +742 -97
- package/dist/utils/nativeSourceContract.mjs.map +1 -1
- package/dist/utils/nativeSourceContract.native.js +758 -70
- package/dist/utils/nativeSourceContract.native.js.map +1 -1
- package/expo-plugin.cjs +15 -2
- package/native-project-patches.cjs +42 -0
- package/package.json +12 -12
- package/src/exports/expoPlugin.test.mjs +52 -0
- package/src/exports/prebuildWithoutExpo.test.ts +18 -0
- package/src/exports/prebuildWithoutExpo.ts +18 -1
- package/src/utils/nativeSourceContract.test.ts +74 -9
- package/src/utils/nativeSourceContract.ts +810 -105
- package/types/exports/prebuildWithoutExpo.d.ts.map +1 -1
- package/types/utils/nativeSourceContract.d.ts.map +1 -1
|
@@ -1562,12 +1562,13 @@ function nativeSourceContract(file, source) {
|
|
|
1562
1562
|
}));
|
|
1563
1563
|
declarations.push(...views.map(function(view) {
|
|
1564
1564
|
return `export declare function ${view.name}(props: {${view.props.map(function(prop) {
|
|
1565
|
-
return ` ${prop.name}${prop.optional ? "?" : ""}: ${prop.type}
|
|
1566
|
-
}).join("
|
|
1565
|
+
return ` ${prop.name}${prop.optional ? "?" : ""}: ${prop.type};`;
|
|
1566
|
+
}).join("")} style?: StyleProp<ViewStyle> }): ReactElement`;
|
|
1567
1567
|
}));
|
|
1568
1568
|
if (defaultView) {
|
|
1569
1569
|
declarations.unshift("export default function SwiftPackage(props: Record<string, unknown>): ReactElement");
|
|
1570
1570
|
}
|
|
1571
|
+
if (views.length > 0) declarations.unshift("import type { StyleProp, ViewStyle } from 'react-native'");
|
|
1571
1572
|
if (defaultView || views.length > 0) declarations.unshift("import type { ReactElement } from 'react'");
|
|
1572
1573
|
var declaration = `${GENERATED_HEADER}${declarations.join("\n\n")}
|
|
1573
1574
|
`;
|
|
@@ -1939,18 +1940,151 @@ function renderSwiftSourceGlue(packageId, contracts) {
|
|
|
1939
1940
|
];
|
|
1940
1941
|
};
|
|
1941
1942
|
var renderViews = function renderViews2() {
|
|
1943
|
+
var notLinked2 = " default: throw NSError(domain: \"OneNativeSource\", code: 1, userInfo: [NSLocalizedDescriptionKey: \"swift view \\(name) is not linked; rebuild the app\"])";
|
|
1944
|
+
var fieldsOf = function fieldsOf2(view) {
|
|
1945
|
+
return view.props.filter(function(prop) {
|
|
1946
|
+
return !prop.callback || prop.optional;
|
|
1947
|
+
}).map(function(prop, index) {
|
|
1948
|
+
var type = prop.callback ? "Bool?" : prop.nativeType;
|
|
1949
|
+
var optional = type.endsWith("?");
|
|
1950
|
+
var list = /^\[(.*)\]\??$/.exec(type);
|
|
1951
|
+
return {
|
|
1952
|
+
name: prop.name,
|
|
1953
|
+
type,
|
|
1954
|
+
optional,
|
|
1955
|
+
list: list && !list[1].includes(":") ? list[1] : null,
|
|
1956
|
+
key: `p${index}`
|
|
1957
|
+
};
|
|
1958
|
+
});
|
|
1959
|
+
};
|
|
1942
1960
|
return [
|
|
1943
|
-
" private
|
|
1944
|
-
"
|
|
1961
|
+
" private struct OneListPatch<E: Decodable>: Decodable {",
|
|
1962
|
+
" let length: Int",
|
|
1963
|
+
" let set: [(Int, E)]",
|
|
1964
|
+
" private struct Entry: Decodable {",
|
|
1965
|
+
" let index: Int",
|
|
1966
|
+
" let value: E",
|
|
1967
|
+
" init(from decoder: Decoder) throws {",
|
|
1968
|
+
" var c = try decoder.unkeyedContainer()",
|
|
1969
|
+
" index = try c.decode(Int.self)",
|
|
1970
|
+
" value = try c.decode(E.self)",
|
|
1971
|
+
" }",
|
|
1972
|
+
" }",
|
|
1973
|
+
" private enum CodingKeys: String, CodingKey { case length, set }",
|
|
1974
|
+
" init(from decoder: Decoder) throws {",
|
|
1975
|
+
" let c = try decoder.container(keyedBy: CodingKeys.self)",
|
|
1976
|
+
" length = try c.decode(Int.self, forKey: .length)",
|
|
1977
|
+
" set = try c.decode([Entry].self, forKey: .set).map { ($0.index, $0.value) }",
|
|
1978
|
+
" }",
|
|
1945
1979
|
" }",
|
|
1946
|
-
" static func
|
|
1947
|
-
"
|
|
1948
|
-
"
|
|
1980
|
+
" private nonisolated static func patch<E>(_ list: inout [E], _ patch: OneListPatch<E>) -> Bool {",
|
|
1981
|
+
" if patch.length < 0 { return false }",
|
|
1982
|
+
" if list.count > patch.length { list.removeLast(list.count - patch.length) }",
|
|
1983
|
+
" for (index, value) in patch.set {",
|
|
1984
|
+
" if index < list.count { list[index] = value } else if index == list.count && index < patch.length { list.append(value) } else { return false }",
|
|
1949
1985
|
" }",
|
|
1986
|
+
" return list.count == patch.length",
|
|
1987
|
+
" }",
|
|
1988
|
+
" private nonisolated static func missing(_ prop: String) -> Error {",
|
|
1989
|
+
" NSError(domain: \"OneNativeSource\", code: 1, userInfo: [NSLocalizedDescriptionKey: \"missing prop \\(prop)\"])",
|
|
1990
|
+
" }",
|
|
1991
|
+
...views.flatMap(function(view) {
|
|
1992
|
+
var fields = fieldsOf(view);
|
|
1993
|
+
var lists = fields.filter(function(field) {
|
|
1994
|
+
return field.list;
|
|
1995
|
+
});
|
|
1996
|
+
return [
|
|
1997
|
+
...lists.length > 0 ? [
|
|
1998
|
+
` private struct Lists_${view.name}: Decodable {`,
|
|
1999
|
+
...lists.map(function(field) {
|
|
2000
|
+
return ` let ${field.key}: OneListPatch<${field.list}>?`;
|
|
2001
|
+
}),
|
|
2002
|
+
` private enum CodingKeys: String, CodingKey { ${lists.map(function(field) {
|
|
2003
|
+
return `case ${field.key} = ${JSON.stringify(field.name)}`;
|
|
2004
|
+
}).join("; ")} }`,
|
|
2005
|
+
" }"
|
|
2006
|
+
] : [],
|
|
2007
|
+
` private struct Doc_${view.name}: Decodable {`,
|
|
2008
|
+
" let seq: Int",
|
|
2009
|
+
" let base: Int?",
|
|
2010
|
+
...lists.length > 0 ? [` let lists: Lists_${view.name}?`] : [],
|
|
2011
|
+
...fields.map(function(field) {
|
|
2012
|
+
return ` let ${field.key}: Optional<${field.type}>`;
|
|
2013
|
+
}),
|
|
2014
|
+
` private enum CodingKeys: String, CodingKey { ${[
|
|
2015
|
+
"case seq = \"$seq\"",
|
|
2016
|
+
"case base = \"$base\"",
|
|
2017
|
+
"case lists = \"$lists\"",
|
|
2018
|
+
...fields.map(function(field) {
|
|
2019
|
+
return `case ${field.key} = ${JSON.stringify(field.name)}`;
|
|
2020
|
+
})
|
|
2021
|
+
].join("; ")} }`,
|
|
2022
|
+
" init(from decoder: Decoder) throws {",
|
|
2023
|
+
" let c = try decoder.container(keyedBy: CodingKeys.self)",
|
|
2024
|
+
" seq = try c.decodeIfPresent(Int.self, forKey: .seq) ?? 0",
|
|
2025
|
+
" base = try c.decodeIfPresent(Int.self, forKey: .base)",
|
|
2026
|
+
...lists.length > 0 ? [` lists = try c.decodeIfPresent(Lists_${view.name}.self, forKey: .lists)`] : [],
|
|
2027
|
+
...fields.map(function(field) {
|
|
2028
|
+
return ` ${field.key} = try c.contains(.${field.key}) ? Optional<${field.type}>.some(c.decode(${field.type}.self, forKey: .${field.key})) : Optional<${field.type}>.none`;
|
|
2029
|
+
}),
|
|
2030
|
+
" }",
|
|
2031
|
+
" }",
|
|
2032
|
+
` private final class Held_${view.name} {`,
|
|
2033
|
+
" var oneSeq: Int",
|
|
2034
|
+
...fields.map(function(field) {
|
|
2035
|
+
return ` var \`${field.name}\`: ${field.type}`;
|
|
2036
|
+
}),
|
|
2037
|
+
` init(_ d: Doc_${view.name}) throws {`,
|
|
2038
|
+
" oneSeq = d.seq",
|
|
2039
|
+
...fields.flatMap(function(field) {
|
|
2040
|
+
return field.optional ? [` \`${field.name}\` = d.${field.key}.flatMap { $0 }`] : [` guard let ${field.key} = d.${field.key} else { throw ${bodyName}.missing(${JSON.stringify(field.name)}) }`, ` \`${field.name}\` = ${field.key}`];
|
|
2041
|
+
}),
|
|
2042
|
+
" }",
|
|
2043
|
+
` func apply(_ d: Doc_${view.name}) -> Bool {`,
|
|
2044
|
+
" if d.base != oneSeq { return false }",
|
|
2045
|
+
...lists.flatMap(function(field) {
|
|
2046
|
+
return [
|
|
2047
|
+
` var ${field.key} = \`${field.name}\``,
|
|
2048
|
+
` if let v = d.${field.key} { ${field.key} = v }`,
|
|
2049
|
+
` if let p = d.lists?.${field.key} {`,
|
|
2050
|
+
...field.optional ? [
|
|
2051
|
+
` guard var items = ${field.key} else { return false }`,
|
|
2052
|
+
` if !${bodyName}.patch(&items, p) { return false }`,
|
|
2053
|
+
` ${field.key} = items`
|
|
2054
|
+
] : [` if !${bodyName}.patch(&${field.key}, p) { return false }`],
|
|
2055
|
+
" }"
|
|
2056
|
+
];
|
|
2057
|
+
}),
|
|
2058
|
+
...fields.map(function(field) {
|
|
2059
|
+
return field.list ? ` \`${field.name}\` = ${field.key}` : ` if let v = d.${field.key} { \`${field.name}\` = v }`;
|
|
2060
|
+
}),
|
|
2061
|
+
" oneSeq = d.seq",
|
|
2062
|
+
" return true",
|
|
2063
|
+
" }",
|
|
2064
|
+
" }"
|
|
2065
|
+
];
|
|
2066
|
+
}),
|
|
2067
|
+
" static func update(_ held: AnyObject?, _ name: String, _ text: String) throws -> AnyObject? {",
|
|
2068
|
+
" let data = Data(text.utf8)",
|
|
1950
2069
|
" switch name {",
|
|
1951
2070
|
...views.flatMap(function(view) {
|
|
1952
2071
|
return [
|
|
1953
2072
|
` case ${JSON.stringify(view.name)}:`,
|
|
2073
|
+
` let doc = try decode(Doc_${view.name}.self, from: data)`,
|
|
2074
|
+
` if doc.base == nil { return try Held_${view.name}(doc) }`,
|
|
2075
|
+
` guard let h = held as? Held_${view.name}, h.apply(doc) else { return nil }`,
|
|
2076
|
+
" return h"
|
|
2077
|
+
];
|
|
2078
|
+
}),
|
|
2079
|
+
notLinked2,
|
|
2080
|
+
" }",
|
|
2081
|
+
" }",
|
|
2082
|
+
" static func build(_ held: AnyObject, _ name: String, _ emit: @escaping (String, String) -> Void) throws -> AnyView {",
|
|
2083
|
+
" switch name {",
|
|
2084
|
+
...views.flatMap(function(view) {
|
|
2085
|
+
return [
|
|
2086
|
+
` case ${JSON.stringify(view.name)}:`,
|
|
2087
|
+
` guard ${fieldsOf(view).length > 0 ? "let props = held as?" : "held is"} Held_${view.name} else { throw NSError(domain: "OneNativeSource", code: 1, userInfo: [NSLocalizedDescriptionKey: "props of another view for \\(name)"]) }`,
|
|
1954
2088
|
` return AnyView(${view.name}(`,
|
|
1955
2089
|
...view.props.map(function(prop, index) {
|
|
1956
2090
|
var comma = index === view.props.length - 1 ? "" : ",";
|
|
@@ -1962,16 +2096,19 @@ function renderSwiftSourceGlue(packageId, contracts) {
|
|
|
1962
2096
|
var lambda = `{ (${params}) -> Void in emit(${key}, args([${prop.callback.map(function(_, n) {
|
|
1963
2097
|
return `a${n}`;
|
|
1964
2098
|
}).join(", ")}])) }`;
|
|
1965
|
-
return ` ${prop.name}: ${prop.optional ? `
|
|
2099
|
+
return ` ${prop.name}: ${prop.optional ? `props.\`${prop.name}\` == true ? ${lambda} : nil` : lambda}${comma}`;
|
|
1966
2100
|
}
|
|
1967
|
-
|
|
1968
|
-
return ` ${prop.name}: try decode(${input}, as: ${prop.nativeType}.self)${comma}`;
|
|
2101
|
+
return ` ${prop.name}: props.\`${prop.name}\`${comma}`;
|
|
1969
2102
|
}),
|
|
1970
2103
|
" ))"
|
|
1971
2104
|
];
|
|
1972
2105
|
}),
|
|
1973
|
-
|
|
2106
|
+
notLinked2,
|
|
1974
2107
|
" }",
|
|
2108
|
+
" }",
|
|
2109
|
+
" static func view(_ name: String, _ propsJson: String, _ emit: @escaping (String, String) -> Void) throws -> AnyView {",
|
|
2110
|
+
" guard let held = try update(nil, name, propsJson) else { throw NSError(domain: \"OneNativeSource\", code: 1, userInfo: [NSLocalizedDescriptionKey: \"\\(name) needs a full props document\"]) }",
|
|
2111
|
+
" return try build(held, name, emit)",
|
|
1975
2112
|
" }"
|
|
1976
2113
|
];
|
|
1977
2114
|
};
|
|
@@ -2042,13 +2179,15 @@ function renderSwiftSourceGlue(packageId, contracts) {
|
|
|
2042
2179
|
" if let date = (try? isoDateFractional.parse(text)) ?? (try? isoDate.parse(text)) { return date }",
|
|
2043
2180
|
" throw DecodingError.dataCorruptedError(in: container, debugDescription: \"expected ISO date\")",
|
|
2044
2181
|
" }",
|
|
2045
|
-
" private static func decode<T: Decodable>(_
|
|
2046
|
-
" let data = try JSONSerialization.data(withJSONObject: value, options: [.fragmentsAllowed])",
|
|
2182
|
+
" private static func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T {",
|
|
2047
2183
|
" let decoder = JSONDecoder()",
|
|
2048
2184
|
" decoder.dateDecodingStrategy = .custom { try decodeDate($0) }",
|
|
2049
2185
|
" decoder.dataDecodingStrategy = .base64",
|
|
2050
2186
|
" return try decoder.decode(T.self, from: data)",
|
|
2051
2187
|
" }",
|
|
2188
|
+
" private static func decode<T: Decodable>(_ value: Any, as type: T.Type) throws -> T {",
|
|
2189
|
+
" try decode(T.self, from: try JSONSerialization.data(withJSONObject: value, options: [.fragmentsAllowed]))",
|
|
2190
|
+
" }",
|
|
2052
2191
|
" private static func encode(_ value: any Encodable) throws -> String {",
|
|
2053
2192
|
" let encoder = JSONEncoder()",
|
|
2054
2193
|
" encoder.dateEncodingStrategy = .iso8601",
|
|
@@ -2116,6 +2255,8 @@ function renderSwiftSourceGlue(packageId, contracts) {
|
|
|
2116
2255
|
" public required override init() {}",
|
|
2117
2256
|
` public var contractHash: String { ${bodyName}.hash }`,
|
|
2118
2257
|
` public func view(_ name: String, _ propsJson: String, _ emit: @escaping (String, String) -> Void) throws -> AnyView { try ${bodyName}.view(name, propsJson, emit) }`,
|
|
2258
|
+
` public func updateView(_ held: AnyObject?, _ name: String, _ document: String) throws -> AnyObject? { try ${bodyName}.update(held, name, document) }`,
|
|
2259
|
+
` public func buildView(_ held: AnyObject, _ name: String, _ emit: @escaping (String, String) -> Void) throws -> AnyView { try ${bodyName}.build(held, name, emit) }`,
|
|
2119
2260
|
"}"
|
|
2120
2261
|
] : [],
|
|
2121
2262
|
"#endif",
|
|
@@ -2126,6 +2267,14 @@ function renderSwiftSourceGlue(packageId, contracts) {
|
|
|
2126
2267
|
source
|
|
2127
2268
|
};
|
|
2128
2269
|
}
|
|
2270
|
+
function unboxedZero(type) {
|
|
2271
|
+
if (type === "Int") return "0";
|
|
2272
|
+
if (type === "Long") return "0L";
|
|
2273
|
+
if (type === "Double") return "0.0";
|
|
2274
|
+
if (type === "Float") return "0f";
|
|
2275
|
+
if (type === "Boolean") return "false";
|
|
2276
|
+
return null;
|
|
2277
|
+
}
|
|
2129
2278
|
function renderKotlinSourceGlue(sourceId, contract) {
|
|
2130
2279
|
if (contract.language !== "kotlin") throw new Error("kotlin glue received a swift contract");
|
|
2131
2280
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(sourceId)) throw new Error(`invalid kotlin source id ${sourceId}`);
|
|
@@ -2133,6 +2282,8 @@ function renderKotlinSourceGlue(sourceId, contract) {
|
|
|
2133
2282
|
sourceId,
|
|
2134
2283
|
contract: contract.hash
|
|
2135
2284
|
})).digest("hex");
|
|
2285
|
+
var readerClass = `OneJsonReader_${sourceId}`;
|
|
2286
|
+
var tokenClass = `OneJsonToken_${sourceId}`;
|
|
2136
2287
|
var structsByName = new Map(contract.structs.map(function(entry) {
|
|
2137
2288
|
return [entry.name, entry];
|
|
2138
2289
|
}));
|
|
@@ -2145,6 +2296,23 @@ function renderKotlinSourceGlue(sourceId, contract) {
|
|
|
2145
2296
|
var isDateType = function isDateType2(type) {
|
|
2146
2297
|
return type === "Date" || type === "java.util.Date";
|
|
2147
2298
|
};
|
|
2299
|
+
var qualifiedType = function qualifiedType1(type) {
|
|
2300
|
+
if (type.endsWith("?")) return `${qualifiedType(type.slice(0, -1))}?`;
|
|
2301
|
+
if (type.startsWith("List<") && type.endsWith(">")) return `List<${qualifiedType(type.slice(5, -1))}>`;
|
|
2302
|
+
if (type.startsWith("Map<String,") && type.endsWith(">")) return `Map<String, ${qualifiedType(type.slice(11, -1))}>`;
|
|
2303
|
+
if (structsByName.has(type) || enumsByName.has(type)) return qualified(type);
|
|
2304
|
+
if (isDateType(type)) return "java.util.Date";
|
|
2305
|
+
if ([
|
|
2306
|
+
"String",
|
|
2307
|
+
"Boolean",
|
|
2308
|
+
"Double",
|
|
2309
|
+
"Float",
|
|
2310
|
+
"Int",
|
|
2311
|
+
"Long",
|
|
2312
|
+
"ByteArray"
|
|
2313
|
+
].includes(type)) return type;
|
|
2314
|
+
throw new Error(`unsupported kotlin glue type ${type}`);
|
|
2315
|
+
};
|
|
2148
2316
|
var mentions = function mentions1(type, match) {
|
|
2149
2317
|
var rest = type;
|
|
2150
2318
|
while (rest.endsWith("?")) rest = rest.slice(0, -1);
|
|
@@ -2196,25 +2364,25 @@ function renderKotlinSourceGlue(sourceId, contract) {
|
|
|
2196
2364
|
return leaf === "ByteArray";
|
|
2197
2365
|
});
|
|
2198
2366
|
});
|
|
2199
|
-
var value = function value1(type,
|
|
2367
|
+
var value = function value1(type, reader, path) {
|
|
2200
2368
|
if (type.endsWith("?")) {
|
|
2201
|
-
return `(${
|
|
2202
|
-
}
|
|
2203
|
-
if (type === "String") return `readString(${
|
|
2204
|
-
if (type === "Boolean") return `readBoolean(${
|
|
2205
|
-
if (type === "Double") return `readDouble(${
|
|
2206
|
-
if (type === "Float") return `readDouble(${
|
|
2207
|
-
if (type === "Int") return `readInt(${
|
|
2208
|
-
if (type === "Long") return `readLong(${
|
|
2209
|
-
if (isDateType(type)) return `readDate(${
|
|
2210
|
-
if (type === "ByteArray") return `readData(${
|
|
2211
|
-
if (structsByName.has(type)) return `read${type}(${
|
|
2212
|
-
if (enumsByName.has(type)) return `read${type}(${
|
|
2369
|
+
return `(if (${reader}.peek() == ${tokenClass}.NULL) { ${reader}.nextNull(); null } else ${value(type.slice(0, -1), reader, path)})`;
|
|
2370
|
+
}
|
|
2371
|
+
if (type === "String") return `readString(${reader}, ${path})`;
|
|
2372
|
+
if (type === "Boolean") return `readBoolean(${reader}, ${path})`;
|
|
2373
|
+
if (type === "Double") return `readDouble(${reader}, ${path})`;
|
|
2374
|
+
if (type === "Float") return `readDouble(${reader}, ${path}).toFloat()`;
|
|
2375
|
+
if (type === "Int") return `readInt(${reader}, ${path})`;
|
|
2376
|
+
if (type === "Long") return `readLong(${reader}, ${path})`;
|
|
2377
|
+
if (isDateType(type)) return `readDate(${reader}, ${path})`;
|
|
2378
|
+
if (type === "ByteArray") return `readData(${reader}, ${path})`;
|
|
2379
|
+
if (structsByName.has(type)) return `read${type}(${reader}, ${path})`;
|
|
2380
|
+
if (enumsByName.has(type)) return `read${type}(${reader}, ${path})`;
|
|
2213
2381
|
if (type.startsWith("List<") && type.endsWith(">")) {
|
|
2214
|
-
return `readList(${
|
|
2382
|
+
return `readList(${reader}, ${path}) { itemReader, itemPath -> ${value(type.slice(5, -1), "itemReader", "itemPath")} }`;
|
|
2215
2383
|
}
|
|
2216
2384
|
if (type.startsWith("Map<String,") && type.endsWith(">")) {
|
|
2217
|
-
return `readMap(${
|
|
2385
|
+
return `readMap(${reader}, ${path}) { itemReader, itemPath -> ${value(type.slice(11, -1), "itemReader", "itemPath")} }`;
|
|
2218
2386
|
}
|
|
2219
2387
|
throw new Error(`unsupported kotlin glue type ${type}`);
|
|
2220
2388
|
};
|
|
@@ -2234,17 +2402,45 @@ function renderKotlinSourceGlue(sourceId, contract) {
|
|
|
2234
2402
|
}
|
|
2235
2403
|
return `JSONObject.wrap(${accessor})`;
|
|
2236
2404
|
};
|
|
2405
|
+
var cap = function cap2(name) {
|
|
2406
|
+
return name.slice(0, 1).toUpperCase() + name.slice(1);
|
|
2407
|
+
};
|
|
2237
2408
|
var structHelpers = contract.structs.flatMap(function(struct) {
|
|
2238
2409
|
return [
|
|
2239
|
-
`private fun read${struct.name}(
|
|
2240
|
-
|
|
2410
|
+
`private fun read${struct.name}(reader: ${readerClass}, path: String): ${qualified(struct.name)} {`,
|
|
2411
|
+
` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("$path: expected object")`,
|
|
2412
|
+
" reader.beginObject()",
|
|
2413
|
+
...struct.fields.flatMap(function(field) {
|
|
2414
|
+
var declared = qualifiedType(field.nativeType);
|
|
2415
|
+
if (field.optional) return [` var ${field.name}: ${declared} = ${field.fallback}`];
|
|
2416
|
+
var zero = unboxedZero(field.nativeType);
|
|
2417
|
+
if (zero !== null) return [` var ${field.name} = ${zero}`, ` var has${cap(field.name)} = false`];
|
|
2418
|
+
var boxed = declared.endsWith("?") ? declared : `${declared}?`;
|
|
2419
|
+
return [` var ${field.name}: ${boxed} = null`, ...field.nativeType.endsWith("?") ? [` var has${cap(field.name)} = false`] : []];
|
|
2420
|
+
}),
|
|
2421
|
+
" while (reader.hasNext()) {",
|
|
2422
|
+
` when (reader.selectName(${struct.fields.map(function(field) {
|
|
2423
|
+
return JSON.stringify(field.name);
|
|
2424
|
+
}).join(", ")})) {`,
|
|
2425
|
+
...struct.fields.map(function(field, index) {
|
|
2426
|
+
var fieldPath = `"$path.${field.name}"`;
|
|
2427
|
+
var read = `${field.name} = ${value(field.nativeType, "reader", fieldPath)}`;
|
|
2428
|
+
var needsFlag = !field.optional && (field.nativeType.endsWith("?") || unboxedZero(field.nativeType) !== null);
|
|
2429
|
+
return needsFlag ? ` ${index} -> { ${read}; has${cap(field.name)} = true }` : ` ${index} -> ${read}`;
|
|
2430
|
+
}),
|
|
2431
|
+
" else -> reader.skipValue()",
|
|
2432
|
+
" }",
|
|
2433
|
+
" }",
|
|
2434
|
+
" reader.endObject()",
|
|
2435
|
+
...struct.fields.flatMap(function(field) {
|
|
2436
|
+
return !field.optional && unboxedZero(field.nativeType) !== null ? [` if (!has${cap(field.name)}) error("$path.${field.name}: required")`] : [];
|
|
2437
|
+
}),
|
|
2241
2438
|
` return ${qualified(struct.name)}(`,
|
|
2242
2439
|
...struct.fields.map(function(field) {
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
return ` ${field.name} = ${value(field.nativeType, input, fieldPath)},`;
|
|
2440
|
+
if (field.optional) return ` ${field.name} = ${field.name},`;
|
|
2441
|
+
if (field.nativeType.endsWith("?")) return ` ${field.name} = (if (has${cap(field.name)}) ${field.name} else error("$path.${field.name}: required")),`;
|
|
2442
|
+
if (unboxedZero(field.nativeType) !== null) return ` ${field.name} = ${field.name},`;
|
|
2443
|
+
return ` ${field.name} = (${field.name} ?: error("$path.${field.name}: required")),`;
|
|
2248
2444
|
}),
|
|
2249
2445
|
" )",
|
|
2250
2446
|
"}",
|
|
@@ -2258,8 +2454,8 @@ function renderKotlinSourceGlue(sourceId, contract) {
|
|
|
2258
2454
|
});
|
|
2259
2455
|
var enumHelpers = contract.enums.flatMap(function(entry) {
|
|
2260
2456
|
return [
|
|
2261
|
-
`private fun read${entry.name}(
|
|
2262
|
-
" val text = readString(
|
|
2457
|
+
`private fun read${entry.name}(reader: ${readerClass}, path: String): ${qualified(entry.name)} {`,
|
|
2458
|
+
" val text = readString(reader, path)",
|
|
2263
2459
|
` return try { ${qualified(entry.name)}.valueOf(text) } catch (e: IllegalArgumentException) { error("$path: expected one of ${entry.cases.map(function(item) {
|
|
2264
2460
|
return item.name;
|
|
2265
2461
|
}).join(", ")}") }`,
|
|
@@ -2268,40 +2464,322 @@ function renderKotlinSourceGlue(sourceId, contract) {
|
|
|
2268
2464
|
});
|
|
2269
2465
|
var source = [
|
|
2270
2466
|
"import androidx.annotation.Keep",
|
|
2271
|
-
...contract.views.length === 0 ? [] : [
|
|
2467
|
+
...contract.views.length === 0 ? [] : [
|
|
2468
|
+
"import androidx.compose.runtime.Composable",
|
|
2469
|
+
"import dev.onejs.one.OneNativeSourceKeyedState",
|
|
2470
|
+
"import dev.onejs.one.OneNativeSourceViewDispatch"
|
|
2471
|
+
],
|
|
2272
2472
|
"import dev.onejs.one.OneNativeSourceDispatch",
|
|
2273
2473
|
...needsEvents ? ["import dev.onejs.one.source.OneNativeSourceEvents"] : [],
|
|
2274
2474
|
"import org.json.JSONArray",
|
|
2275
2475
|
"import org.json.JSONObject",
|
|
2276
2476
|
"",
|
|
2277
|
-
|
|
2278
|
-
"
|
|
2279
|
-
"
|
|
2280
|
-
"
|
|
2477
|
+
`private enum class ${tokenClass} { BEGIN_OBJECT, END_OBJECT, BEGIN_ARRAY, END_ARRAY, STRING, NUMBER, BOOLEAN, NULL }`,
|
|
2478
|
+
"",
|
|
2479
|
+
"// a forward-only json reader over the props text: values decode straight",
|
|
2480
|
+
"// into typed props in one pass with no intermediate JSONObject. pure",
|
|
2481
|
+
"// kotlin with no java or android imports, so the same glue compiles for",
|
|
2482
|
+
"// kotlin/wasm.",
|
|
2483
|
+
`private class ${readerClass}(private val text: String) {`,
|
|
2484
|
+
" private var pos = 0",
|
|
2485
|
+
"",
|
|
2486
|
+
" private fun skipSpace() {",
|
|
2487
|
+
" while (pos < text.length) {",
|
|
2488
|
+
" when (text[pos]) {",
|
|
2489
|
+
" ' ', '\\t', '\\n', '\\r' -> pos++",
|
|
2490
|
+
" else -> return",
|
|
2491
|
+
" }",
|
|
2492
|
+
" }",
|
|
2493
|
+
" }",
|
|
2494
|
+
"",
|
|
2495
|
+
" private fun fail(message: String): Nothing = throw IllegalStateException(\"$message near offset $pos\")",
|
|
2496
|
+
"",
|
|
2497
|
+
` fun peek(): ${tokenClass} {`,
|
|
2498
|
+
" skipSpace()",
|
|
2499
|
+
" if (pos >= text.length) fail(\"unexpected end of json\")",
|
|
2500
|
+
" return when (text[pos]) {",
|
|
2501
|
+
` '{' -> ${tokenClass}.BEGIN_OBJECT`,
|
|
2502
|
+
` '}' -> ${tokenClass}.END_OBJECT`,
|
|
2503
|
+
` '[' -> ${tokenClass}.BEGIN_ARRAY`,
|
|
2504
|
+
` ']' -> ${tokenClass}.END_ARRAY`,
|
|
2505
|
+
` '"' -> ${tokenClass}.STRING`,
|
|
2506
|
+
` 't', 'f' -> ${tokenClass}.BOOLEAN`,
|
|
2507
|
+
` 'n' -> ${tokenClass}.NULL`,
|
|
2508
|
+
` '-', in '0'..'9' -> ${tokenClass}.NUMBER`,
|
|
2509
|
+
" else -> fail(\"unexpected character\")",
|
|
2510
|
+
" }",
|
|
2511
|
+
" }",
|
|
2512
|
+
"",
|
|
2513
|
+
" private fun expect(char: Char) {",
|
|
2514
|
+
" skipSpace()",
|
|
2515
|
+
` if (pos >= text.length || text[pos] != char) fail("expected '$char'")`,
|
|
2516
|
+
" pos++",
|
|
2517
|
+
" }",
|
|
2518
|
+
"",
|
|
2519
|
+
" fun beginObject() = expect('{')",
|
|
2520
|
+
" fun endObject() = expect('}')",
|
|
2521
|
+
" fun beginArray() = expect('[')",
|
|
2522
|
+
" fun endArray() = expect(']')",
|
|
2523
|
+
"",
|
|
2524
|
+
" // inside an object or array: true when another value follows, consuming",
|
|
2525
|
+
" // a comma first; false at the closing bracket.",
|
|
2526
|
+
" fun hasNext(): Boolean {",
|
|
2527
|
+
" skipSpace()",
|
|
2528
|
+
" if (pos >= text.length) fail(\"unexpected end of json\")",
|
|
2529
|
+
" if (text[pos] == ',') {",
|
|
2530
|
+
" pos++",
|
|
2531
|
+
" return true",
|
|
2532
|
+
" }",
|
|
2533
|
+
" return text[pos] != '}' && text[pos] != ']'",
|
|
2534
|
+
" }",
|
|
2535
|
+
"",
|
|
2536
|
+
" fun nextName(): String {",
|
|
2537
|
+
" skipSpace()",
|
|
2538
|
+
` if (pos >= text.length || text[pos] != '"') fail("expected a name")`,
|
|
2539
|
+
" val name = readString()",
|
|
2540
|
+
" skipSpace()",
|
|
2541
|
+
` if (pos >= text.length || text[pos] != ':') fail("expected ':'")`,
|
|
2542
|
+
" pos++",
|
|
2543
|
+
" return name",
|
|
2544
|
+
" }",
|
|
2545
|
+
"",
|
|
2546
|
+
" fun nextString(): String {",
|
|
2547
|
+
" skipSpace()",
|
|
2548
|
+
` if (pos >= text.length || text[pos] != '"') fail("expected a string")`,
|
|
2549
|
+
" return readString()",
|
|
2550
|
+
" }",
|
|
2551
|
+
"",
|
|
2552
|
+
" // matches an object name against the expected keys without allocating",
|
|
2553
|
+
" // it: the index of the match, or -1 for an unknown key. the name and",
|
|
2554
|
+
" // colon are consumed either way, so the caller skips the value on -1.",
|
|
2555
|
+
" fun selectName(vararg options: String): Int {",
|
|
2556
|
+
" skipSpace()",
|
|
2557
|
+
` if (pos >= text.length || text[pos] != '"') fail("expected a name")`,
|
|
2558
|
+
" var end = pos + 1",
|
|
2559
|
+
` while (end < text.length && text[end] != '"' && text[end] != '\\\\') end++`,
|
|
2560
|
+
" if (end < text.length && text[end] == '\\\\') {",
|
|
2561
|
+
" val decoded = readString()",
|
|
2562
|
+
" skipSpace()",
|
|
2563
|
+
` if (pos >= text.length || text[pos] != ':') fail("expected ':'")`,
|
|
2564
|
+
" pos++",
|
|
2565
|
+
" return options.indexOf(decoded)",
|
|
2566
|
+
" }",
|
|
2567
|
+
" if (end >= text.length) fail(\"unterminated string\")",
|
|
2568
|
+
" val length = end - pos - 1",
|
|
2569
|
+
" var index = -1",
|
|
2570
|
+
" for (i in options.indices) {",
|
|
2571
|
+
" if (options[i].length == length && text.regionMatches(pos + 1, options[i], 0, length)) { index = i; break }",
|
|
2572
|
+
" }",
|
|
2573
|
+
" pos = end + 1",
|
|
2574
|
+
" skipSpace()",
|
|
2575
|
+
` if (pos >= text.length || text[pos] != ':') fail("expected ':'")`,
|
|
2576
|
+
" pos++",
|
|
2577
|
+
" return index",
|
|
2578
|
+
" }",
|
|
2579
|
+
"",
|
|
2580
|
+
" private fun readString(): String {",
|
|
2581
|
+
" pos++ // the opening quote",
|
|
2582
|
+
" var end = pos",
|
|
2583
|
+
` while (end < text.length && text[end] != '"' && text[end] != '\\\\') end++`,
|
|
2584
|
+
` if (end < text.length && text[end] == '"') {`,
|
|
2585
|
+
" val out = text.substring(pos, end)",
|
|
2586
|
+
" pos = end + 1",
|
|
2587
|
+
" return out",
|
|
2588
|
+
" }",
|
|
2589
|
+
" val out = StringBuilder(text.substring(pos, end))",
|
|
2590
|
+
" pos = end",
|
|
2591
|
+
" while (true) {",
|
|
2592
|
+
" if (pos >= text.length) fail(\"unterminated string\")",
|
|
2593
|
+
" val char = text[pos++]",
|
|
2594
|
+
` if (char == '"') return out.toString()`,
|
|
2595
|
+
" if (char != '\\\\') {",
|
|
2596
|
+
" out.append(char)",
|
|
2597
|
+
" continue",
|
|
2598
|
+
" }",
|
|
2599
|
+
" if (pos >= text.length) fail(\"unterminated escape\")",
|
|
2600
|
+
" when (text[pos++]) {",
|
|
2601
|
+
` '"', '\\\\', '/' -> out.append(text[pos - 1])`,
|
|
2602
|
+
" 'b' -> out.append('\\b')",
|
|
2603
|
+
" 'f' -> out.append('\\u000C')",
|
|
2604
|
+
" 'n' -> out.append('\\n')",
|
|
2605
|
+
" 'r' -> out.append('\\r')",
|
|
2606
|
+
" 't' -> out.append('\\t')",
|
|
2607
|
+
" 'u' -> {",
|
|
2608
|
+
" if (pos + 4 > text.length) fail(\"bad unicode escape\")",
|
|
2609
|
+
" var code = 0",
|
|
2610
|
+
" for (i in 0 until 4) {",
|
|
2611
|
+
" val digit = text[pos + i].lowercaseChar()",
|
|
2612
|
+
` val value = if (digit in '0'..'9') digit - '0' else if (digit in 'a'..'f') digit - 'a' + 10 else fail("bad unicode escape")`,
|
|
2613
|
+
" code = code * 16 + value",
|
|
2614
|
+
" }",
|
|
2615
|
+
" pos += 4",
|
|
2616
|
+
" out.append(code.toChar())",
|
|
2617
|
+
" }",
|
|
2618
|
+
" else -> fail(\"bad escape\")",
|
|
2619
|
+
" }",
|
|
2620
|
+
" }",
|
|
2621
|
+
" }",
|
|
2622
|
+
"",
|
|
2623
|
+
" fun nextDouble(): Double {",
|
|
2624
|
+
" skipSpace()",
|
|
2625
|
+
" val start = pos",
|
|
2626
|
+
" while (pos < text.length && (text[pos] in '0'..'9' || text[pos] == '.' || text[pos] == 'e' || text[pos] == 'E' || text[pos] == '+' || text[pos] == '-')) pos++",
|
|
2627
|
+
" if (pos == start) fail(\"expected a number\")",
|
|
2628
|
+
" return text.substring(start, pos).toDoubleOrNull() ?: fail(\"bad number\")",
|
|
2629
|
+
" }",
|
|
2630
|
+
"",
|
|
2631
|
+
" // plain-digit fast paths: the value when the token at pos is an integer",
|
|
2632
|
+
" // in range, null when it needs the double slow path. nothing is",
|
|
2633
|
+
" // consumed on null, so the caller falls back to nextDouble.",
|
|
2634
|
+
" fun nextIntOrNull(): Int? {",
|
|
2635
|
+
" var i = pos",
|
|
2636
|
+
" val negative = i < text.length && text[i] == '-'",
|
|
2637
|
+
" if (negative) i++",
|
|
2638
|
+
" var value = 0L",
|
|
2639
|
+
" val digits = i",
|
|
2640
|
+
" while (i < text.length && text[i] in '0'..'9') {",
|
|
2641
|
+
" value = value * 10 + (text[i] - '0')",
|
|
2642
|
+
" if (value > 2147483648L) return null",
|
|
2643
|
+
" i++",
|
|
2644
|
+
" }",
|
|
2645
|
+
" if (i == digits) return null",
|
|
2646
|
+
" if (i < text.length && (text[i] == '.' || text[i] == 'e' || text[i] == 'E')) return null",
|
|
2647
|
+
" if (!negative && value > 2147483647L) return null",
|
|
2648
|
+
" pos = i",
|
|
2649
|
+
" return if (negative) -value.toInt() else value.toInt()",
|
|
2650
|
+
" }",
|
|
2651
|
+
"",
|
|
2652
|
+
" fun nextLongOrNull(): Long? {",
|
|
2653
|
+
" var i = pos",
|
|
2654
|
+
" val negative = i < text.length && text[i] == '-'",
|
|
2655
|
+
" if (negative) i++",
|
|
2656
|
+
" var value = 0L",
|
|
2657
|
+
" val digits = i",
|
|
2658
|
+
" while (i < text.length && text[i] in '0'..'9') {",
|
|
2659
|
+
" val digit = (text[i] - '0').toLong()",
|
|
2660
|
+
" if (value > (9007199254740991L - digit) / 10) return null",
|
|
2661
|
+
" value = value * 10 + digit",
|
|
2662
|
+
" i++",
|
|
2663
|
+
" }",
|
|
2664
|
+
" if (i == digits) return null",
|
|
2665
|
+
" if (i < text.length && (text[i] == '.' || text[i] == 'e' || text[i] == 'E')) return null",
|
|
2666
|
+
" pos = i",
|
|
2667
|
+
" return if (negative) -value else value",
|
|
2668
|
+
" }",
|
|
2669
|
+
"",
|
|
2670
|
+
" fun nextBoolean(): Boolean {",
|
|
2671
|
+
" skipSpace()",
|
|
2672
|
+
" if (text.startsWith(\"true\", pos)) {",
|
|
2673
|
+
" pos += 4",
|
|
2674
|
+
" return true",
|
|
2675
|
+
" }",
|
|
2676
|
+
" if (text.startsWith(\"false\", pos)) {",
|
|
2677
|
+
" pos += 5",
|
|
2678
|
+
" return false",
|
|
2679
|
+
" }",
|
|
2680
|
+
" fail(\"expected a boolean\")",
|
|
2681
|
+
" }",
|
|
2682
|
+
"",
|
|
2683
|
+
" fun nextNull() {",
|
|
2684
|
+
" skipSpace()",
|
|
2685
|
+
" if (!text.startsWith(\"null\", pos)) fail(\"expected null\")",
|
|
2686
|
+
" pos += 4",
|
|
2687
|
+
" }",
|
|
2688
|
+
"",
|
|
2689
|
+
" fun skipValue() {",
|
|
2690
|
+
" when (peek()) {",
|
|
2691
|
+
` ${tokenClass}.BEGIN_OBJECT -> {`,
|
|
2692
|
+
" beginObject()",
|
|
2693
|
+
" while (hasNext()) {",
|
|
2694
|
+
" nextName()",
|
|
2695
|
+
" skipValue()",
|
|
2696
|
+
" }",
|
|
2697
|
+
" endObject()",
|
|
2698
|
+
" }",
|
|
2699
|
+
` ${tokenClass}.BEGIN_ARRAY -> {`,
|
|
2700
|
+
" beginArray()",
|
|
2701
|
+
" while (hasNext()) {",
|
|
2702
|
+
" skipValue()",
|
|
2703
|
+
" }",
|
|
2704
|
+
" endArray()",
|
|
2705
|
+
" }",
|
|
2706
|
+
` ${tokenClass}.STRING -> nextString()`,
|
|
2707
|
+
` ${tokenClass}.NUMBER -> nextDouble()`,
|
|
2708
|
+
` ${tokenClass}.BOOLEAN -> nextBoolean()`,
|
|
2709
|
+
` ${tokenClass}.NULL -> nextNull()`,
|
|
2710
|
+
" else -> fail(\"unexpected value\")",
|
|
2711
|
+
" }",
|
|
2712
|
+
" }",
|
|
2713
|
+
"",
|
|
2714
|
+
" fun endDocument() {",
|
|
2715
|
+
" skipSpace()",
|
|
2716
|
+
" if (pos != text.length) fail(\"trailing characters\")",
|
|
2717
|
+
" }",
|
|
2718
|
+
"}",
|
|
2719
|
+
"",
|
|
2720
|
+
`private fun readString(reader: ${readerClass}, path: String): String {`,
|
|
2721
|
+
` if (reader.peek() != ${tokenClass}.STRING) error("$path: expected string")`,
|
|
2722
|
+
" return reader.nextString()",
|
|
2723
|
+
"}",
|
|
2724
|
+
`private fun readBoolean(reader: ${readerClass}, path: String): Boolean {`,
|
|
2725
|
+
` if (reader.peek() != ${tokenClass}.BOOLEAN) error("$path: expected boolean")`,
|
|
2726
|
+
" return reader.nextBoolean()",
|
|
2727
|
+
"}",
|
|
2728
|
+
`private fun readDouble(reader: ${readerClass}, path: String): Double {`,
|
|
2729
|
+
` if (reader.peek() != ${tokenClass}.NUMBER) error("$path: expected number")`,
|
|
2730
|
+
" val result = reader.nextDouble()",
|
|
2281
2731
|
" if (!result.isFinite()) error(\"$path: expected finite number\")",
|
|
2282
2732
|
" return result",
|
|
2283
2733
|
"}",
|
|
2284
|
-
|
|
2285
|
-
|
|
2734
|
+
`private fun readInt(reader: ${readerClass}, path: String): Int {`,
|
|
2735
|
+
` if (reader.peek() != ${tokenClass}.NUMBER) error("$path: expected number")`,
|
|
2736
|
+
" reader.nextIntOrNull()?.let { return it }",
|
|
2737
|
+
" val result = readDouble(reader, path)",
|
|
2286
2738
|
" if (result % 1.0 != 0.0 || result < Int.MIN_VALUE || result > Int.MAX_VALUE) error(\"$path: expected Int\")",
|
|
2287
2739
|
" return result.toInt()",
|
|
2288
2740
|
"}",
|
|
2289
|
-
|
|
2290
|
-
|
|
2741
|
+
`private fun readLong(reader: ${readerClass}, path: String): Long {`,
|
|
2742
|
+
` if (reader.peek() != ${tokenClass}.NUMBER) error("$path: expected number")`,
|
|
2743
|
+
" reader.nextLongOrNull()?.let { return it }",
|
|
2744
|
+
" val result = readDouble(reader, path)",
|
|
2291
2745
|
" if (result % 1.0 != 0.0 || result < -9007199254740991.0 || result > 9007199254740991.0) error(\"$path: expected safe Long\")",
|
|
2292
2746
|
" return result.toLong()",
|
|
2293
2747
|
"}",
|
|
2294
|
-
"
|
|
2295
|
-
"
|
|
2296
|
-
|
|
2748
|
+
"// the host sends true for each callback the element passed; any other",
|
|
2749
|
+
"// value reads as absent.",
|
|
2750
|
+
`private fun readOptBoolean(reader: ${readerClass}): Boolean {`,
|
|
2751
|
+
` if (reader.peek() != ${tokenClass}.BOOLEAN) {`,
|
|
2752
|
+
" reader.skipValue()",
|
|
2753
|
+
" return false",
|
|
2754
|
+
" }",
|
|
2755
|
+
" return reader.nextBoolean()",
|
|
2756
|
+
"}",
|
|
2757
|
+
`private fun <T> readList(reader: ${readerClass}, path: String, read: (${readerClass}, String) -> T): List<T> {`,
|
|
2758
|
+
` if (reader.peek() != ${tokenClass}.BEGIN_ARRAY) error("$path: expected array")`,
|
|
2759
|
+
" reader.beginArray()",
|
|
2760
|
+
" val result = mutableListOf<T>()",
|
|
2761
|
+
" var index = 0",
|
|
2762
|
+
" while (reader.hasNext()) {",
|
|
2763
|
+
" result.add(read(reader, \"$path[$index]\"))",
|
|
2764
|
+
" index++",
|
|
2765
|
+
" }",
|
|
2766
|
+
" reader.endArray()",
|
|
2767
|
+
" return result",
|
|
2297
2768
|
"}",
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
"
|
|
2769
|
+
`private fun <T> readMap(reader: ${readerClass}, path: String, read: (${readerClass}, String) -> T): Map<String, T> {`,
|
|
2770
|
+
` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("$path: expected object")`,
|
|
2771
|
+
" reader.beginObject()",
|
|
2772
|
+
" val result = mutableMapOf<String, T>()",
|
|
2773
|
+
" while (reader.hasNext()) {",
|
|
2774
|
+
" val key = reader.nextName()",
|
|
2775
|
+
" result[key] = read(reader, \"$path.$key\")",
|
|
2776
|
+
" }",
|
|
2777
|
+
" reader.endObject()",
|
|
2778
|
+
" return result",
|
|
2301
2779
|
"}",
|
|
2302
2780
|
...needsDate ? [
|
|
2303
|
-
|
|
2304
|
-
" val text = readString(
|
|
2781
|
+
`private fun readDate(reader: ${readerClass}, path: String): java.util.Date {`,
|
|
2782
|
+
" val text = readString(reader, path)",
|
|
2305
2783
|
` for (pattern in arrayOf("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "yyyy-MM-dd'T'HH:mm:ss'Z'")) {`,
|
|
2306
2784
|
" try {",
|
|
2307
2785
|
" return java.text.SimpleDateFormat(pattern, java.util.Locale.US).apply { timeZone = java.util.TimeZone.getTimeZone(\"UTC\"); isLenient = false }.parse(text) ?: error(\"$path: expected date\")",
|
|
@@ -2313,9 +2791,9 @@ function renderKotlinSourceGlue(sourceId, contract) {
|
|
|
2313
2791
|
` java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", java.util.Locale.US).apply { timeZone = java.util.TimeZone.getTimeZone("UTC") }.format(value)`
|
|
2314
2792
|
] : [],
|
|
2315
2793
|
...needsData ? [
|
|
2316
|
-
|
|
2794
|
+
`private fun readData(reader: ${readerClass}, path: String): ByteArray {`,
|
|
2317
2795
|
" try {",
|
|
2318
|
-
" return android.util.Base64.decode(readString(
|
|
2796
|
+
" return android.util.Base64.decode(readString(reader, path), android.util.Base64.NO_WRAP)",
|
|
2319
2797
|
" } catch (decodeError: IllegalArgumentException) {",
|
|
2320
2798
|
" error(\"$path: expected base64 data\")",
|
|
2321
2799
|
" }",
|
|
@@ -2327,7 +2805,9 @@ function renderKotlinSourceGlue(sourceId, contract) {
|
|
|
2327
2805
|
`@Keep class OneNativeSource_${sourceId}: OneNativeSourceDispatch {`,
|
|
2328
2806
|
` override val contractHash = ${JSON.stringify(hash)}`,
|
|
2329
2807
|
" override suspend fun call(module: String, method: String, argsJson: String): String {",
|
|
2330
|
-
|
|
2808
|
+
` val reader = ${readerClass}(argsJson)`,
|
|
2809
|
+
` if (reader.peek() != ${tokenClass}.BEGIN_ARRAY) error("args must be a JSON array")`,
|
|
2810
|
+
" reader.beginArray()",
|
|
2331
2811
|
" return when (module) {",
|
|
2332
2812
|
...contract.modules.flatMap(function(module) {
|
|
2333
2813
|
return [
|
|
@@ -2343,16 +2823,24 @@ function renderKotlinSourceGlue(sourceId, contract) {
|
|
|
2343
2823
|
}).join("")}.toString()) }`;
|
|
2344
2824
|
return [
|
|
2345
2825
|
` ${JSON.stringify(method.name)} -> {`,
|
|
2346
|
-
` require(
|
|
2826
|
+
` require(!reader.hasNext()) { "${sourceId}.${module.name}.${method.name} subscribes; it takes no arguments" }`,
|
|
2827
|
+
" reader.endArray()",
|
|
2828
|
+
" reader.endDocument()",
|
|
2347
2829
|
` ${qualified(module.name)}.${method.name}(${closure})`,
|
|
2348
2830
|
" \"null\"",
|
|
2349
2831
|
" }"
|
|
2350
2832
|
];
|
|
2351
2833
|
}
|
|
2352
|
-
var lines = [` ${JSON.stringify(method.name)} -> {
|
|
2834
|
+
var lines = [` ${JSON.stringify(method.name)} -> {`];
|
|
2835
|
+
var arityMessage = `${sourceId}.${module.name}.${method.name} expects ${method.parameters.length} arguments`;
|
|
2353
2836
|
method.parameters.forEach(function(parameter, index) {
|
|
2354
|
-
lines.push(` val arg${index} = ${value(parameter.nativeType,
|
|
2837
|
+
lines.push(` val arg${index} = ${value(parameter.nativeType, "reader", JSON.stringify(`args[${index}]`))}`);
|
|
2838
|
+
if (index < method.parameters.length - 1) {
|
|
2839
|
+
lines.push(` require(reader.hasNext()) { ${JSON.stringify(arityMessage)} }`);
|
|
2840
|
+
}
|
|
2355
2841
|
});
|
|
2842
|
+
lines.push(` require(!reader.hasNext()) { ${JSON.stringify(arityMessage)} }`);
|
|
2843
|
+
lines.push(" reader.endArray()", " reader.endDocument()");
|
|
2356
2844
|
var call = `${qualified(module.name)}.${method.name}(${method.parameters.map(function(_, index) {
|
|
2357
2845
|
return `arg${index}`;
|
|
2358
2846
|
}).join(", ")})`;
|
|
@@ -2376,27 +2864,223 @@ function renderKotlinSourceGlue(sourceId, contract) {
|
|
|
2376
2864
|
" }",
|
|
2377
2865
|
"}",
|
|
2378
2866
|
"",
|
|
2379
|
-
...contract.views.length === 0 ? [] : viewGlue(sourceId, contract, hash, value, encode)
|
|
2867
|
+
...contract.views.length === 0 ? [] : viewGlue(sourceId, contract, hash, value, encode, qualifiedType)
|
|
2380
2868
|
].join("\n");
|
|
2381
2869
|
return {
|
|
2382
2870
|
hash,
|
|
2383
2871
|
source
|
|
2384
2872
|
};
|
|
2385
2873
|
}
|
|
2386
|
-
function viewGlue(sourceId, contract, hash, value, encode) {
|
|
2874
|
+
function viewGlue(sourceId, contract, hash, value, encode, qualifiedType) {
|
|
2387
2875
|
var qualifier = contract.packageName ? `${contract.packageName}.` : "";
|
|
2876
|
+
var cap = function cap2(name) {
|
|
2877
|
+
return name.slice(0, 1).toUpperCase() + name.slice(1);
|
|
2878
|
+
};
|
|
2879
|
+
var readerClass = `OneJsonReader_${sourceId}`;
|
|
2880
|
+
var tokenClass = `OneJsonToken_${sourceId}`;
|
|
2881
|
+
var SEQ_KEY = "\"\\$seq\"";
|
|
2882
|
+
var BASE_KEY = "\"\\$base\"";
|
|
2883
|
+
var LISTS_KEY = "\"\\$lists\"";
|
|
2884
|
+
var listElement = function listElement2(type) {
|
|
2885
|
+
var inner = type.endsWith("?") ? type.slice(0, -1) : type;
|
|
2886
|
+
return inner.startsWith("List<") && inner.endsWith(">") ? inner.slice(5, -1) : null;
|
|
2887
|
+
};
|
|
2888
|
+
var holders = contract.views.flatMap(function(view) {
|
|
2889
|
+
return [
|
|
2890
|
+
`private class Held${view.name}_${sourceId} {`,
|
|
2891
|
+
...view.props.flatMap(function(prop) {
|
|
2892
|
+
if (prop.callback) {
|
|
2893
|
+
return prop.nativeType.endsWith("?") ? [` var has${cap(prop.name)}: Boolean = false`] : [];
|
|
2894
|
+
}
|
|
2895
|
+
var declared = qualifiedType(prop.nativeType);
|
|
2896
|
+
if (prop.optional || prop.nativeType.endsWith("?")) return [` var ${prop.name}: ${declared} = null`];
|
|
2897
|
+
var zero = unboxedZero(prop.nativeType);
|
|
2898
|
+
if (zero !== null) return [` var ${prop.name}: ${declared} = ${zero}`];
|
|
2899
|
+
if (prop.nativeType === "String") return [` var ${prop.name}: String = ""`];
|
|
2900
|
+
return [` var ${prop.name}: ${declared}? = null`];
|
|
2901
|
+
}),
|
|
2902
|
+
"}"
|
|
2903
|
+
];
|
|
2904
|
+
});
|
|
2905
|
+
var mergers = contract.views.flatMap(function(view) {
|
|
2906
|
+
return view.props.flatMap(function(prop) {
|
|
2907
|
+
if (prop.callback) return [];
|
|
2908
|
+
var elem = listElement(prop.nativeType);
|
|
2909
|
+
if (elem === null) return [];
|
|
2910
|
+
var element = qualifiedType(elem);
|
|
2911
|
+
return [
|
|
2912
|
+
`private fun mergeLists${view.name}${cap(prop.name)}(reader: ${readerClass}, old: List<${element}>?, prop: String): List<${element}>? {`,
|
|
2913
|
+
` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("lists.$prop: expected object")`,
|
|
2914
|
+
" reader.beginObject()",
|
|
2915
|
+
" var length = -1",
|
|
2916
|
+
` var set: List<Pair<Int, ${element}>>? = null`,
|
|
2917
|
+
" while (reader.hasNext()) {",
|
|
2918
|
+
" when (reader.selectName(\"length\", \"set\")) {",
|
|
2919
|
+
` 0 -> length = readInt(reader, "lists.$prop.length")`,
|
|
2920
|
+
` 1 -> set = readList(reader, "lists.$prop.set") { itemReader, itemPath -> itemReader.beginArray(); val si = readInt(itemReader, itemPath); if (!itemReader.hasNext()) error("$itemPath: expected element"); val sv = ${value(elem, "itemReader", "itemPath")}; itemReader.endArray(); si to sv }`,
|
|
2921
|
+
" else -> reader.skipValue()",
|
|
2922
|
+
" }",
|
|
2923
|
+
" }",
|
|
2924
|
+
" reader.endObject()",
|
|
2925
|
+
" if (length < 0) error(\"lists.$prop: needs length\")",
|
|
2926
|
+
" val current = old ?: return null",
|
|
2927
|
+
" val entries = set ?: emptyList()",
|
|
2928
|
+
" for (j in 1 until entries.size) require(entries[j].first > entries[j - 1].first) { \"lists.$prop: set out of order\" }",
|
|
2929
|
+
` val fresh = ArrayList<${element}>(length)`,
|
|
2930
|
+
" var s = 0",
|
|
2931
|
+
" for (i in 0 until length) {",
|
|
2932
|
+
" if (s < entries.size && entries[s].first == i) { fresh.add(entries[s].second); s++ }",
|
|
2933
|
+
" else if (i < current.size) fresh.add(current[i])",
|
|
2934
|
+
" else return null",
|
|
2935
|
+
" }",
|
|
2936
|
+
" return fresh",
|
|
2937
|
+
"}"
|
|
2938
|
+
];
|
|
2939
|
+
});
|
|
2940
|
+
});
|
|
2388
2941
|
return [
|
|
2942
|
+
...holders,
|
|
2943
|
+
...mergers,
|
|
2944
|
+
"",
|
|
2389
2945
|
`@Keep class OneNativeSourceViews_${sourceId}: OneNativeSourceViewDispatch {`,
|
|
2390
2946
|
` override val contractHash = ${JSON.stringify(hash)}`,
|
|
2947
|
+
" override fun apply(view: String, propsJson: String, state: OneNativeSourceKeyedState): Boolean {",
|
|
2948
|
+
` val reader = ${readerClass}(propsJson)`,
|
|
2949
|
+
` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("props must be a JSON object")`,
|
|
2950
|
+
" reader.beginObject()",
|
|
2951
|
+
" when (view) {",
|
|
2952
|
+
...contract.views.flatMap(function(view) {
|
|
2953
|
+
var holder = `Held${view.name}_${sourceId}`;
|
|
2954
|
+
var lists = view.props.filter(function(prop) {
|
|
2955
|
+
return !prop.callback && listElement(prop.nativeType) !== null;
|
|
2956
|
+
});
|
|
2957
|
+
var options = [
|
|
2958
|
+
SEQ_KEY,
|
|
2959
|
+
BASE_KEY,
|
|
2960
|
+
LISTS_KEY,
|
|
2961
|
+
...view.props.map(function(prop) {
|
|
2962
|
+
return JSON.stringify(prop.name);
|
|
2963
|
+
})
|
|
2964
|
+
].join(", ");
|
|
2965
|
+
return [
|
|
2966
|
+
` ${JSON.stringify(view.name)} -> {`,
|
|
2967
|
+
` var held = state.views[${JSON.stringify(view.name)}] as? ${holder}`,
|
|
2968
|
+
" var seq = -1",
|
|
2969
|
+
" var hasSeq = false",
|
|
2970
|
+
" var base = -1",
|
|
2971
|
+
" var hasBase = false",
|
|
2972
|
+
" var sawLists = false",
|
|
2973
|
+
...view.props.flatMap(function(prop) {
|
|
2974
|
+
if (prop.callback) {
|
|
2975
|
+
return prop.nativeType.endsWith("?") ? [` var ${prop.name}Doc: Boolean? = null`] : [];
|
|
2976
|
+
}
|
|
2977
|
+
var declared = qualifiedType(prop.nativeType);
|
|
2978
|
+
if (prop.optional || prop.nativeType.endsWith("?")) {
|
|
2979
|
+
return [` var ${prop.name}: ${declared} = null`, ` var has${cap(prop.name)} = false`];
|
|
2980
|
+
}
|
|
2981
|
+
var zero = unboxedZero(prop.nativeType);
|
|
2982
|
+
if (zero !== null) return [` var ${prop.name} = ${zero}`, ` var has${cap(prop.name)} = false`];
|
|
2983
|
+
return [` var ${prop.name}: ${declared}? = null`];
|
|
2984
|
+
}),
|
|
2985
|
+
...lists.map(function(prop) {
|
|
2986
|
+
return ` var ${prop.name}MergeFailed = false`;
|
|
2987
|
+
}),
|
|
2988
|
+
" while (reader.hasNext()) {",
|
|
2989
|
+
` when (reader.selectName(${options})) {`,
|
|
2990
|
+
" 0 -> { seq = readInt(reader, \"seq\"); hasSeq = true }",
|
|
2991
|
+
" 1 -> { base = readInt(reader, \"base\"); hasBase = true }",
|
|
2992
|
+
" 2 -> {",
|
|
2993
|
+
" sawLists = true",
|
|
2994
|
+
` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("lists: expected object")`,
|
|
2995
|
+
" reader.beginObject()",
|
|
2996
|
+
" while (reader.hasNext()) {",
|
|
2997
|
+
` when (reader.selectName(${lists.map(function(prop) {
|
|
2998
|
+
return JSON.stringify(prop.name);
|
|
2999
|
+
}).join(", ")})) {`,
|
|
3000
|
+
...lists.map(function(prop, listsIndex) {
|
|
3001
|
+
return ` ${listsIndex} -> { val merged${cap(prop.name)} = mergeLists${view.name}${cap(prop.name)}(reader, held?.${prop.name}, ${JSON.stringify(prop.name)}); if (merged${cap(prop.name)} == null) ${prop.name}MergeFailed = true else ${prop.name} = merged${cap(prop.name)} }`;
|
|
3002
|
+
}),
|
|
3003
|
+
" else -> reader.skipValue()",
|
|
3004
|
+
" }",
|
|
3005
|
+
" }",
|
|
3006
|
+
" reader.endObject()",
|
|
3007
|
+
" }",
|
|
3008
|
+
...view.props.map(function(prop, index) {
|
|
3009
|
+
var arm = index + 3;
|
|
3010
|
+
if (prop.callback) {
|
|
3011
|
+
return prop.nativeType.endsWith("?") ? ` ${arm} -> ${prop.name}Doc = readOptBoolean(reader)` : ` ${arm} -> reader.skipValue()`;
|
|
3012
|
+
}
|
|
3013
|
+
var path = JSON.stringify(`props.${prop.name}`);
|
|
3014
|
+
var read = `${prop.name} = ${value(prop.nativeType, "reader", path)}`;
|
|
3015
|
+
var needsFlag = prop.optional || prop.nativeType.endsWith("?") || unboxedZero(prop.nativeType) !== null;
|
|
3016
|
+
return needsFlag ? ` ${arm} -> { ${read}; has${cap(prop.name)} = true }` : ` ${arm} -> ${read}`;
|
|
3017
|
+
}),
|
|
3018
|
+
" else -> reader.skipValue()",
|
|
3019
|
+
" }",
|
|
3020
|
+
" }",
|
|
3021
|
+
" reader.endObject()",
|
|
3022
|
+
" reader.endDocument()",
|
|
3023
|
+
" if (hasBase && !hasSeq) error(\"seq: required\")",
|
|
3024
|
+
" if (sawLists && !hasBase) error(\"lists: needs a change document\")",
|
|
3025
|
+
" if (!hasBase) {",
|
|
3026
|
+
...view.props.flatMap(function(prop) {
|
|
3027
|
+
if (prop.callback || prop.optional) return [];
|
|
3028
|
+
if (prop.nativeType.endsWith("?") || unboxedZero(prop.nativeType) !== null) {
|
|
3029
|
+
return [` if (!has${cap(prop.name)}) error("props.${prop.name}: required")`];
|
|
3030
|
+
}
|
|
3031
|
+
return [];
|
|
3032
|
+
}),
|
|
3033
|
+
` val h = held ?: ${holder}().also { state.views[${JSON.stringify(view.name)}] = it }`,
|
|
3034
|
+
...view.props.flatMap(function(prop) {
|
|
3035
|
+
if (prop.callback) {
|
|
3036
|
+
return prop.nativeType.endsWith("?") ? [` h.has${cap(prop.name)} = ${prop.name}Doc == true`] : [];
|
|
3037
|
+
}
|
|
3038
|
+
if (prop.optional || prop.nativeType.endsWith("?") || unboxedZero(prop.nativeType) !== null) {
|
|
3039
|
+
return [` h.${prop.name} = ${prop.name}`];
|
|
3040
|
+
}
|
|
3041
|
+
return [` h.${prop.name} = (${prop.name} ?: error("props.${prop.name}: required"))`];
|
|
3042
|
+
}),
|
|
3043
|
+
" if (hasSeq) state.seq = seq",
|
|
3044
|
+
" state.revision++",
|
|
3045
|
+
" return true",
|
|
3046
|
+
" }",
|
|
3047
|
+
" if (seq == state.seq) return true",
|
|
3048
|
+
" val h = held",
|
|
3049
|
+
" if (base == state.seq && h != null) {",
|
|
3050
|
+
...view.props.flatMap(function(prop) {
|
|
3051
|
+
if (prop.callback) {
|
|
3052
|
+
return prop.nativeType.endsWith("?") ? [` if (${prop.name}Doc != null) h.has${cap(prop.name)} = ${prop.name}Doc`] : [];
|
|
3053
|
+
}
|
|
3054
|
+
var mergeFailed = listElement(prop.nativeType) !== null ? [` if (${prop.name}MergeFailed) error("lists.${prop.name}: cannot merge")`] : [];
|
|
3055
|
+
if (prop.optional || prop.nativeType.endsWith("?") || unboxedZero(prop.nativeType) !== null) {
|
|
3056
|
+
return [...mergeFailed, ` if (has${cap(prop.name)}) h.${prop.name} = ${prop.name}`];
|
|
3057
|
+
}
|
|
3058
|
+
return [...mergeFailed, ` if (${prop.name} != null) h.${prop.name} = ${prop.name}`];
|
|
3059
|
+
}),
|
|
3060
|
+
" state.seq = seq",
|
|
3061
|
+
" state.revision++",
|
|
3062
|
+
" return true",
|
|
3063
|
+
" }",
|
|
3064
|
+
" return false",
|
|
3065
|
+
" }"
|
|
3066
|
+
];
|
|
3067
|
+
}),
|
|
3068
|
+
" else -> error(\"composable $view is not linked; rebuild the app\")",
|
|
3069
|
+
" }",
|
|
3070
|
+
" }",
|
|
2391
3071
|
" @Composable",
|
|
2392
|
-
" override fun Content(view: String,
|
|
2393
|
-
" val props = JSONObject(propsJson)",
|
|
3072
|
+
" override fun Content(view: String, state: OneNativeSourceKeyedState, emit: (String, String) -> Unit) {",
|
|
2394
3073
|
" when (view) {",
|
|
2395
3074
|
...contract.views.flatMap(function(view) {
|
|
3075
|
+
var holder = `Held${view.name}_${sourceId}`;
|
|
2396
3076
|
return [
|
|
2397
|
-
` ${JSON.stringify(view.name)} ->
|
|
3077
|
+
` ${JSON.stringify(view.name)} -> {`,
|
|
3078
|
+
" // the revision read resubscribes this scope on every apply",
|
|
3079
|
+
" val revision = state.revision",
|
|
3080
|
+
` val held = state.views[${JSON.stringify(view.name)}] as? ${holder}`,
|
|
3081
|
+
" held?.let { h ->",
|
|
3082
|
+
` ${qualifier}${view.name}(`,
|
|
2398
3083
|
...view.props.map(function(prop) {
|
|
2399
|
-
var path = JSON.stringify(`props.${prop.name}`);
|
|
2400
3084
|
if (prop.callback) {
|
|
2401
3085
|
var args = prop.callback.map(function(_, index) {
|
|
2402
3086
|
return `a${index}`;
|
|
@@ -2408,12 +3092,16 @@ function viewGlue(sourceId, contract, hash, value, encode) {
|
|
|
2408
3092
|
return `.put(${encode(types[index], arg)})`;
|
|
2409
3093
|
}).join("")}.toString())`;
|
|
2410
3094
|
var lambda = `{ ${args.length > 0 ? `${args.join(", ")} -> ` : ""}${body} }`;
|
|
2411
|
-
return `
|
|
3095
|
+
return ` ${prop.name} = ${prop.nativeType.endsWith("?") ? `if (h.has${cap(prop.name)}) (${lambda}) else null` : lambda},`;
|
|
2412
3096
|
}
|
|
2413
|
-
|
|
2414
|
-
|
|
3097
|
+
if (prop.optional || prop.nativeType.endsWith("?") || unboxedZero(prop.nativeType) !== null || prop.nativeType === "String") {
|
|
3098
|
+
return ` ${prop.name} = h.${prop.name},`;
|
|
3099
|
+
}
|
|
3100
|
+
return ` ${prop.name} = h.${prop.name}!!,`;
|
|
2415
3101
|
}),
|
|
2416
|
-
"
|
|
3102
|
+
" )",
|
|
3103
|
+
" }",
|
|
3104
|
+
" }"
|
|
2417
3105
|
];
|
|
2418
3106
|
}),
|
|
2419
3107
|
" else -> error(\"composable $view is not linked; rebuild the app\")",
|