vxrn 2.13.10 → 2.14.1
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
|
@@ -1189,10 +1189,11 @@ function nativeSourceContract(file, source) {
|
|
|
1189
1189
|
...module.methods.map((method) => method.subscription ? ` ${method.name}(listener: (${method.subscription.payload.map((arg, index) => `value${index}: ${arg.type}`).join(", ")}) => void): () => void` : ` ${method.name}(${method.parameters.map((param) => `${param.name}: ${param.type}`).join(", ")}): Promise<${method.result}>`),
|
|
1190
1190
|
"}"
|
|
1191
1191
|
].join("\n")));
|
|
1192
|
-
declarations.push(...views.map((view) => `export declare function ${view.name}(props: {${view.props.map((prop) => ` ${prop.name}${prop.optional ? "?" : ""}: ${prop.type}
|
|
1192
|
+
declarations.push(...views.map((view) => `export declare function ${view.name}(props: {${view.props.map((prop) => ` ${prop.name}${prop.optional ? "?" : ""}: ${prop.type};`).join("")} style?: StyleProp<ViewStyle> }): ReactElement`));
|
|
1193
1193
|
if (defaultView) {
|
|
1194
1194
|
declarations.unshift("export default function SwiftPackage(props: Record<string, unknown>): ReactElement");
|
|
1195
1195
|
}
|
|
1196
|
+
if (views.length > 0) declarations.unshift("import type { StyleProp, ViewStyle } from 'react-native'");
|
|
1196
1197
|
if (defaultView || views.length > 0) declarations.unshift("import type { ReactElement } from 'react'");
|
|
1197
1198
|
const declaration = `${GENERATED_HEADER}${declarations.join("\n\n")}
|
|
1198
1199
|
`;
|
|
@@ -1420,35 +1421,147 @@ function renderSwiftSourceGlue(packageId, contracts) {
|
|
|
1420
1421
|
" }",
|
|
1421
1422
|
" }"
|
|
1422
1423
|
];
|
|
1423
|
-
const renderViews = () =>
|
|
1424
|
-
"
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1424
|
+
const renderViews = () => {
|
|
1425
|
+
const notLinked2 = " default: throw NSError(domain: \"OneNativeSource\", code: 1, userInfo: [NSLocalizedDescriptionKey: \"swift view \\(name) is not linked; rebuild the app\"])";
|
|
1426
|
+
const fieldsOf = (view) => view.props.filter((prop) => !prop.callback || prop.optional).map((prop, index) => {
|
|
1427
|
+
const type = prop.callback ? "Bool?" : prop.nativeType;
|
|
1428
|
+
const optional = type.endsWith("?");
|
|
1429
|
+
const list = /^\[(.*)\]\??$/.exec(type);
|
|
1430
|
+
return {
|
|
1431
|
+
name: prop.name,
|
|
1432
|
+
type,
|
|
1433
|
+
optional,
|
|
1434
|
+
list: list && !list[1].includes(":") ? list[1] : null,
|
|
1435
|
+
key: `p${index}`
|
|
1436
|
+
};
|
|
1437
|
+
});
|
|
1438
|
+
return [
|
|
1439
|
+
" private struct OneListPatch<E: Decodable>: Decodable {",
|
|
1440
|
+
" let length: Int",
|
|
1441
|
+
" let set: [(Int, E)]",
|
|
1442
|
+
" private struct Entry: Decodable {",
|
|
1443
|
+
" let index: Int",
|
|
1444
|
+
" let value: E",
|
|
1445
|
+
" init(from decoder: Decoder) throws {",
|
|
1446
|
+
" var c = try decoder.unkeyedContainer()",
|
|
1447
|
+
" index = try c.decode(Int.self)",
|
|
1448
|
+
" value = try c.decode(E.self)",
|
|
1449
|
+
" }",
|
|
1450
|
+
" }",
|
|
1451
|
+
" private enum CodingKeys: String, CodingKey { case length, set }",
|
|
1452
|
+
" init(from decoder: Decoder) throws {",
|
|
1453
|
+
" let c = try decoder.container(keyedBy: CodingKeys.self)",
|
|
1454
|
+
" length = try c.decode(Int.self, forKey: .length)",
|
|
1455
|
+
" set = try c.decode([Entry].self, forKey: .set).map { ($0.index, $0.value) }",
|
|
1456
|
+
" }",
|
|
1457
|
+
" }",
|
|
1458
|
+
" private nonisolated static func patch<E>(_ list: inout [E], _ patch: OneListPatch<E>) -> Bool {",
|
|
1459
|
+
" if patch.length < 0 { return false }",
|
|
1460
|
+
" if list.count > patch.length { list.removeLast(list.count - patch.length) }",
|
|
1461
|
+
" for (index, value) in patch.set {",
|
|
1462
|
+
" if index < list.count { list[index] = value } else if index == list.count && index < patch.length { list.append(value) } else { return false }",
|
|
1463
|
+
" }",
|
|
1464
|
+
" return list.count == patch.length",
|
|
1465
|
+
" }",
|
|
1466
|
+
" private nonisolated static func missing(_ prop: String) -> Error {",
|
|
1467
|
+
" NSError(domain: \"OneNativeSource\", code: 1, userInfo: [NSLocalizedDescriptionKey: \"missing prop \\(prop)\"])",
|
|
1468
|
+
" }",
|
|
1469
|
+
...views.flatMap((view) => {
|
|
1470
|
+
const fields = fieldsOf(view);
|
|
1471
|
+
const lists = fields.filter((field) => field.list);
|
|
1472
|
+
return [
|
|
1473
|
+
...lists.length > 0 ? [
|
|
1474
|
+
` private struct Lists_${view.name}: Decodable {`,
|
|
1475
|
+
...lists.map((field) => ` let ${field.key}: OneListPatch<${field.list}>?`),
|
|
1476
|
+
` private enum CodingKeys: String, CodingKey { ${lists.map((field) => `case ${field.key} = ${JSON.stringify(field.name)}`).join("; ")} }`,
|
|
1477
|
+
" }"
|
|
1478
|
+
] : [],
|
|
1479
|
+
` private struct Doc_${view.name}: Decodable {`,
|
|
1480
|
+
" let seq: Int",
|
|
1481
|
+
" let base: Int?",
|
|
1482
|
+
...lists.length > 0 ? [` let lists: Lists_${view.name}?`] : [],
|
|
1483
|
+
...fields.map((field) => ` let ${field.key}: Optional<${field.type}>`),
|
|
1484
|
+
` private enum CodingKeys: String, CodingKey { ${[
|
|
1485
|
+
"case seq = \"$seq\"",
|
|
1486
|
+
"case base = \"$base\"",
|
|
1487
|
+
"case lists = \"$lists\"",
|
|
1488
|
+
...fields.map((field) => `case ${field.key} = ${JSON.stringify(field.name)}`)
|
|
1489
|
+
].join("; ")} }`,
|
|
1490
|
+
" init(from decoder: Decoder) throws {",
|
|
1491
|
+
" let c = try decoder.container(keyedBy: CodingKeys.self)",
|
|
1492
|
+
" seq = try c.decodeIfPresent(Int.self, forKey: .seq) ?? 0",
|
|
1493
|
+
" base = try c.decodeIfPresent(Int.self, forKey: .base)",
|
|
1494
|
+
...lists.length > 0 ? [` lists = try c.decodeIfPresent(Lists_${view.name}.self, forKey: .lists)`] : [],
|
|
1495
|
+
...fields.map((field) => ` ${field.key} = try c.contains(.${field.key}) ? Optional<${field.type}>.some(c.decode(${field.type}.self, forKey: .${field.key})) : Optional<${field.type}>.none`),
|
|
1496
|
+
" }",
|
|
1497
|
+
" }",
|
|
1498
|
+
` private final class Held_${view.name} {`,
|
|
1499
|
+
" var oneSeq: Int",
|
|
1500
|
+
...fields.map((field) => ` var \`${field.name}\`: ${field.type}`),
|
|
1501
|
+
` init(_ d: Doc_${view.name}) throws {`,
|
|
1502
|
+
" oneSeq = d.seq",
|
|
1503
|
+
...fields.flatMap((field) => 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}`]),
|
|
1504
|
+
" }",
|
|
1505
|
+
` func apply(_ d: Doc_${view.name}) -> Bool {`,
|
|
1506
|
+
" if d.base != oneSeq { return false }",
|
|
1507
|
+
...lists.flatMap((field) => [
|
|
1508
|
+
` var ${field.key} = \`${field.name}\``,
|
|
1509
|
+
` if let v = d.${field.key} { ${field.key} = v }`,
|
|
1510
|
+
` if let p = d.lists?.${field.key} {`,
|
|
1511
|
+
...field.optional ? [
|
|
1512
|
+
` guard var items = ${field.key} else { return false }`,
|
|
1513
|
+
` if !${bodyName}.patch(&items, p) { return false }`,
|
|
1514
|
+
` ${field.key} = items`
|
|
1515
|
+
] : [` if !${bodyName}.patch(&${field.key}, p) { return false }`],
|
|
1516
|
+
" }"
|
|
1517
|
+
]),
|
|
1518
|
+
...fields.map((field) => field.list ? ` \`${field.name}\` = ${field.key}` : ` if let v = d.${field.key} { \`${field.name}\` = v }`),
|
|
1519
|
+
" oneSeq = d.seq",
|
|
1520
|
+
" return true",
|
|
1521
|
+
" }",
|
|
1522
|
+
" }"
|
|
1523
|
+
];
|
|
1445
1524
|
}),
|
|
1446
|
-
"
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1525
|
+
" static func update(_ held: AnyObject?, _ name: String, _ text: String) throws -> AnyObject? {",
|
|
1526
|
+
" let data = Data(text.utf8)",
|
|
1527
|
+
" switch name {",
|
|
1528
|
+
...views.flatMap((view) => [
|
|
1529
|
+
` case ${JSON.stringify(view.name)}:`,
|
|
1530
|
+
` let doc = try decode(Doc_${view.name}.self, from: data)`,
|
|
1531
|
+
` if doc.base == nil { return try Held_${view.name}(doc) }`,
|
|
1532
|
+
` guard let h = held as? Held_${view.name}, h.apply(doc) else { return nil }`,
|
|
1533
|
+
" return h"
|
|
1534
|
+
]),
|
|
1535
|
+
notLinked2,
|
|
1536
|
+
" }",
|
|
1537
|
+
" }",
|
|
1538
|
+
" static func build(_ held: AnyObject, _ name: String, _ emit: @escaping (String, String) -> Void) throws -> AnyView {",
|
|
1539
|
+
" switch name {",
|
|
1540
|
+
...views.flatMap((view) => [
|
|
1541
|
+
` case ${JSON.stringify(view.name)}:`,
|
|
1542
|
+
` 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)"]) }`,
|
|
1543
|
+
` return AnyView(${view.name}(`,
|
|
1544
|
+
...view.props.map((prop, index) => {
|
|
1545
|
+
const comma = index === view.props.length - 1 ? "" : ",";
|
|
1546
|
+
const key = JSON.stringify(prop.name);
|
|
1547
|
+
if (prop.callback) {
|
|
1548
|
+
const params = prop.callback.map((arg, n) => `a${n}: ${arg.nativeType}`).join(", ");
|
|
1549
|
+
const lambda = `{ (${params}) -> Void in emit(${key}, args([${prop.callback.map((_, n) => `a${n}`).join(", ")}])) }`;
|
|
1550
|
+
return ` ${prop.name}: ${prop.optional ? `props.\`${prop.name}\` == true ? ${lambda} : nil` : lambda}${comma}`;
|
|
1551
|
+
}
|
|
1552
|
+
return ` ${prop.name}: props.\`${prop.name}\`${comma}`;
|
|
1553
|
+
}),
|
|
1554
|
+
" ))"
|
|
1555
|
+
]),
|
|
1556
|
+
notLinked2,
|
|
1557
|
+
" }",
|
|
1558
|
+
" }",
|
|
1559
|
+
" static func view(_ name: String, _ propsJson: String, _ emit: @escaping (String, String) -> Void) throws -> AnyView {",
|
|
1560
|
+
" guard let held = try update(nil, name, propsJson) else { throw NSError(domain: \"OneNativeSource\", code: 1, userInfo: [NSLocalizedDescriptionKey: \"\\(name) needs a full props document\"]) }",
|
|
1561
|
+
" return try build(held, name, emit)",
|
|
1562
|
+
" }"
|
|
1563
|
+
];
|
|
1564
|
+
};
|
|
1452
1565
|
const source = [
|
|
1453
1566
|
"import Foundation",
|
|
1454
1567
|
"import SwiftUI",
|
|
@@ -1514,13 +1627,15 @@ function renderSwiftSourceGlue(packageId, contracts) {
|
|
|
1514
1627
|
" if let date = (try? isoDateFractional.parse(text)) ?? (try? isoDate.parse(text)) { return date }",
|
|
1515
1628
|
" throw DecodingError.dataCorruptedError(in: container, debugDescription: \"expected ISO date\")",
|
|
1516
1629
|
" }",
|
|
1517
|
-
" private static func decode<T: Decodable>(_
|
|
1518
|
-
" let data = try JSONSerialization.data(withJSONObject: value, options: [.fragmentsAllowed])",
|
|
1630
|
+
" private static func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T {",
|
|
1519
1631
|
" let decoder = JSONDecoder()",
|
|
1520
1632
|
" decoder.dateDecodingStrategy = .custom { try decodeDate($0) }",
|
|
1521
1633
|
" decoder.dataDecodingStrategy = .base64",
|
|
1522
1634
|
" return try decoder.decode(T.self, from: data)",
|
|
1523
1635
|
" }",
|
|
1636
|
+
" private static func decode<T: Decodable>(_ value: Any, as type: T.Type) throws -> T {",
|
|
1637
|
+
" try decode(T.self, from: try JSONSerialization.data(withJSONObject: value, options: [.fragmentsAllowed]))",
|
|
1638
|
+
" }",
|
|
1524
1639
|
" private static func encode(_ value: any Encodable) throws -> String {",
|
|
1525
1640
|
" let encoder = JSONEncoder()",
|
|
1526
1641
|
" encoder.dateEncodingStrategy = .iso8601",
|
|
@@ -1588,6 +1703,8 @@ function renderSwiftSourceGlue(packageId, contracts) {
|
|
|
1588
1703
|
" public required override init() {}",
|
|
1589
1704
|
` public var contractHash: String { ${bodyName}.hash }`,
|
|
1590
1705
|
` public func view(_ name: String, _ propsJson: String, _ emit: @escaping (String, String) -> Void) throws -> AnyView { try ${bodyName}.view(name, propsJson, emit) }`,
|
|
1706
|
+
` public func updateView(_ held: AnyObject?, _ name: String, _ document: String) throws -> AnyObject? { try ${bodyName}.update(held, name, document) }`,
|
|
1707
|
+
` public func buildView(_ held: AnyObject, _ name: String, _ emit: @escaping (String, String) -> Void) throws -> AnyView { try ${bodyName}.build(held, name, emit) }`,
|
|
1591
1708
|
"}"
|
|
1592
1709
|
] : [],
|
|
1593
1710
|
"#endif",
|
|
@@ -1598,6 +1715,14 @@ function renderSwiftSourceGlue(packageId, contracts) {
|
|
|
1598
1715
|
source
|
|
1599
1716
|
};
|
|
1600
1717
|
}
|
|
1718
|
+
function unboxedZero(type) {
|
|
1719
|
+
if (type === "Int") return "0";
|
|
1720
|
+
if (type === "Long") return "0L";
|
|
1721
|
+
if (type === "Double") return "0.0";
|
|
1722
|
+
if (type === "Float") return "0f";
|
|
1723
|
+
if (type === "Boolean") return "false";
|
|
1724
|
+
return null;
|
|
1725
|
+
}
|
|
1601
1726
|
function renderKotlinSourceGlue(sourceId, contract) {
|
|
1602
1727
|
if (contract.language !== "kotlin") throw new Error("kotlin glue received a swift contract");
|
|
1603
1728
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(sourceId)) throw new Error(`invalid kotlin source id ${sourceId}`);
|
|
@@ -1605,10 +1730,29 @@ function renderKotlinSourceGlue(sourceId, contract) {
|
|
|
1605
1730
|
sourceId,
|
|
1606
1731
|
contract: contract.hash
|
|
1607
1732
|
})).digest("hex");
|
|
1733
|
+
const readerClass = `OneJsonReader_${sourceId}`;
|
|
1734
|
+
const tokenClass = `OneJsonToken_${sourceId}`;
|
|
1608
1735
|
const structsByName = new Map(contract.structs.map((entry) => [entry.name, entry]));
|
|
1609
1736
|
const enumsByName = new Map(contract.enums.map((entry) => [entry.name, entry]));
|
|
1610
1737
|
const qualified = (name) => contract.packageName ? `${contract.packageName}.${name}` : name;
|
|
1611
1738
|
const isDateType = (type) => type === "Date" || type === "java.util.Date";
|
|
1739
|
+
const qualifiedType = (type) => {
|
|
1740
|
+
if (type.endsWith("?")) return `${qualifiedType(type.slice(0, -1))}?`;
|
|
1741
|
+
if (type.startsWith("List<") && type.endsWith(">")) return `List<${qualifiedType(type.slice(5, -1))}>`;
|
|
1742
|
+
if (type.startsWith("Map<String,") && type.endsWith(">")) return `Map<String, ${qualifiedType(type.slice(11, -1))}>`;
|
|
1743
|
+
if (structsByName.has(type) || enumsByName.has(type)) return qualified(type);
|
|
1744
|
+
if (isDateType(type)) return "java.util.Date";
|
|
1745
|
+
if ([
|
|
1746
|
+
"String",
|
|
1747
|
+
"Boolean",
|
|
1748
|
+
"Double",
|
|
1749
|
+
"Float",
|
|
1750
|
+
"Int",
|
|
1751
|
+
"Long",
|
|
1752
|
+
"ByteArray"
|
|
1753
|
+
].includes(type)) return type;
|
|
1754
|
+
throw new Error(`unsupported kotlin glue type ${type}`);
|
|
1755
|
+
};
|
|
1612
1756
|
const mentions = (type, match) => {
|
|
1613
1757
|
let rest = type;
|
|
1614
1758
|
while (rest.endsWith("?")) rest = rest.slice(0, -1);
|
|
@@ -1628,25 +1772,25 @@ function renderKotlinSourceGlue(sourceId, contract) {
|
|
|
1628
1772
|
const needsEvents = contract.modules.some((module) => module.methods.some((method) => method.subscription));
|
|
1629
1773
|
const needsDate = nativeTypes.some((type) => mentions(type, isDateType));
|
|
1630
1774
|
const needsData = nativeTypes.some((type) => mentions(type, (leaf) => leaf === "ByteArray"));
|
|
1631
|
-
const value = (type,
|
|
1775
|
+
const value = (type, reader, path) => {
|
|
1632
1776
|
if (type.endsWith("?")) {
|
|
1633
|
-
return `(${
|
|
1634
|
-
}
|
|
1635
|
-
if (type === "String") return `readString(${
|
|
1636
|
-
if (type === "Boolean") return `readBoolean(${
|
|
1637
|
-
if (type === "Double") return `readDouble(${
|
|
1638
|
-
if (type === "Float") return `readDouble(${
|
|
1639
|
-
if (type === "Int") return `readInt(${
|
|
1640
|
-
if (type === "Long") return `readLong(${
|
|
1641
|
-
if (isDateType(type)) return `readDate(${
|
|
1642
|
-
if (type === "ByteArray") return `readData(${
|
|
1643
|
-
if (structsByName.has(type)) return `read${type}(${
|
|
1644
|
-
if (enumsByName.has(type)) return `read${type}(${
|
|
1777
|
+
return `(if (${reader}.peek() == ${tokenClass}.NULL) { ${reader}.nextNull(); null } else ${value(type.slice(0, -1), reader, path)})`;
|
|
1778
|
+
}
|
|
1779
|
+
if (type === "String") return `readString(${reader}, ${path})`;
|
|
1780
|
+
if (type === "Boolean") return `readBoolean(${reader}, ${path})`;
|
|
1781
|
+
if (type === "Double") return `readDouble(${reader}, ${path})`;
|
|
1782
|
+
if (type === "Float") return `readDouble(${reader}, ${path}).toFloat()`;
|
|
1783
|
+
if (type === "Int") return `readInt(${reader}, ${path})`;
|
|
1784
|
+
if (type === "Long") return `readLong(${reader}, ${path})`;
|
|
1785
|
+
if (isDateType(type)) return `readDate(${reader}, ${path})`;
|
|
1786
|
+
if (type === "ByteArray") return `readData(${reader}, ${path})`;
|
|
1787
|
+
if (structsByName.has(type)) return `read${type}(${reader}, ${path})`;
|
|
1788
|
+
if (enumsByName.has(type)) return `read${type}(${reader}, ${path})`;
|
|
1645
1789
|
if (type.startsWith("List<") && type.endsWith(">")) {
|
|
1646
|
-
return `readList(${
|
|
1790
|
+
return `readList(${reader}, ${path}) { itemReader, itemPath -> ${value(type.slice(5, -1), "itemReader", "itemPath")} }`;
|
|
1647
1791
|
}
|
|
1648
1792
|
if (type.startsWith("Map<String,") && type.endsWith(">")) {
|
|
1649
|
-
return `readMap(${
|
|
1793
|
+
return `readMap(${reader}, ${path}) { itemReader, itemPath -> ${value(type.slice(11, -1), "itemReader", "itemPath")} }`;
|
|
1650
1794
|
}
|
|
1651
1795
|
throw new Error(`unsupported kotlin glue type ${type}`);
|
|
1652
1796
|
};
|
|
@@ -1666,15 +1810,38 @@ function renderKotlinSourceGlue(sourceId, contract) {
|
|
|
1666
1810
|
}
|
|
1667
1811
|
return `JSONObject.wrap(${accessor})`;
|
|
1668
1812
|
};
|
|
1813
|
+
const cap = (name) => name.slice(0, 1).toUpperCase() + name.slice(1);
|
|
1669
1814
|
const structHelpers = contract.structs.flatMap((struct) => [
|
|
1670
|
-
`private fun read${struct.name}(
|
|
1671
|
-
|
|
1815
|
+
`private fun read${struct.name}(reader: ${readerClass}, path: String): ${qualified(struct.name)} {`,
|
|
1816
|
+
` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("$path: expected object")`,
|
|
1817
|
+
" reader.beginObject()",
|
|
1818
|
+
...struct.fields.flatMap((field) => {
|
|
1819
|
+
const declared = qualifiedType(field.nativeType);
|
|
1820
|
+
if (field.optional) return [` var ${field.name}: ${declared} = ${field.fallback}`];
|
|
1821
|
+
const zero = unboxedZero(field.nativeType);
|
|
1822
|
+
if (zero !== null) return [` var ${field.name} = ${zero}`, ` var has${cap(field.name)} = false`];
|
|
1823
|
+
const boxed = declared.endsWith("?") ? declared : `${declared}?`;
|
|
1824
|
+
return [` var ${field.name}: ${boxed} = null`, ...field.nativeType.endsWith("?") ? [` var has${cap(field.name)} = false`] : []];
|
|
1825
|
+
}),
|
|
1826
|
+
" while (reader.hasNext()) {",
|
|
1827
|
+
` when (reader.selectName(${struct.fields.map((field) => JSON.stringify(field.name)).join(", ")})) {`,
|
|
1828
|
+
...struct.fields.map((field, index) => {
|
|
1829
|
+
const fieldPath = `"$path.${field.name}"`;
|
|
1830
|
+
const read = `${field.name} = ${value(field.nativeType, "reader", fieldPath)}`;
|
|
1831
|
+
const needsFlag = !field.optional && (field.nativeType.endsWith("?") || unboxedZero(field.nativeType) !== null);
|
|
1832
|
+
return needsFlag ? ` ${index} -> { ${read}; has${cap(field.name)} = true }` : ` ${index} -> ${read}`;
|
|
1833
|
+
}),
|
|
1834
|
+
" else -> reader.skipValue()",
|
|
1835
|
+
" }",
|
|
1836
|
+
" }",
|
|
1837
|
+
" reader.endObject()",
|
|
1838
|
+
...struct.fields.flatMap((field) => !field.optional && unboxedZero(field.nativeType) !== null ? [` if (!has${cap(field.name)}) error("$path.${field.name}: required")`] : []),
|
|
1672
1839
|
` return ${qualified(struct.name)}(`,
|
|
1673
1840
|
...struct.fields.map((field) => {
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
return ` ${field.name} = ${
|
|
1841
|
+
if (field.optional) return ` ${field.name} = ${field.name},`;
|
|
1842
|
+
if (field.nativeType.endsWith("?")) return ` ${field.name} = (if (has${cap(field.name)}) ${field.name} else error("$path.${field.name}: required")),`;
|
|
1843
|
+
if (unboxedZero(field.nativeType) !== null) return ` ${field.name} = ${field.name},`;
|
|
1844
|
+
return ` ${field.name} = (${field.name} ?: error("$path.${field.name}: required")),`;
|
|
1678
1845
|
}),
|
|
1679
1846
|
" )",
|
|
1680
1847
|
"}",
|
|
@@ -1684,47 +1851,329 @@ function renderKotlinSourceGlue(sourceId, contract) {
|
|
|
1684
1851
|
"}"
|
|
1685
1852
|
]);
|
|
1686
1853
|
const enumHelpers = contract.enums.flatMap((entry) => [
|
|
1687
|
-
`private fun read${entry.name}(
|
|
1688
|
-
" val text = readString(
|
|
1854
|
+
`private fun read${entry.name}(reader: ${readerClass}, path: String): ${qualified(entry.name)} {`,
|
|
1855
|
+
" val text = readString(reader, path)",
|
|
1689
1856
|
` return try { ${qualified(entry.name)}.valueOf(text) } catch (e: IllegalArgumentException) { error("$path: expected one of ${entry.cases.map((item) => item.name).join(", ")}") }`,
|
|
1690
1857
|
"}"
|
|
1691
1858
|
]);
|
|
1692
1859
|
const source = [
|
|
1693
1860
|
"import androidx.annotation.Keep",
|
|
1694
|
-
...contract.views.length === 0 ? [] : [
|
|
1861
|
+
...contract.views.length === 0 ? [] : [
|
|
1862
|
+
"import androidx.compose.runtime.Composable",
|
|
1863
|
+
"import dev.onejs.one.OneNativeSourceKeyedState",
|
|
1864
|
+
"import dev.onejs.one.OneNativeSourceViewDispatch"
|
|
1865
|
+
],
|
|
1695
1866
|
"import dev.onejs.one.OneNativeSourceDispatch",
|
|
1696
1867
|
...needsEvents ? ["import dev.onejs.one.source.OneNativeSourceEvents"] : [],
|
|
1697
1868
|
"import org.json.JSONArray",
|
|
1698
1869
|
"import org.json.JSONObject",
|
|
1699
1870
|
"",
|
|
1700
|
-
|
|
1701
|
-
"
|
|
1702
|
-
"
|
|
1703
|
-
"
|
|
1871
|
+
`private enum class ${tokenClass} { BEGIN_OBJECT, END_OBJECT, BEGIN_ARRAY, END_ARRAY, STRING, NUMBER, BOOLEAN, NULL }`,
|
|
1872
|
+
"",
|
|
1873
|
+
"// a forward-only json reader over the props text: values decode straight",
|
|
1874
|
+
"// into typed props in one pass with no intermediate JSONObject. pure",
|
|
1875
|
+
"// kotlin with no java or android imports, so the same glue compiles for",
|
|
1876
|
+
"// kotlin/wasm.",
|
|
1877
|
+
`private class ${readerClass}(private val text: String) {`,
|
|
1878
|
+
" private var pos = 0",
|
|
1879
|
+
"",
|
|
1880
|
+
" private fun skipSpace() {",
|
|
1881
|
+
" while (pos < text.length) {",
|
|
1882
|
+
" when (text[pos]) {",
|
|
1883
|
+
" ' ', '\\t', '\\n', '\\r' -> pos++",
|
|
1884
|
+
" else -> return",
|
|
1885
|
+
" }",
|
|
1886
|
+
" }",
|
|
1887
|
+
" }",
|
|
1888
|
+
"",
|
|
1889
|
+
" private fun fail(message: String): Nothing = throw IllegalStateException(\"$message near offset $pos\")",
|
|
1890
|
+
"",
|
|
1891
|
+
` fun peek(): ${tokenClass} {`,
|
|
1892
|
+
" skipSpace()",
|
|
1893
|
+
" if (pos >= text.length) fail(\"unexpected end of json\")",
|
|
1894
|
+
" return when (text[pos]) {",
|
|
1895
|
+
` '{' -> ${tokenClass}.BEGIN_OBJECT`,
|
|
1896
|
+
` '}' -> ${tokenClass}.END_OBJECT`,
|
|
1897
|
+
` '[' -> ${tokenClass}.BEGIN_ARRAY`,
|
|
1898
|
+
` ']' -> ${tokenClass}.END_ARRAY`,
|
|
1899
|
+
` '"' -> ${tokenClass}.STRING`,
|
|
1900
|
+
` 't', 'f' -> ${tokenClass}.BOOLEAN`,
|
|
1901
|
+
` 'n' -> ${tokenClass}.NULL`,
|
|
1902
|
+
` '-', in '0'..'9' -> ${tokenClass}.NUMBER`,
|
|
1903
|
+
" else -> fail(\"unexpected character\")",
|
|
1904
|
+
" }",
|
|
1905
|
+
" }",
|
|
1906
|
+
"",
|
|
1907
|
+
" private fun expect(char: Char) {",
|
|
1908
|
+
" skipSpace()",
|
|
1909
|
+
` if (pos >= text.length || text[pos] != char) fail("expected '$char'")`,
|
|
1910
|
+
" pos++",
|
|
1911
|
+
" }",
|
|
1912
|
+
"",
|
|
1913
|
+
" fun beginObject() = expect('{')",
|
|
1914
|
+
" fun endObject() = expect('}')",
|
|
1915
|
+
" fun beginArray() = expect('[')",
|
|
1916
|
+
" fun endArray() = expect(']')",
|
|
1917
|
+
"",
|
|
1918
|
+
" // inside an object or array: true when another value follows, consuming",
|
|
1919
|
+
" // a comma first; false at the closing bracket.",
|
|
1920
|
+
" fun hasNext(): Boolean {",
|
|
1921
|
+
" skipSpace()",
|
|
1922
|
+
" if (pos >= text.length) fail(\"unexpected end of json\")",
|
|
1923
|
+
" if (text[pos] == ',') {",
|
|
1924
|
+
" pos++",
|
|
1925
|
+
" return true",
|
|
1926
|
+
" }",
|
|
1927
|
+
" return text[pos] != '}' && text[pos] != ']'",
|
|
1928
|
+
" }",
|
|
1929
|
+
"",
|
|
1930
|
+
" fun nextName(): String {",
|
|
1931
|
+
" skipSpace()",
|
|
1932
|
+
` if (pos >= text.length || text[pos] != '"') fail("expected a name")`,
|
|
1933
|
+
" val name = readString()",
|
|
1934
|
+
" skipSpace()",
|
|
1935
|
+
` if (pos >= text.length || text[pos] != ':') fail("expected ':'")`,
|
|
1936
|
+
" pos++",
|
|
1937
|
+
" return name",
|
|
1938
|
+
" }",
|
|
1939
|
+
"",
|
|
1940
|
+
" fun nextString(): String {",
|
|
1941
|
+
" skipSpace()",
|
|
1942
|
+
` if (pos >= text.length || text[pos] != '"') fail("expected a string")`,
|
|
1943
|
+
" return readString()",
|
|
1944
|
+
" }",
|
|
1945
|
+
"",
|
|
1946
|
+
" // matches an object name against the expected keys without allocating",
|
|
1947
|
+
" // it: the index of the match, or -1 for an unknown key. the name and",
|
|
1948
|
+
" // colon are consumed either way, so the caller skips the value on -1.",
|
|
1949
|
+
" fun selectName(vararg options: String): Int {",
|
|
1950
|
+
" skipSpace()",
|
|
1951
|
+
` if (pos >= text.length || text[pos] != '"') fail("expected a name")`,
|
|
1952
|
+
" var end = pos + 1",
|
|
1953
|
+
` while (end < text.length && text[end] != '"' && text[end] != '\\\\') end++`,
|
|
1954
|
+
" if (end < text.length && text[end] == '\\\\') {",
|
|
1955
|
+
" val decoded = readString()",
|
|
1956
|
+
" skipSpace()",
|
|
1957
|
+
` if (pos >= text.length || text[pos] != ':') fail("expected ':'")`,
|
|
1958
|
+
" pos++",
|
|
1959
|
+
" return options.indexOf(decoded)",
|
|
1960
|
+
" }",
|
|
1961
|
+
" if (end >= text.length) fail(\"unterminated string\")",
|
|
1962
|
+
" val length = end - pos - 1",
|
|
1963
|
+
" var index = -1",
|
|
1964
|
+
" for (i in options.indices) {",
|
|
1965
|
+
" if (options[i].length == length && text.regionMatches(pos + 1, options[i], 0, length)) { index = i; break }",
|
|
1966
|
+
" }",
|
|
1967
|
+
" pos = end + 1",
|
|
1968
|
+
" skipSpace()",
|
|
1969
|
+
` if (pos >= text.length || text[pos] != ':') fail("expected ':'")`,
|
|
1970
|
+
" pos++",
|
|
1971
|
+
" return index",
|
|
1972
|
+
" }",
|
|
1973
|
+
"",
|
|
1974
|
+
" private fun readString(): String {",
|
|
1975
|
+
" pos++ // the opening quote",
|
|
1976
|
+
" var end = pos",
|
|
1977
|
+
` while (end < text.length && text[end] != '"' && text[end] != '\\\\') end++`,
|
|
1978
|
+
` if (end < text.length && text[end] == '"') {`,
|
|
1979
|
+
" val out = text.substring(pos, end)",
|
|
1980
|
+
" pos = end + 1",
|
|
1981
|
+
" return out",
|
|
1982
|
+
" }",
|
|
1983
|
+
" val out = StringBuilder(text.substring(pos, end))",
|
|
1984
|
+
" pos = end",
|
|
1985
|
+
" while (true) {",
|
|
1986
|
+
" if (pos >= text.length) fail(\"unterminated string\")",
|
|
1987
|
+
" val char = text[pos++]",
|
|
1988
|
+
` if (char == '"') return out.toString()`,
|
|
1989
|
+
" if (char != '\\\\') {",
|
|
1990
|
+
" out.append(char)",
|
|
1991
|
+
" continue",
|
|
1992
|
+
" }",
|
|
1993
|
+
" if (pos >= text.length) fail(\"unterminated escape\")",
|
|
1994
|
+
" when (text[pos++]) {",
|
|
1995
|
+
` '"', '\\\\', '/' -> out.append(text[pos - 1])`,
|
|
1996
|
+
" 'b' -> out.append('\\b')",
|
|
1997
|
+
" 'f' -> out.append('\\u000C')",
|
|
1998
|
+
" 'n' -> out.append('\\n')",
|
|
1999
|
+
" 'r' -> out.append('\\r')",
|
|
2000
|
+
" 't' -> out.append('\\t')",
|
|
2001
|
+
" 'u' -> {",
|
|
2002
|
+
" if (pos + 4 > text.length) fail(\"bad unicode escape\")",
|
|
2003
|
+
" var code = 0",
|
|
2004
|
+
" for (i in 0 until 4) {",
|
|
2005
|
+
" val digit = text[pos + i].lowercaseChar()",
|
|
2006
|
+
` val value = if (digit in '0'..'9') digit - '0' else if (digit in 'a'..'f') digit - 'a' + 10 else fail("bad unicode escape")`,
|
|
2007
|
+
" code = code * 16 + value",
|
|
2008
|
+
" }",
|
|
2009
|
+
" pos += 4",
|
|
2010
|
+
" out.append(code.toChar())",
|
|
2011
|
+
" }",
|
|
2012
|
+
" else -> fail(\"bad escape\")",
|
|
2013
|
+
" }",
|
|
2014
|
+
" }",
|
|
2015
|
+
" }",
|
|
2016
|
+
"",
|
|
2017
|
+
" fun nextDouble(): Double {",
|
|
2018
|
+
" skipSpace()",
|
|
2019
|
+
" val start = pos",
|
|
2020
|
+
" while (pos < text.length && (text[pos] in '0'..'9' || text[pos] == '.' || text[pos] == 'e' || text[pos] == 'E' || text[pos] == '+' || text[pos] == '-')) pos++",
|
|
2021
|
+
" if (pos == start) fail(\"expected a number\")",
|
|
2022
|
+
" return text.substring(start, pos).toDoubleOrNull() ?: fail(\"bad number\")",
|
|
2023
|
+
" }",
|
|
2024
|
+
"",
|
|
2025
|
+
" // plain-digit fast paths: the value when the token at pos is an integer",
|
|
2026
|
+
" // in range, null when it needs the double slow path. nothing is",
|
|
2027
|
+
" // consumed on null, so the caller falls back to nextDouble.",
|
|
2028
|
+
" fun nextIntOrNull(): Int? {",
|
|
2029
|
+
" var i = pos",
|
|
2030
|
+
" val negative = i < text.length && text[i] == '-'",
|
|
2031
|
+
" if (negative) i++",
|
|
2032
|
+
" var value = 0L",
|
|
2033
|
+
" val digits = i",
|
|
2034
|
+
" while (i < text.length && text[i] in '0'..'9') {",
|
|
2035
|
+
" value = value * 10 + (text[i] - '0')",
|
|
2036
|
+
" if (value > 2147483648L) return null",
|
|
2037
|
+
" i++",
|
|
2038
|
+
" }",
|
|
2039
|
+
" if (i == digits) return null",
|
|
2040
|
+
" if (i < text.length && (text[i] == '.' || text[i] == 'e' || text[i] == 'E')) return null",
|
|
2041
|
+
" if (!negative && value > 2147483647L) return null",
|
|
2042
|
+
" pos = i",
|
|
2043
|
+
" return if (negative) -value.toInt() else value.toInt()",
|
|
2044
|
+
" }",
|
|
2045
|
+
"",
|
|
2046
|
+
" fun nextLongOrNull(): Long? {",
|
|
2047
|
+
" var i = pos",
|
|
2048
|
+
" val negative = i < text.length && text[i] == '-'",
|
|
2049
|
+
" if (negative) i++",
|
|
2050
|
+
" var value = 0L",
|
|
2051
|
+
" val digits = i",
|
|
2052
|
+
" while (i < text.length && text[i] in '0'..'9') {",
|
|
2053
|
+
" val digit = (text[i] - '0').toLong()",
|
|
2054
|
+
" if (value > (9007199254740991L - digit) / 10) return null",
|
|
2055
|
+
" value = value * 10 + digit",
|
|
2056
|
+
" i++",
|
|
2057
|
+
" }",
|
|
2058
|
+
" if (i == digits) return null",
|
|
2059
|
+
" if (i < text.length && (text[i] == '.' || text[i] == 'e' || text[i] == 'E')) return null",
|
|
2060
|
+
" pos = i",
|
|
2061
|
+
" return if (negative) -value else value",
|
|
2062
|
+
" }",
|
|
2063
|
+
"",
|
|
2064
|
+
" fun nextBoolean(): Boolean {",
|
|
2065
|
+
" skipSpace()",
|
|
2066
|
+
" if (text.startsWith(\"true\", pos)) {",
|
|
2067
|
+
" pos += 4",
|
|
2068
|
+
" return true",
|
|
2069
|
+
" }",
|
|
2070
|
+
" if (text.startsWith(\"false\", pos)) {",
|
|
2071
|
+
" pos += 5",
|
|
2072
|
+
" return false",
|
|
2073
|
+
" }",
|
|
2074
|
+
" fail(\"expected a boolean\")",
|
|
2075
|
+
" }",
|
|
2076
|
+
"",
|
|
2077
|
+
" fun nextNull() {",
|
|
2078
|
+
" skipSpace()",
|
|
2079
|
+
" if (!text.startsWith(\"null\", pos)) fail(\"expected null\")",
|
|
2080
|
+
" pos += 4",
|
|
2081
|
+
" }",
|
|
2082
|
+
"",
|
|
2083
|
+
" fun skipValue() {",
|
|
2084
|
+
" when (peek()) {",
|
|
2085
|
+
` ${tokenClass}.BEGIN_OBJECT -> {`,
|
|
2086
|
+
" beginObject()",
|
|
2087
|
+
" while (hasNext()) {",
|
|
2088
|
+
" nextName()",
|
|
2089
|
+
" skipValue()",
|
|
2090
|
+
" }",
|
|
2091
|
+
" endObject()",
|
|
2092
|
+
" }",
|
|
2093
|
+
` ${tokenClass}.BEGIN_ARRAY -> {`,
|
|
2094
|
+
" beginArray()",
|
|
2095
|
+
" while (hasNext()) {",
|
|
2096
|
+
" skipValue()",
|
|
2097
|
+
" }",
|
|
2098
|
+
" endArray()",
|
|
2099
|
+
" }",
|
|
2100
|
+
` ${tokenClass}.STRING -> nextString()`,
|
|
2101
|
+
` ${tokenClass}.NUMBER -> nextDouble()`,
|
|
2102
|
+
` ${tokenClass}.BOOLEAN -> nextBoolean()`,
|
|
2103
|
+
` ${tokenClass}.NULL -> nextNull()`,
|
|
2104
|
+
" else -> fail(\"unexpected value\")",
|
|
2105
|
+
" }",
|
|
2106
|
+
" }",
|
|
2107
|
+
"",
|
|
2108
|
+
" fun endDocument() {",
|
|
2109
|
+
" skipSpace()",
|
|
2110
|
+
" if (pos != text.length) fail(\"trailing characters\")",
|
|
2111
|
+
" }",
|
|
2112
|
+
"}",
|
|
2113
|
+
"",
|
|
2114
|
+
`private fun readString(reader: ${readerClass}, path: String): String {`,
|
|
2115
|
+
` if (reader.peek() != ${tokenClass}.STRING) error("$path: expected string")`,
|
|
2116
|
+
" return reader.nextString()",
|
|
2117
|
+
"}",
|
|
2118
|
+
`private fun readBoolean(reader: ${readerClass}, path: String): Boolean {`,
|
|
2119
|
+
` if (reader.peek() != ${tokenClass}.BOOLEAN) error("$path: expected boolean")`,
|
|
2120
|
+
" return reader.nextBoolean()",
|
|
2121
|
+
"}",
|
|
2122
|
+
`private fun readDouble(reader: ${readerClass}, path: String): Double {`,
|
|
2123
|
+
` if (reader.peek() != ${tokenClass}.NUMBER) error("$path: expected number")`,
|
|
2124
|
+
" val result = reader.nextDouble()",
|
|
1704
2125
|
" if (!result.isFinite()) error(\"$path: expected finite number\")",
|
|
1705
2126
|
" return result",
|
|
1706
2127
|
"}",
|
|
1707
|
-
|
|
1708
|
-
|
|
2128
|
+
`private fun readInt(reader: ${readerClass}, path: String): Int {`,
|
|
2129
|
+
` if (reader.peek() != ${tokenClass}.NUMBER) error("$path: expected number")`,
|
|
2130
|
+
" reader.nextIntOrNull()?.let { return it }",
|
|
2131
|
+
" val result = readDouble(reader, path)",
|
|
1709
2132
|
" if (result % 1.0 != 0.0 || result < Int.MIN_VALUE || result > Int.MAX_VALUE) error(\"$path: expected Int\")",
|
|
1710
2133
|
" return result.toInt()",
|
|
1711
2134
|
"}",
|
|
1712
|
-
|
|
1713
|
-
|
|
2135
|
+
`private fun readLong(reader: ${readerClass}, path: String): Long {`,
|
|
2136
|
+
` if (reader.peek() != ${tokenClass}.NUMBER) error("$path: expected number")`,
|
|
2137
|
+
" reader.nextLongOrNull()?.let { return it }",
|
|
2138
|
+
" val result = readDouble(reader, path)",
|
|
1714
2139
|
" if (result % 1.0 != 0.0 || result < -9007199254740991.0 || result > 9007199254740991.0) error(\"$path: expected safe Long\")",
|
|
1715
2140
|
" return result.toLong()",
|
|
1716
2141
|
"}",
|
|
1717
|
-
"
|
|
1718
|
-
"
|
|
1719
|
-
|
|
2142
|
+
"// the host sends true for each callback the element passed; any other",
|
|
2143
|
+
"// value reads as absent.",
|
|
2144
|
+
`private fun readOptBoolean(reader: ${readerClass}): Boolean {`,
|
|
2145
|
+
` if (reader.peek() != ${tokenClass}.BOOLEAN) {`,
|
|
2146
|
+
" reader.skipValue()",
|
|
2147
|
+
" return false",
|
|
2148
|
+
" }",
|
|
2149
|
+
" return reader.nextBoolean()",
|
|
1720
2150
|
"}",
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
"
|
|
2151
|
+
`private fun <T> readList(reader: ${readerClass}, path: String, read: (${readerClass}, String) -> T): List<T> {`,
|
|
2152
|
+
` if (reader.peek() != ${tokenClass}.BEGIN_ARRAY) error("$path: expected array")`,
|
|
2153
|
+
" reader.beginArray()",
|
|
2154
|
+
" val result = mutableListOf<T>()",
|
|
2155
|
+
" var index = 0",
|
|
2156
|
+
" while (reader.hasNext()) {",
|
|
2157
|
+
" result.add(read(reader, \"$path[$index]\"))",
|
|
2158
|
+
" index++",
|
|
2159
|
+
" }",
|
|
2160
|
+
" reader.endArray()",
|
|
2161
|
+
" return result",
|
|
2162
|
+
"}",
|
|
2163
|
+
`private fun <T> readMap(reader: ${readerClass}, path: String, read: (${readerClass}, String) -> T): Map<String, T> {`,
|
|
2164
|
+
` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("$path: expected object")`,
|
|
2165
|
+
" reader.beginObject()",
|
|
2166
|
+
" val result = mutableMapOf<String, T>()",
|
|
2167
|
+
" while (reader.hasNext()) {",
|
|
2168
|
+
" val key = reader.nextName()",
|
|
2169
|
+
" result[key] = read(reader, \"$path.$key\")",
|
|
2170
|
+
" }",
|
|
2171
|
+
" reader.endObject()",
|
|
2172
|
+
" return result",
|
|
1724
2173
|
"}",
|
|
1725
2174
|
...needsDate ? [
|
|
1726
|
-
|
|
1727
|
-
" val text = readString(
|
|
2175
|
+
`private fun readDate(reader: ${readerClass}, path: String): java.util.Date {`,
|
|
2176
|
+
" val text = readString(reader, path)",
|
|
1728
2177
|
` for (pattern in arrayOf("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "yyyy-MM-dd'T'HH:mm:ss'Z'")) {`,
|
|
1729
2178
|
" try {",
|
|
1730
2179
|
" return java.text.SimpleDateFormat(pattern, java.util.Locale.US).apply { timeZone = java.util.TimeZone.getTimeZone(\"UTC\"); isLenient = false }.parse(text) ?: error(\"$path: expected date\")",
|
|
@@ -1736,9 +2185,9 @@ function renderKotlinSourceGlue(sourceId, contract) {
|
|
|
1736
2185
|
` 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)`
|
|
1737
2186
|
] : [],
|
|
1738
2187
|
...needsData ? [
|
|
1739
|
-
|
|
2188
|
+
`private fun readData(reader: ${readerClass}, path: String): ByteArray {`,
|
|
1740
2189
|
" try {",
|
|
1741
|
-
" return android.util.Base64.decode(readString(
|
|
2190
|
+
" return android.util.Base64.decode(readString(reader, path), android.util.Base64.NO_WRAP)",
|
|
1742
2191
|
" } catch (decodeError: IllegalArgumentException) {",
|
|
1743
2192
|
" error(\"$path: expected base64 data\")",
|
|
1744
2193
|
" }",
|
|
@@ -1750,7 +2199,9 @@ function renderKotlinSourceGlue(sourceId, contract) {
|
|
|
1750
2199
|
`@Keep class OneNativeSource_${sourceId}: OneNativeSourceDispatch {`,
|
|
1751
2200
|
` override val contractHash = ${JSON.stringify(hash)}`,
|
|
1752
2201
|
" override suspend fun call(module: String, method: String, argsJson: String): String {",
|
|
1753
|
-
|
|
2202
|
+
` val reader = ${readerClass}(argsJson)`,
|
|
2203
|
+
` if (reader.peek() != ${tokenClass}.BEGIN_ARRAY) error("args must be a JSON array")`,
|
|
2204
|
+
" reader.beginArray()",
|
|
1754
2205
|
" return when (module) {",
|
|
1755
2206
|
...contract.modules.flatMap((module) => [
|
|
1756
2207
|
` ${JSON.stringify(`${sourceId}.${module.name}`)} -> when (method) {`,
|
|
@@ -1761,16 +2212,24 @@ function renderKotlinSourceGlue(sourceId, contract) {
|
|
|
1761
2212
|
const closure = subscription.payload.length === 0 ? `{ OneNativeSourceEvents.send(${JSON.stringify(channel)}, JSONArray().toString()) }` : `{ ${subscription.payload.map((_, index) => `a${index}`).join(", ")} -> OneNativeSourceEvents.send(${JSON.stringify(channel)}, JSONArray()${subscription.payload.map((arg, index) => `.put(${encode(arg.nativeType, `a${index}`)})`).join("")}.toString()) }`;
|
|
1762
2213
|
return [
|
|
1763
2214
|
` ${JSON.stringify(method.name)} -> {`,
|
|
1764
|
-
` require(
|
|
2215
|
+
` require(!reader.hasNext()) { "${sourceId}.${module.name}.${method.name} subscribes; it takes no arguments" }`,
|
|
2216
|
+
" reader.endArray()",
|
|
2217
|
+
" reader.endDocument()",
|
|
1765
2218
|
` ${qualified(module.name)}.${method.name}(${closure})`,
|
|
1766
2219
|
" \"null\"",
|
|
1767
2220
|
" }"
|
|
1768
2221
|
];
|
|
1769
2222
|
}
|
|
1770
|
-
const lines = [` ${JSON.stringify(method.name)} -> {
|
|
2223
|
+
const lines = [` ${JSON.stringify(method.name)} -> {`];
|
|
2224
|
+
const arityMessage = `${sourceId}.${module.name}.${method.name} expects ${method.parameters.length} arguments`;
|
|
1771
2225
|
method.parameters.forEach((parameter, index) => {
|
|
1772
|
-
lines.push(` val arg${index} = ${value(parameter.nativeType,
|
|
2226
|
+
lines.push(` val arg${index} = ${value(parameter.nativeType, "reader", JSON.stringify(`args[${index}]`))}`);
|
|
2227
|
+
if (index < method.parameters.length - 1) {
|
|
2228
|
+
lines.push(` require(reader.hasNext()) { ${JSON.stringify(arityMessage)} }`);
|
|
2229
|
+
}
|
|
1773
2230
|
});
|
|
2231
|
+
lines.push(` require(!reader.hasNext()) { ${JSON.stringify(arityMessage)} }`);
|
|
2232
|
+
lines.push(" reader.endArray()", " reader.endDocument()");
|
|
1774
2233
|
const call = `${qualified(module.name)}.${method.name}(${method.parameters.map((_, index) => `arg${index}`).join(", ")})`;
|
|
1775
2234
|
if (method.result === "void") {
|
|
1776
2235
|
lines.push(` ${call}`, " \"null\"");
|
|
@@ -1791,38 +2250,224 @@ function renderKotlinSourceGlue(sourceId, contract) {
|
|
|
1791
2250
|
" }",
|
|
1792
2251
|
"}",
|
|
1793
2252
|
"",
|
|
1794
|
-
...contract.views.length === 0 ? [] : viewGlue(sourceId, contract, hash, value, encode)
|
|
2253
|
+
...contract.views.length === 0 ? [] : viewGlue(sourceId, contract, hash, value, encode, qualifiedType)
|
|
1795
2254
|
].join("\n");
|
|
1796
2255
|
return {
|
|
1797
2256
|
hash,
|
|
1798
2257
|
source
|
|
1799
2258
|
};
|
|
1800
2259
|
}
|
|
1801
|
-
function viewGlue(sourceId, contract, hash, value, encode) {
|
|
2260
|
+
function viewGlue(sourceId, contract, hash, value, encode, qualifiedType) {
|
|
1802
2261
|
const qualifier = contract.packageName ? `${contract.packageName}.` : "";
|
|
2262
|
+
const cap = (name) => name.slice(0, 1).toUpperCase() + name.slice(1);
|
|
2263
|
+
const readerClass = `OneJsonReader_${sourceId}`;
|
|
2264
|
+
const tokenClass = `OneJsonToken_${sourceId}`;
|
|
2265
|
+
const SEQ_KEY = "\"\\$seq\"";
|
|
2266
|
+
const BASE_KEY = "\"\\$base\"";
|
|
2267
|
+
const LISTS_KEY = "\"\\$lists\"";
|
|
2268
|
+
const listElement = (type) => {
|
|
2269
|
+
const inner = type.endsWith("?") ? type.slice(0, -1) : type;
|
|
2270
|
+
return inner.startsWith("List<") && inner.endsWith(">") ? inner.slice(5, -1) : null;
|
|
2271
|
+
};
|
|
2272
|
+
const holders = contract.views.flatMap((view) => [
|
|
2273
|
+
`private class Held${view.name}_${sourceId} {`,
|
|
2274
|
+
...view.props.flatMap((prop) => {
|
|
2275
|
+
if (prop.callback) {
|
|
2276
|
+
return prop.nativeType.endsWith("?") ? [` var has${cap(prop.name)}: Boolean = false`] : [];
|
|
2277
|
+
}
|
|
2278
|
+
const declared = qualifiedType(prop.nativeType);
|
|
2279
|
+
if (prop.optional || prop.nativeType.endsWith("?")) return [` var ${prop.name}: ${declared} = null`];
|
|
2280
|
+
const zero = unboxedZero(prop.nativeType);
|
|
2281
|
+
if (zero !== null) return [` var ${prop.name}: ${declared} = ${zero}`];
|
|
2282
|
+
if (prop.nativeType === "String") return [` var ${prop.name}: String = ""`];
|
|
2283
|
+
return [` var ${prop.name}: ${declared}? = null`];
|
|
2284
|
+
}),
|
|
2285
|
+
"}"
|
|
2286
|
+
]);
|
|
2287
|
+
const mergers = contract.views.flatMap((view) => view.props.flatMap((prop) => {
|
|
2288
|
+
if (prop.callback) return [];
|
|
2289
|
+
const elem = listElement(prop.nativeType);
|
|
2290
|
+
if (elem === null) return [];
|
|
2291
|
+
const element = qualifiedType(elem);
|
|
2292
|
+
return [
|
|
2293
|
+
`private fun mergeLists${view.name}${cap(prop.name)}(reader: ${readerClass}, old: List<${element}>?, prop: String): List<${element}>? {`,
|
|
2294
|
+
` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("lists.$prop: expected object")`,
|
|
2295
|
+
" reader.beginObject()",
|
|
2296
|
+
" var length = -1",
|
|
2297
|
+
` var set: List<Pair<Int, ${element}>>? = null`,
|
|
2298
|
+
" while (reader.hasNext()) {",
|
|
2299
|
+
" when (reader.selectName(\"length\", \"set\")) {",
|
|
2300
|
+
` 0 -> length = readInt(reader, "lists.$prop.length")`,
|
|
2301
|
+
` 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 }`,
|
|
2302
|
+
" else -> reader.skipValue()",
|
|
2303
|
+
" }",
|
|
2304
|
+
" }",
|
|
2305
|
+
" reader.endObject()",
|
|
2306
|
+
" if (length < 0) error(\"lists.$prop: needs length\")",
|
|
2307
|
+
" val current = old ?: return null",
|
|
2308
|
+
" val entries = set ?: emptyList()",
|
|
2309
|
+
" for (j in 1 until entries.size) require(entries[j].first > entries[j - 1].first) { \"lists.$prop: set out of order\" }",
|
|
2310
|
+
` val fresh = ArrayList<${element}>(length)`,
|
|
2311
|
+
" var s = 0",
|
|
2312
|
+
" for (i in 0 until length) {",
|
|
2313
|
+
" if (s < entries.size && entries[s].first == i) { fresh.add(entries[s].second); s++ }",
|
|
2314
|
+
" else if (i < current.size) fresh.add(current[i])",
|
|
2315
|
+
" else return null",
|
|
2316
|
+
" }",
|
|
2317
|
+
" return fresh",
|
|
2318
|
+
"}"
|
|
2319
|
+
];
|
|
2320
|
+
}));
|
|
1803
2321
|
return [
|
|
2322
|
+
...holders,
|
|
2323
|
+
...mergers,
|
|
2324
|
+
"",
|
|
1804
2325
|
`@Keep class OneNativeSourceViews_${sourceId}: OneNativeSourceViewDispatch {`,
|
|
1805
2326
|
` override val contractHash = ${JSON.stringify(hash)}`,
|
|
2327
|
+
" override fun apply(view: String, propsJson: String, state: OneNativeSourceKeyedState): Boolean {",
|
|
2328
|
+
` val reader = ${readerClass}(propsJson)`,
|
|
2329
|
+
` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("props must be a JSON object")`,
|
|
2330
|
+
" reader.beginObject()",
|
|
2331
|
+
" when (view) {",
|
|
2332
|
+
...contract.views.flatMap((view) => {
|
|
2333
|
+
const holder = `Held${view.name}_${sourceId}`;
|
|
2334
|
+
const lists = view.props.filter((prop) => !prop.callback && listElement(prop.nativeType) !== null);
|
|
2335
|
+
const options = [
|
|
2336
|
+
SEQ_KEY,
|
|
2337
|
+
BASE_KEY,
|
|
2338
|
+
LISTS_KEY,
|
|
2339
|
+
...view.props.map((prop) => JSON.stringify(prop.name))
|
|
2340
|
+
].join(", ");
|
|
2341
|
+
return [
|
|
2342
|
+
` ${JSON.stringify(view.name)} -> {`,
|
|
2343
|
+
` var held = state.views[${JSON.stringify(view.name)}] as? ${holder}`,
|
|
2344
|
+
" var seq = -1",
|
|
2345
|
+
" var hasSeq = false",
|
|
2346
|
+
" var base = -1",
|
|
2347
|
+
" var hasBase = false",
|
|
2348
|
+
" var sawLists = false",
|
|
2349
|
+
...view.props.flatMap((prop) => {
|
|
2350
|
+
if (prop.callback) {
|
|
2351
|
+
return prop.nativeType.endsWith("?") ? [` var ${prop.name}Doc: Boolean? = null`] : [];
|
|
2352
|
+
}
|
|
2353
|
+
const declared = qualifiedType(prop.nativeType);
|
|
2354
|
+
if (prop.optional || prop.nativeType.endsWith("?")) {
|
|
2355
|
+
return [` var ${prop.name}: ${declared} = null`, ` var has${cap(prop.name)} = false`];
|
|
2356
|
+
}
|
|
2357
|
+
const zero = unboxedZero(prop.nativeType);
|
|
2358
|
+
if (zero !== null) return [` var ${prop.name} = ${zero}`, ` var has${cap(prop.name)} = false`];
|
|
2359
|
+
return [` var ${prop.name}: ${declared}? = null`];
|
|
2360
|
+
}),
|
|
2361
|
+
...lists.map((prop) => ` var ${prop.name}MergeFailed = false`),
|
|
2362
|
+
" while (reader.hasNext()) {",
|
|
2363
|
+
` when (reader.selectName(${options})) {`,
|
|
2364
|
+
" 0 -> { seq = readInt(reader, \"seq\"); hasSeq = true }",
|
|
2365
|
+
" 1 -> { base = readInt(reader, \"base\"); hasBase = true }",
|
|
2366
|
+
" 2 -> {",
|
|
2367
|
+
" sawLists = true",
|
|
2368
|
+
` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("lists: expected object")`,
|
|
2369
|
+
" reader.beginObject()",
|
|
2370
|
+
" while (reader.hasNext()) {",
|
|
2371
|
+
` when (reader.selectName(${lists.map((prop) => JSON.stringify(prop.name)).join(", ")})) {`,
|
|
2372
|
+
...lists.map((prop, listsIndex) => ` ${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)} }`),
|
|
2373
|
+
" else -> reader.skipValue()",
|
|
2374
|
+
" }",
|
|
2375
|
+
" }",
|
|
2376
|
+
" reader.endObject()",
|
|
2377
|
+
" }",
|
|
2378
|
+
...view.props.map((prop, index) => {
|
|
2379
|
+
const arm = index + 3;
|
|
2380
|
+
if (prop.callback) {
|
|
2381
|
+
return prop.nativeType.endsWith("?") ? ` ${arm} -> ${prop.name}Doc = readOptBoolean(reader)` : ` ${arm} -> reader.skipValue()`;
|
|
2382
|
+
}
|
|
2383
|
+
const path = JSON.stringify(`props.${prop.name}`);
|
|
2384
|
+
const read = `${prop.name} = ${value(prop.nativeType, "reader", path)}`;
|
|
2385
|
+
const needsFlag = prop.optional || prop.nativeType.endsWith("?") || unboxedZero(prop.nativeType) !== null;
|
|
2386
|
+
return needsFlag ? ` ${arm} -> { ${read}; has${cap(prop.name)} = true }` : ` ${arm} -> ${read}`;
|
|
2387
|
+
}),
|
|
2388
|
+
" else -> reader.skipValue()",
|
|
2389
|
+
" }",
|
|
2390
|
+
" }",
|
|
2391
|
+
" reader.endObject()",
|
|
2392
|
+
" reader.endDocument()",
|
|
2393
|
+
" if (hasBase && !hasSeq) error(\"seq: required\")",
|
|
2394
|
+
" if (sawLists && !hasBase) error(\"lists: needs a change document\")",
|
|
2395
|
+
" if (!hasBase) {",
|
|
2396
|
+
...view.props.flatMap((prop) => {
|
|
2397
|
+
if (prop.callback || prop.optional) return [];
|
|
2398
|
+
if (prop.nativeType.endsWith("?") || unboxedZero(prop.nativeType) !== null) {
|
|
2399
|
+
return [` if (!has${cap(prop.name)}) error("props.${prop.name}: required")`];
|
|
2400
|
+
}
|
|
2401
|
+
return [];
|
|
2402
|
+
}),
|
|
2403
|
+
` val h = held ?: ${holder}().also { state.views[${JSON.stringify(view.name)}] = it }`,
|
|
2404
|
+
...view.props.flatMap((prop) => {
|
|
2405
|
+
if (prop.callback) {
|
|
2406
|
+
return prop.nativeType.endsWith("?") ? [` h.has${cap(prop.name)} = ${prop.name}Doc == true`] : [];
|
|
2407
|
+
}
|
|
2408
|
+
if (prop.optional || prop.nativeType.endsWith("?") || unboxedZero(prop.nativeType) !== null) {
|
|
2409
|
+
return [` h.${prop.name} = ${prop.name}`];
|
|
2410
|
+
}
|
|
2411
|
+
return [` h.${prop.name} = (${prop.name} ?: error("props.${prop.name}: required"))`];
|
|
2412
|
+
}),
|
|
2413
|
+
" if (hasSeq) state.seq = seq",
|
|
2414
|
+
" state.revision++",
|
|
2415
|
+
" return true",
|
|
2416
|
+
" }",
|
|
2417
|
+
" if (seq == state.seq) return true",
|
|
2418
|
+
" val h = held",
|
|
2419
|
+
" if (base == state.seq && h != null) {",
|
|
2420
|
+
...view.props.flatMap((prop) => {
|
|
2421
|
+
if (prop.callback) {
|
|
2422
|
+
return prop.nativeType.endsWith("?") ? [` if (${prop.name}Doc != null) h.has${cap(prop.name)} = ${prop.name}Doc`] : [];
|
|
2423
|
+
}
|
|
2424
|
+
const mergeFailed = listElement(prop.nativeType) !== null ? [` if (${prop.name}MergeFailed) error("lists.${prop.name}: cannot merge")`] : [];
|
|
2425
|
+
if (prop.optional || prop.nativeType.endsWith("?") || unboxedZero(prop.nativeType) !== null) {
|
|
2426
|
+
return [...mergeFailed, ` if (has${cap(prop.name)}) h.${prop.name} = ${prop.name}`];
|
|
2427
|
+
}
|
|
2428
|
+
return [...mergeFailed, ` if (${prop.name} != null) h.${prop.name} = ${prop.name}`];
|
|
2429
|
+
}),
|
|
2430
|
+
" state.seq = seq",
|
|
2431
|
+
" state.revision++",
|
|
2432
|
+
" return true",
|
|
2433
|
+
" }",
|
|
2434
|
+
" return false",
|
|
2435
|
+
" }"
|
|
2436
|
+
];
|
|
2437
|
+
}),
|
|
2438
|
+
" else -> error(\"composable $view is not linked; rebuild the app\")",
|
|
2439
|
+
" }",
|
|
2440
|
+
" }",
|
|
1806
2441
|
" @Composable",
|
|
1807
|
-
" override fun Content(view: String,
|
|
1808
|
-
" val props = JSONObject(propsJson)",
|
|
2442
|
+
" override fun Content(view: String, state: OneNativeSourceKeyedState, emit: (String, String) -> Unit) {",
|
|
1809
2443
|
" when (view) {",
|
|
1810
|
-
...contract.views.flatMap((view) =>
|
|
1811
|
-
`
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
2444
|
+
...contract.views.flatMap((view) => {
|
|
2445
|
+
const holder = `Held${view.name}_${sourceId}`;
|
|
2446
|
+
return [
|
|
2447
|
+
` ${JSON.stringify(view.name)} -> {`,
|
|
2448
|
+
" // the revision read resubscribes this scope on every apply",
|
|
2449
|
+
" val revision = state.revision",
|
|
2450
|
+
` val held = state.views[${JSON.stringify(view.name)}] as? ${holder}`,
|
|
2451
|
+
" held?.let { h ->",
|
|
2452
|
+
` ${qualifier}${view.name}(`,
|
|
2453
|
+
...view.props.map((prop) => {
|
|
2454
|
+
if (prop.callback) {
|
|
2455
|
+
const args = prop.callback.map((_, index) => `a${index}`);
|
|
2456
|
+
const types = prop.callback.map((arg) => arg.nativeType);
|
|
2457
|
+
const body = `emit(${JSON.stringify(prop.name)}, JSONArray()${args.map((arg, index) => `.put(${encode(types[index], arg)})`).join("")}.toString())`;
|
|
2458
|
+
const lambda = `{ ${args.length > 0 ? `${args.join(", ")} -> ` : ""}${body} }`;
|
|
2459
|
+
return ` ${prop.name} = ${prop.nativeType.endsWith("?") ? `if (h.has${cap(prop.name)}) (${lambda}) else null` : lambda},`;
|
|
2460
|
+
}
|
|
2461
|
+
if (prop.optional || prop.nativeType.endsWith("?") || unboxedZero(prop.nativeType) !== null || prop.nativeType === "String") {
|
|
2462
|
+
return ` ${prop.name} = h.${prop.name},`;
|
|
2463
|
+
}
|
|
2464
|
+
return ` ${prop.name} = h.${prop.name}!!,`;
|
|
2465
|
+
}),
|
|
2466
|
+
" )",
|
|
2467
|
+
" }",
|
|
2468
|
+
" }"
|
|
2469
|
+
];
|
|
2470
|
+
}),
|
|
1826
2471
|
" else -> error(\"composable $view is not linked; rebuild the app\")",
|
|
1827
2472
|
" }",
|
|
1828
2473
|
" }",
|