vxrn 2.13.9 → 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
|
@@ -1232,11 +1232,12 @@ export function nativeSourceContract(file: string, source: string): NativeSource
|
|
|
1232
1232
|
'}',
|
|
1233
1233
|
].join('\n')))
|
|
1234
1234
|
declarations.push(...views.map((view) =>
|
|
1235
|
-
`export declare function ${view.name}(props: {${view.props.map((prop) => ` ${prop.name}${prop.optional ? '?' : ''}: ${prop.type}
|
|
1235
|
+
`export declare function ${view.name}(props: {${view.props.map((prop) => ` ${prop.name}${prop.optional ? '?' : ''}: ${prop.type};`).join('')} style?: StyleProp<ViewStyle> }): ReactElement`
|
|
1236
1236
|
))
|
|
1237
1237
|
if (defaultView) {
|
|
1238
1238
|
declarations.unshift("export default function SwiftPackage(props: Record<string, unknown>): ReactElement")
|
|
1239
1239
|
}
|
|
1240
|
+
if (views.length > 0) declarations.unshift("import type { StyleProp, ViewStyle } from 'react-native'")
|
|
1240
1241
|
if (defaultView || views.length > 0) declarations.unshift("import type { ReactElement } from 'react'")
|
|
1241
1242
|
const declaration = `${GENERATED_HEADER}${declarations.join('\n\n')}\n`
|
|
1242
1243
|
return { language, packageName, modules, views, structs, enums, defaultView, hash, declaration }
|
|
@@ -1449,37 +1450,175 @@ export function renderSwiftSourceGlue(
|
|
|
1449
1450
|
' }',
|
|
1450
1451
|
]
|
|
1451
1452
|
// builds an imported view from the host's json props; a callback reports its
|
|
1452
|
-
// arguments to the host as a json array.
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
'
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1453
|
+
// arguments to the host as a json array. the host holds each view's decoded
|
|
1454
|
+
// props between documents: a full document builds them, a change document
|
|
1455
|
+
// (keyed props, see wireProps.native.ts) edits them, and one that does not
|
|
1456
|
+
// follow the held `$seq` is refused so the host can ask for a full document.
|
|
1457
|
+
const renderViews = (): string[] => {
|
|
1458
|
+
const notLinked = ' default: throw NSError(domain: "OneNativeSource", code: 1, userInfo: [NSLocalizedDescriptionKey: "swift view \\(name) is not linked; rebuild the app"])'
|
|
1459
|
+
// the optional callbacks ride as ordinary Bool? props
|
|
1460
|
+
const fieldsOf = (view: NativeSourceView) =>
|
|
1461
|
+
view.props
|
|
1462
|
+
.filter((prop) => !prop.callback || prop.optional)
|
|
1463
|
+
.map((prop, index) => {
|
|
1464
|
+
const type = prop.callback ? 'Bool?' : prop.nativeType
|
|
1465
|
+
const optional = type.endsWith('?')
|
|
1466
|
+
const list = /^\[(.*)\]\??$/.exec(type)
|
|
1467
|
+
return {
|
|
1468
|
+
name: prop.name,
|
|
1469
|
+
type,
|
|
1470
|
+
optional,
|
|
1471
|
+
list: list && !list[1].includes(':') ? list[1] : null,
|
|
1472
|
+
key: `p${index}`,
|
|
1473
|
+
}
|
|
1474
|
+
})
|
|
1475
|
+
return [
|
|
1476
|
+
' private struct OneListPatch<E: Decodable>: Decodable {',
|
|
1477
|
+
' let length: Int',
|
|
1478
|
+
' let set: [(Int, E)]',
|
|
1479
|
+
' private struct Entry: Decodable {',
|
|
1480
|
+
' let index: Int',
|
|
1481
|
+
' let value: E',
|
|
1482
|
+
' init(from decoder: Decoder) throws {',
|
|
1483
|
+
' var c = try decoder.unkeyedContainer()',
|
|
1484
|
+
' index = try c.decode(Int.self)',
|
|
1485
|
+
' value = try c.decode(E.self)',
|
|
1486
|
+
' }',
|
|
1487
|
+
' }',
|
|
1488
|
+
' private enum CodingKeys: String, CodingKey { case length, set }',
|
|
1489
|
+
' init(from decoder: Decoder) throws {',
|
|
1490
|
+
' let c = try decoder.container(keyedBy: CodingKeys.self)',
|
|
1491
|
+
' length = try c.decode(Int.self, forKey: .length)',
|
|
1492
|
+
' set = try c.decode([Entry].self, forKey: .set).map { ($0.index, $0.value) }',
|
|
1493
|
+
' }',
|
|
1494
|
+
' }',
|
|
1495
|
+
// truncate to the new length, replace held indices, append the rest in order
|
|
1496
|
+
' private nonisolated static func patch<E>(_ list: inout [E], _ patch: OneListPatch<E>) -> Bool {',
|
|
1497
|
+
' if patch.length < 0 { return false }',
|
|
1498
|
+
' if list.count > patch.length { list.removeLast(list.count - patch.length) }',
|
|
1499
|
+
' for (index, value) in patch.set {',
|
|
1500
|
+
' if index < list.count { list[index] = value } else if index == list.count && index < patch.length { list.append(value) } else { return false }',
|
|
1501
|
+
' }',
|
|
1502
|
+
' return list.count == patch.length',
|
|
1503
|
+
' }',
|
|
1504
|
+
' private nonisolated static func missing(_ prop: String) -> Error {',
|
|
1505
|
+
' NSError(domain: "OneNativeSource", code: 1, userInfo: [NSLocalizedDescriptionKey: "missing prop \\(prop)"])',
|
|
1506
|
+
' }',
|
|
1507
|
+
...views.flatMap((view) => {
|
|
1508
|
+
const fields = fieldsOf(view)
|
|
1509
|
+
const lists = fields.filter((field) => field.list)
|
|
1510
|
+
return [
|
|
1511
|
+
// one pass over the document text: an absent key is nil and a present
|
|
1512
|
+
// one carries its value, which may itself be null for an optional type
|
|
1513
|
+
...(lists.length > 0
|
|
1514
|
+
? [
|
|
1515
|
+
` private struct Lists_${view.name}: Decodable {`,
|
|
1516
|
+
...lists.map((field) => ` let ${field.key}: OneListPatch<${field.list}>?`),
|
|
1517
|
+
` private enum CodingKeys: String, CodingKey { ${lists.map((field) => `case ${field.key} = ${JSON.stringify(field.name)}`).join('; ')} }`,
|
|
1518
|
+
' }',
|
|
1519
|
+
]
|
|
1520
|
+
: []),
|
|
1521
|
+
` private struct Doc_${view.name}: Decodable {`,
|
|
1522
|
+
' let seq: Int',
|
|
1523
|
+
' let base: Int?',
|
|
1524
|
+
...(lists.length > 0 ? [` let lists: Lists_${view.name}?`] : []),
|
|
1525
|
+
...fields.map((field) => ` let ${field.key}: Optional<${field.type}>`),
|
|
1526
|
+
` private enum CodingKeys: String, CodingKey { ${['case seq = "$seq"', 'case base = "$base"', 'case lists = "$lists"', ...fields.map((field) => `case ${field.key} = ${JSON.stringify(field.name)}`)].join('; ')} }`,
|
|
1527
|
+
' init(from decoder: Decoder) throws {',
|
|
1528
|
+
' let c = try decoder.container(keyedBy: CodingKeys.self)',
|
|
1529
|
+
' seq = try c.decodeIfPresent(Int.self, forKey: .seq) ?? 0',
|
|
1530
|
+
' base = try c.decodeIfPresent(Int.self, forKey: .base)',
|
|
1531
|
+
...(lists.length > 0 ? [` lists = try c.decodeIfPresent(Lists_${view.name}.self, forKey: .lists)`] : []),
|
|
1532
|
+
...fields.map(
|
|
1533
|
+
(field) =>
|
|
1534
|
+
` ${field.key} = try c.contains(.${field.key}) ? Optional<${field.type}>.some(c.decode(${field.type}.self, forKey: .${field.key})) : Optional<${field.type}>.none`
|
|
1535
|
+
),
|
|
1536
|
+
' }',
|
|
1537
|
+
' }',
|
|
1538
|
+
` private final class Held_${view.name} {`,
|
|
1539
|
+
' var oneSeq: Int',
|
|
1540
|
+
...fields.map((field) => ` var \`${field.name}\`: ${field.type}`),
|
|
1541
|
+
` init(_ d: Doc_${view.name}) throws {`,
|
|
1542
|
+
' oneSeq = d.seq',
|
|
1543
|
+
...fields.flatMap((field) =>
|
|
1544
|
+
field.optional
|
|
1545
|
+
? [` \`${field.name}\` = d.${field.key}.flatMap { $0 }`]
|
|
1546
|
+
: [
|
|
1547
|
+
` guard let ${field.key} = d.${field.key} else { throw ${bodyName}.missing(${JSON.stringify(field.name)}) }`,
|
|
1548
|
+
` \`${field.name}\` = ${field.key}`,
|
|
1549
|
+
]
|
|
1550
|
+
),
|
|
1551
|
+
' }',
|
|
1552
|
+
// all or nothing: lists are patched into copies first
|
|
1553
|
+
` func apply(_ d: Doc_${view.name}) -> Bool {`,
|
|
1554
|
+
' if d.base != oneSeq { return false }',
|
|
1555
|
+
...lists.flatMap((field) => [
|
|
1556
|
+
` var ${field.key} = \`${field.name}\``,
|
|
1557
|
+
` if let v = d.${field.key} { ${field.key} = v }`,
|
|
1558
|
+
` if let p = d.lists?.${field.key} {`,
|
|
1559
|
+
...(field.optional
|
|
1560
|
+
? [
|
|
1561
|
+
` guard var items = ${field.key} else { return false }`,
|
|
1562
|
+
` if !${bodyName}.patch(&items, p) { return false }`,
|
|
1563
|
+
` ${field.key} = items`,
|
|
1564
|
+
]
|
|
1565
|
+
: [` if !${bodyName}.patch(&${field.key}, p) { return false }`]),
|
|
1566
|
+
' }',
|
|
1567
|
+
]),
|
|
1568
|
+
...fields.map((field) =>
|
|
1569
|
+
field.list
|
|
1570
|
+
? ` \`${field.name}\` = ${field.key}`
|
|
1571
|
+
: ` if let v = d.${field.key} { \`${field.name}\` = v }`
|
|
1572
|
+
),
|
|
1573
|
+
' oneSeq = d.seq',
|
|
1574
|
+
' return true',
|
|
1575
|
+
' }',
|
|
1576
|
+
' }',
|
|
1577
|
+
]
|
|
1476
1578
|
}),
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1579
|
+
// nil asks the host for a full document
|
|
1580
|
+
' static func update(_ held: AnyObject?, _ name: String, _ text: String) throws -> AnyObject? {',
|
|
1581
|
+
' let data = Data(text.utf8)',
|
|
1582
|
+
' switch name {',
|
|
1583
|
+
...views.flatMap((view) => [
|
|
1584
|
+
` case ${JSON.stringify(view.name)}:`,
|
|
1585
|
+
` let doc = try decode(Doc_${view.name}.self, from: data)`,
|
|
1586
|
+
` if doc.base == nil { return try Held_${view.name}(doc) }`,
|
|
1587
|
+
` guard let h = held as? Held_${view.name}, h.apply(doc) else { return nil }`,
|
|
1588
|
+
' return h',
|
|
1589
|
+
]),
|
|
1590
|
+
notLinked,
|
|
1591
|
+
' }',
|
|
1592
|
+
' }',
|
|
1593
|
+
' static func build(_ held: AnyObject, _ name: String, _ emit: @escaping (String, String) -> Void) throws -> AnyView {',
|
|
1594
|
+
' switch name {',
|
|
1595
|
+
...views.flatMap((view) => [
|
|
1596
|
+
` case ${JSON.stringify(view.name)}:`,
|
|
1597
|
+
` 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)"]) }`,
|
|
1598
|
+
` return AnyView(${view.name}(`,
|
|
1599
|
+
...view.props.map((prop, index) => {
|
|
1600
|
+
const comma = index === view.props.length - 1 ? '' : ','
|
|
1601
|
+
const key = JSON.stringify(prop.name)
|
|
1602
|
+
if (prop.callback) {
|
|
1603
|
+
const params = prop.callback.map((arg, n) => `a${n}: ${arg.nativeType}`).join(', ')
|
|
1604
|
+
const lambda = `{ (${params}) -> Void in emit(${key}, args([${prop.callback.map((_, n) => `a${n}`).join(', ')}])) }`
|
|
1605
|
+
// the host sends true for each callback the element passed
|
|
1606
|
+
return ` ${prop.name}: ${prop.optional ? `props.\`${prop.name}\` == true ? ${lambda} : nil` : lambda}${comma}`
|
|
1607
|
+
}
|
|
1608
|
+
return ` ${prop.name}: props.\`${prop.name}\`${comma}`
|
|
1609
|
+
}),
|
|
1610
|
+
' ))',
|
|
1611
|
+
]),
|
|
1612
|
+
notLinked,
|
|
1613
|
+
' }',
|
|
1614
|
+
' }',
|
|
1615
|
+
// one-shot entry for hosts that keep no held props (peach)
|
|
1616
|
+
' static func view(_ name: String, _ propsJson: String, _ emit: @escaping (String, String) -> Void) throws -> AnyView {',
|
|
1617
|
+
' guard let held = try update(nil, name, propsJson) else { throw NSError(domain: "OneNativeSource", code: 1, userInfo: [NSLocalizedDescriptionKey: "\\(name) needs a full props document"]) }',
|
|
1618
|
+
' return try build(held, name, emit)',
|
|
1619
|
+
' }',
|
|
1620
|
+
]
|
|
1621
|
+
}
|
|
1483
1622
|
const source = [
|
|
1484
1623
|
'import Foundation',
|
|
1485
1624
|
'import SwiftUI',
|
|
@@ -1547,13 +1686,15 @@ export function renderSwiftSourceGlue(
|
|
|
1547
1686
|
' if let date = (try? isoDateFractional.parse(text)) ?? (try? isoDate.parse(text)) { return date }',
|
|
1548
1687
|
' throw DecodingError.dataCorruptedError(in: container, debugDescription: "expected ISO date")',
|
|
1549
1688
|
' }',
|
|
1550
|
-
' private static func decode<T: Decodable>(_
|
|
1551
|
-
' let data = try JSONSerialization.data(withJSONObject: value, options: [.fragmentsAllowed])',
|
|
1689
|
+
' private static func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T {',
|
|
1552
1690
|
' let decoder = JSONDecoder()',
|
|
1553
1691
|
' decoder.dateDecodingStrategy = .custom { try decodeDate($0) }',
|
|
1554
1692
|
' decoder.dataDecodingStrategy = .base64',
|
|
1555
1693
|
' return try decoder.decode(T.self, from: data)',
|
|
1556
1694
|
' }',
|
|
1695
|
+
' private static func decode<T: Decodable>(_ value: Any, as type: T.Type) throws -> T {',
|
|
1696
|
+
' try decode(T.self, from: try JSONSerialization.data(withJSONObject: value, options: [.fragmentsAllowed]))',
|
|
1697
|
+
' }',
|
|
1557
1698
|
' private static func encode(_ value: any Encodable) throws -> String {',
|
|
1558
1699
|
' let encoder = JSONEncoder()',
|
|
1559
1700
|
' encoder.dateEncodingStrategy = .iso8601',
|
|
@@ -1625,6 +1766,8 @@ export function renderSwiftSourceGlue(
|
|
|
1625
1766
|
' public required override init() {}',
|
|
1626
1767
|
` public var contractHash: String { ${bodyName}.hash }`,
|
|
1627
1768
|
` public func view(_ name: String, _ propsJson: String, _ emit: @escaping (String, String) -> Void) throws -> AnyView { try ${bodyName}.view(name, propsJson, emit) }`,
|
|
1769
|
+
` public func updateView(_ held: AnyObject?, _ name: String, _ document: String) throws -> AnyObject? { try ${bodyName}.update(held, name, document) }`,
|
|
1770
|
+
` public func buildView(_ held: AnyObject, _ name: String, _ emit: @escaping (String, String) -> Void) throws -> AnyView { try ${bodyName}.build(held, name, emit) }`,
|
|
1628
1771
|
'}',
|
|
1629
1772
|
] : []),
|
|
1630
1773
|
'#endif',
|
|
@@ -1633,6 +1776,17 @@ export function renderSwiftSourceGlue(
|
|
|
1633
1776
|
return { hash, source }
|
|
1634
1777
|
}
|
|
1635
1778
|
|
|
1779
|
+
// a dummy initializer for a required primitive local: unboxed, so the hot
|
|
1780
|
+
// decode loop never boxes values that are always overwritten or checked.
|
|
1781
|
+
function unboxedZero(type: string): string | null {
|
|
1782
|
+
if (type === 'Int') return '0'
|
|
1783
|
+
if (type === 'Long') return '0L'
|
|
1784
|
+
if (type === 'Double') return '0.0'
|
|
1785
|
+
if (type === 'Float') return '0f'
|
|
1786
|
+
if (type === 'Boolean') return 'false'
|
|
1787
|
+
return null
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1636
1790
|
export function renderKotlinSourceGlue(
|
|
1637
1791
|
sourceId: string,
|
|
1638
1792
|
contract: NativeSourceContract
|
|
@@ -1640,10 +1794,27 @@ export function renderKotlinSourceGlue(
|
|
|
1640
1794
|
if (contract.language !== 'kotlin') throw new Error('kotlin glue received a swift contract')
|
|
1641
1795
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(sourceId)) throw new Error(`invalid kotlin source id ${sourceId}`)
|
|
1642
1796
|
const hash = createHash('sha256').update(JSON.stringify({ sourceId, contract: contract.hash })).digest('hex')
|
|
1797
|
+
// private top-level classes share the package scope for redeclaration
|
|
1798
|
+
// checks (unlike private functions, which the compiler file-mangles), so
|
|
1799
|
+
// every glue file's reader carries the source id in its name.
|
|
1800
|
+
const readerClass = `OneJsonReader_${sourceId}`
|
|
1801
|
+
const tokenClass = `OneJsonToken_${sourceId}`
|
|
1643
1802
|
const structsByName = new Map(contract.structs.map((entry) => [entry.name, entry]))
|
|
1644
1803
|
const enumsByName = new Map(contract.enums.map((entry) => [entry.name, entry]))
|
|
1645
1804
|
const qualified = (name: string) => contract.packageName ? `${contract.packageName}.${name}` : name
|
|
1646
1805
|
const isDateType = (type: string) => type === 'Date' || type === 'java.util.Date'
|
|
1806
|
+
// a declared-variable spelling of a decoded type: struct, enum and date
|
|
1807
|
+
// leaves resolve against the author's package, since the glue file shares
|
|
1808
|
+
// no package or imports with it.
|
|
1809
|
+
const qualifiedType = (type: string): string => {
|
|
1810
|
+
if (type.endsWith('?')) return `${qualifiedType(type.slice(0, -1))}?`
|
|
1811
|
+
if (type.startsWith('List<') && type.endsWith('>')) return `List<${qualifiedType(type.slice(5, -1))}>`
|
|
1812
|
+
if (type.startsWith('Map<String,') && type.endsWith('>')) return `Map<String, ${qualifiedType(type.slice(11, -1))}>`
|
|
1813
|
+
if (structsByName.has(type) || enumsByName.has(type)) return qualified(type)
|
|
1814
|
+
if (isDateType(type)) return 'java.util.Date'
|
|
1815
|
+
if (['String', 'Boolean', 'Double', 'Float', 'Int', 'Long', 'ByteArray'].includes(type)) return type
|
|
1816
|
+
throw new Error(`unsupported kotlin glue type ${type}`)
|
|
1817
|
+
}
|
|
1647
1818
|
// kotlin/wasm cannot resolve the jvm/android/event-host symbols, so a
|
|
1648
1819
|
// contract that never mentions dates, bytes or subscriptions must not
|
|
1649
1820
|
// reference them. every struct helper is always emitted, so a flat scan
|
|
@@ -1676,28 +1847,30 @@ export function renderKotlinSourceGlue(
|
|
|
1676
1847
|
)
|
|
1677
1848
|
const needsDate = nativeTypes.some((type) => mentions(type, isDateType))
|
|
1678
1849
|
const needsData = nativeTypes.some((type) => mentions(type, (leaf) => leaf === 'ByteArray'))
|
|
1679
|
-
|
|
1850
|
+
// a decode expression for one value: the reader sits at the value's first
|
|
1851
|
+
// token and the expression consumes exactly it, so props decode in one
|
|
1852
|
+
// pass with no intermediate JSONObject.
|
|
1853
|
+
const value = (type: string, reader: string, path: string): string => {
|
|
1680
1854
|
if (type.endsWith('?')) {
|
|
1681
|
-
|
|
1682
|
-
return `(${input}).let { raw -> if (raw == null || raw == JSONObject.NULL) null else ${value(type.slice(0, -1), 'raw', path)} }`
|
|
1855
|
+
return `(if (${reader}.peek() == ${tokenClass}.NULL) { ${reader}.nextNull(); null } else ${value(type.slice(0, -1), reader, path)})`
|
|
1683
1856
|
}
|
|
1684
|
-
if (type === 'String') return `readString(${
|
|
1685
|
-
if (type === 'Boolean') return `readBoolean(${
|
|
1686
|
-
if (type === 'Double') return `readDouble(${
|
|
1687
|
-
if (type === 'Float') return `readDouble(${
|
|
1688
|
-
if (type === 'Int') return `readInt(${
|
|
1689
|
-
if (type === 'Long') return `readLong(${
|
|
1857
|
+
if (type === 'String') return `readString(${reader}, ${path})`
|
|
1858
|
+
if (type === 'Boolean') return `readBoolean(${reader}, ${path})`
|
|
1859
|
+
if (type === 'Double') return `readDouble(${reader}, ${path})`
|
|
1860
|
+
if (type === 'Float') return `readDouble(${reader}, ${path}).toFloat()`
|
|
1861
|
+
if (type === 'Int') return `readInt(${reader}, ${path})`
|
|
1862
|
+
if (type === 'Long') return `readLong(${reader}, ${path})`
|
|
1690
1863
|
// dates cross as ISO strings, bytes as base64; structs and enums read
|
|
1691
1864
|
// through the generated helpers below.
|
|
1692
|
-
if (isDateType(type)) return `readDate(${
|
|
1693
|
-
if (type === 'ByteArray') return `readData(${
|
|
1694
|
-
if (structsByName.has(type)) return `read${type}(${
|
|
1695
|
-
if (enumsByName.has(type)) return `read${type}(${
|
|
1865
|
+
if (isDateType(type)) return `readDate(${reader}, ${path})`
|
|
1866
|
+
if (type === 'ByteArray') return `readData(${reader}, ${path})`
|
|
1867
|
+
if (structsByName.has(type)) return `read${type}(${reader}, ${path})`
|
|
1868
|
+
if (enumsByName.has(type)) return `read${type}(${reader}, ${path})`
|
|
1696
1869
|
if (type.startsWith('List<') && type.endsWith('>')) {
|
|
1697
|
-
return `readList(${
|
|
1870
|
+
return `readList(${reader}, ${path}) { itemReader, itemPath -> ${value(type.slice(5, -1), 'itemReader', 'itemPath')} }`
|
|
1698
1871
|
}
|
|
1699
1872
|
if (type.startsWith('Map<String,') && type.endsWith('>')) {
|
|
1700
|
-
return `readMap(${
|
|
1873
|
+
return `readMap(${reader}, ${path}) { itemReader, itemPath -> ${value(type.slice(11, -1), 'itemReader', 'itemPath')} }`
|
|
1701
1874
|
}
|
|
1702
1875
|
throw new Error(`unsupported kotlin glue type ${type}`)
|
|
1703
1876
|
}
|
|
@@ -1721,17 +1894,50 @@ export function renderKotlinSourceGlue(
|
|
|
1721
1894
|
}
|
|
1722
1895
|
return `JSONObject.wrap(${accessor})`
|
|
1723
1896
|
}
|
|
1897
|
+
const cap = (name: string) => name.slice(0, 1).toUpperCase() + name.slice(1)
|
|
1724
1898
|
const structHelpers = contract.structs.flatMap((struct) => [
|
|
1725
|
-
`private fun read${struct.name}(
|
|
1726
|
-
|
|
1899
|
+
`private fun read${struct.name}(reader: ${readerClass}, path: String): ${qualified(struct.name)} {`,
|
|
1900
|
+
` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("$path: expected object")`,
|
|
1901
|
+
' reader.beginObject()',
|
|
1902
|
+
// required fields start null and fail at the constructor when the key
|
|
1903
|
+
// never arrived; a required nullable needs its own flag so an explicit
|
|
1904
|
+
// null does not read as missing. defaulted fields start at the default.
|
|
1905
|
+
...struct.fields.flatMap((field) => {
|
|
1906
|
+
const declared = qualifiedType(field.nativeType)
|
|
1907
|
+
if (field.optional) return [` var ${field.name}: ${declared} = ${field.fallback}`]
|
|
1908
|
+
const zero = unboxedZero(field.nativeType)
|
|
1909
|
+
if (zero !== null) return [` var ${field.name} = ${zero}`, ` var has${cap(field.name)} = false`]
|
|
1910
|
+
const boxed = declared.endsWith('?') ? declared : `${declared}?`
|
|
1911
|
+
return [
|
|
1912
|
+
` var ${field.name}: ${boxed} = null`,
|
|
1913
|
+
...(field.nativeType.endsWith('?') ? [` var has${cap(field.name)} = false`] : []),
|
|
1914
|
+
]
|
|
1915
|
+
}),
|
|
1916
|
+
' while (reader.hasNext()) {',
|
|
1917
|
+
` when (reader.selectName(${struct.fields.map((field) => JSON.stringify(field.name)).join(', ')})) {`,
|
|
1918
|
+
...struct.fields.map((field, index) => {
|
|
1919
|
+
const fieldPath = `"$path.${field.name}"`
|
|
1920
|
+
const read = `${field.name} = ${value(field.nativeType, 'reader', fieldPath)}`
|
|
1921
|
+
const needsFlag = !field.optional && (field.nativeType.endsWith('?') || unboxedZero(field.nativeType) !== null)
|
|
1922
|
+
return needsFlag
|
|
1923
|
+
? ` ${index} -> { ${read}; has${cap(field.name)} = true }`
|
|
1924
|
+
: ` ${index} -> ${read}`
|
|
1925
|
+
}),
|
|
1926
|
+
' else -> reader.skipValue()',
|
|
1927
|
+
' }',
|
|
1928
|
+
' }',
|
|
1929
|
+
' reader.endObject()',
|
|
1930
|
+
...struct.fields.flatMap((field) =>
|
|
1931
|
+
!field.optional && unboxedZero(field.nativeType) !== null
|
|
1932
|
+
? [` if (!has${cap(field.name)}) error("$path.${field.name}: required")`]
|
|
1933
|
+
: []
|
|
1934
|
+
),
|
|
1727
1935
|
` return ${qualified(struct.name)}(`,
|
|
1728
1936
|
...struct.fields.map((field) => {
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
: `(obj.opt(${JSON.stringify(field.name)}) ?: ${required})`
|
|
1734
|
-
return ` ${field.name} = ${value(field.nativeType, input, fieldPath)},`
|
|
1937
|
+
if (field.optional) return ` ${field.name} = ${field.name},`
|
|
1938
|
+
if (field.nativeType.endsWith('?')) return ` ${field.name} = (if (has${cap(field.name)}) ${field.name} else error("$path.${field.name}: required")),`
|
|
1939
|
+
if (unboxedZero(field.nativeType) !== null) return ` ${field.name} = ${field.name},`
|
|
1940
|
+
return ` ${field.name} = (${field.name} ?: error("$path.${field.name}: required")),`
|
|
1735
1941
|
}),
|
|
1736
1942
|
' )',
|
|
1737
1943
|
'}',
|
|
@@ -1743,50 +1949,328 @@ export function renderKotlinSourceGlue(
|
|
|
1743
1949
|
'}',
|
|
1744
1950
|
])
|
|
1745
1951
|
const enumHelpers = contract.enums.flatMap((entry) => [
|
|
1746
|
-
`private fun read${entry.name}(
|
|
1747
|
-
' val text = readString(
|
|
1952
|
+
`private fun read${entry.name}(reader: ${readerClass}, path: String): ${qualified(entry.name)} {`,
|
|
1953
|
+
' val text = readString(reader, path)',
|
|
1748
1954
|
` return try { ${qualified(entry.name)}.valueOf(text) } catch (e: IllegalArgumentException) { error("$path: expected one of ${entry.cases.map((item) => item.name).join(', ')}") }`,
|
|
1749
1955
|
'}',
|
|
1750
1956
|
])
|
|
1751
1957
|
const source = [
|
|
1752
1958
|
'import androidx.annotation.Keep',
|
|
1753
|
-
...(contract.views.length === 0 ? [] : ['import androidx.compose.runtime.Composable', 'import dev.onejs.one.OneNativeSourceViewDispatch']),
|
|
1959
|
+
...(contract.views.length === 0 ? [] : ['import androidx.compose.runtime.Composable', 'import dev.onejs.one.OneNativeSourceKeyedState', 'import dev.onejs.one.OneNativeSourceViewDispatch']),
|
|
1754
1960
|
'import dev.onejs.one.OneNativeSourceDispatch',
|
|
1755
1961
|
...(needsEvents ? ['import dev.onejs.one.source.OneNativeSourceEvents'] : []),
|
|
1756
1962
|
'import org.json.JSONArray',
|
|
1757
1963
|
'import org.json.JSONObject',
|
|
1758
1964
|
'',
|
|
1759
|
-
|
|
1760
|
-
'
|
|
1761
|
-
'
|
|
1762
|
-
'
|
|
1965
|
+
`private enum class ${tokenClass} { BEGIN_OBJECT, END_OBJECT, BEGIN_ARRAY, END_ARRAY, STRING, NUMBER, BOOLEAN, NULL }`,
|
|
1966
|
+
'',
|
|
1967
|
+
'// a forward-only json reader over the props text: values decode straight',
|
|
1968
|
+
'// into typed props in one pass with no intermediate JSONObject. pure',
|
|
1969
|
+
'// kotlin with no java or android imports, so the same glue compiles for',
|
|
1970
|
+
'// kotlin/wasm.',
|
|
1971
|
+
`private class ${readerClass}(private val text: String) {`,
|
|
1972
|
+
' private var pos = 0',
|
|
1973
|
+
'',
|
|
1974
|
+
' private fun skipSpace() {',
|
|
1975
|
+
' while (pos < text.length) {',
|
|
1976
|
+
' when (text[pos]) {',
|
|
1977
|
+
' \' \', \'\\t\', \'\\n\', \'\\r\' -> pos++',
|
|
1978
|
+
' else -> return',
|
|
1979
|
+
' }',
|
|
1980
|
+
' }',
|
|
1981
|
+
' }',
|
|
1982
|
+
'',
|
|
1983
|
+
' private fun fail(message: String): Nothing = throw IllegalStateException("$message near offset $pos")',
|
|
1984
|
+
'',
|
|
1985
|
+
` fun peek(): ${tokenClass} {`,
|
|
1986
|
+
' skipSpace()',
|
|
1987
|
+
' if (pos >= text.length) fail("unexpected end of json")',
|
|
1988
|
+
' return when (text[pos]) {',
|
|
1989
|
+
` '{' -> ${tokenClass}.BEGIN_OBJECT`,
|
|
1990
|
+
` '}' -> ${tokenClass}.END_OBJECT`,
|
|
1991
|
+
` '[' -> ${tokenClass}.BEGIN_ARRAY`,
|
|
1992
|
+
` ']' -> ${tokenClass}.END_ARRAY`,
|
|
1993
|
+
` '"' -> ${tokenClass}.STRING`,
|
|
1994
|
+
` 't', 'f' -> ${tokenClass}.BOOLEAN`,
|
|
1995
|
+
` 'n' -> ${tokenClass}.NULL`,
|
|
1996
|
+
` '-', in '0'..'9' -> ${tokenClass}.NUMBER`,
|
|
1997
|
+
' else -> fail("unexpected character")',
|
|
1998
|
+
' }',
|
|
1999
|
+
' }',
|
|
2000
|
+
'',
|
|
2001
|
+
' private fun expect(char: Char) {',
|
|
2002
|
+
' skipSpace()',
|
|
2003
|
+
' if (pos >= text.length || text[pos] != char) fail("expected \'$char\'")',
|
|
2004
|
+
' pos++',
|
|
2005
|
+
' }',
|
|
2006
|
+
'',
|
|
2007
|
+
' fun beginObject() = expect(\'{\')',
|
|
2008
|
+
' fun endObject() = expect(\'}\')',
|
|
2009
|
+
' fun beginArray() = expect(\'[\')',
|
|
2010
|
+
' fun endArray() = expect(\']\')',
|
|
2011
|
+
'',
|
|
2012
|
+
' // inside an object or array: true when another value follows, consuming',
|
|
2013
|
+
' // a comma first; false at the closing bracket.',
|
|
2014
|
+
' fun hasNext(): Boolean {',
|
|
2015
|
+
' skipSpace()',
|
|
2016
|
+
' if (pos >= text.length) fail("unexpected end of json")',
|
|
2017
|
+
' if (text[pos] == \',\') {',
|
|
2018
|
+
' pos++',
|
|
2019
|
+
' return true',
|
|
2020
|
+
' }',
|
|
2021
|
+
' return text[pos] != \'}\' && text[pos] != \']\'',
|
|
2022
|
+
' }',
|
|
2023
|
+
'',
|
|
2024
|
+
' fun nextName(): String {',
|
|
2025
|
+
' skipSpace()',
|
|
2026
|
+
' if (pos >= text.length || text[pos] != \'"\') fail("expected a name")',
|
|
2027
|
+
' val name = readString()',
|
|
2028
|
+
' skipSpace()',
|
|
2029
|
+
' if (pos >= text.length || text[pos] != \':\') fail("expected \':\'")',
|
|
2030
|
+
' pos++',
|
|
2031
|
+
' return name',
|
|
2032
|
+
' }',
|
|
2033
|
+
'',
|
|
2034
|
+
' fun nextString(): String {',
|
|
2035
|
+
' skipSpace()',
|
|
2036
|
+
' if (pos >= text.length || text[pos] != \'"\') fail("expected a string")',
|
|
2037
|
+
' return readString()',
|
|
2038
|
+
' }',
|
|
2039
|
+
'',
|
|
2040
|
+
' // matches an object name against the expected keys without allocating',
|
|
2041
|
+
' // it: the index of the match, or -1 for an unknown key. the name and',
|
|
2042
|
+
' // colon are consumed either way, so the caller skips the value on -1.',
|
|
2043
|
+
' fun selectName(vararg options: String): Int {',
|
|
2044
|
+
' skipSpace()',
|
|
2045
|
+
' if (pos >= text.length || text[pos] != \'"\') fail("expected a name")',
|
|
2046
|
+
' var end = pos + 1',
|
|
2047
|
+
' while (end < text.length && text[end] != \'"\' && text[end] != \'\\\\\') end++',
|
|
2048
|
+
' if (end < text.length && text[end] == \'\\\\\') {',
|
|
2049
|
+
' val decoded = readString()',
|
|
2050
|
+
' skipSpace()',
|
|
2051
|
+
' if (pos >= text.length || text[pos] != \':\') fail("expected \':\'")',
|
|
2052
|
+
' pos++',
|
|
2053
|
+
' return options.indexOf(decoded)',
|
|
2054
|
+
' }',
|
|
2055
|
+
' if (end >= text.length) fail("unterminated string")',
|
|
2056
|
+
' val length = end - pos - 1',
|
|
2057
|
+
' var index = -1',
|
|
2058
|
+
' for (i in options.indices) {',
|
|
2059
|
+
' if (options[i].length == length && text.regionMatches(pos + 1, options[i], 0, length)) { index = i; break }',
|
|
2060
|
+
' }',
|
|
2061
|
+
' pos = end + 1',
|
|
2062
|
+
' skipSpace()',
|
|
2063
|
+
' if (pos >= text.length || text[pos] != \':\') fail("expected \':\'")',
|
|
2064
|
+
' pos++',
|
|
2065
|
+
' return index',
|
|
2066
|
+
' }',
|
|
2067
|
+
'',
|
|
2068
|
+
' private fun readString(): String {',
|
|
2069
|
+
' pos++ // the opening quote',
|
|
2070
|
+
' var end = pos',
|
|
2071
|
+
' while (end < text.length && text[end] != \'"\' && text[end] != \'\\\\\') end++',
|
|
2072
|
+
' if (end < text.length && text[end] == \'"\') {',
|
|
2073
|
+
' val out = text.substring(pos, end)',
|
|
2074
|
+
' pos = end + 1',
|
|
2075
|
+
' return out',
|
|
2076
|
+
' }',
|
|
2077
|
+
' val out = StringBuilder(text.substring(pos, end))',
|
|
2078
|
+
' pos = end',
|
|
2079
|
+
' while (true) {',
|
|
2080
|
+
' if (pos >= text.length) fail("unterminated string")',
|
|
2081
|
+
' val char = text[pos++]',
|
|
2082
|
+
' if (char == \'"\') return out.toString()',
|
|
2083
|
+
' if (char != \'\\\\\') {',
|
|
2084
|
+
' out.append(char)',
|
|
2085
|
+
' continue',
|
|
2086
|
+
' }',
|
|
2087
|
+
' if (pos >= text.length) fail("unterminated escape")',
|
|
2088
|
+
' when (text[pos++]) {',
|
|
2089
|
+
' \'"\', \'\\\\\', \'/\' -> out.append(text[pos - 1])',
|
|
2090
|
+
' \'b\' -> out.append(\'\\b\')',
|
|
2091
|
+
' \'f\' -> out.append(\'\\u000C\')',
|
|
2092
|
+
' \'n\' -> out.append(\'\\n\')',
|
|
2093
|
+
' \'r\' -> out.append(\'\\r\')',
|
|
2094
|
+
' \'t\' -> out.append(\'\\t\')',
|
|
2095
|
+
' \'u\' -> {',
|
|
2096
|
+
' if (pos + 4 > text.length) fail("bad unicode escape")',
|
|
2097
|
+
' var code = 0',
|
|
2098
|
+
' for (i in 0 until 4) {',
|
|
2099
|
+
' val digit = text[pos + i].lowercaseChar()',
|
|
2100
|
+
' val value = if (digit in \'0\'..\'9\') digit - \'0\' else if (digit in \'a\'..\'f\') digit - \'a\' + 10 else fail("bad unicode escape")',
|
|
2101
|
+
' code = code * 16 + value',
|
|
2102
|
+
' }',
|
|
2103
|
+
' pos += 4',
|
|
2104
|
+
' out.append(code.toChar())',
|
|
2105
|
+
' }',
|
|
2106
|
+
' else -> fail("bad escape")',
|
|
2107
|
+
' }',
|
|
2108
|
+
' }',
|
|
2109
|
+
' }',
|
|
2110
|
+
'',
|
|
2111
|
+
' fun nextDouble(): Double {',
|
|
2112
|
+
' skipSpace()',
|
|
2113
|
+
' val start = pos',
|
|
2114
|
+
' while (pos < text.length && (text[pos] in \'0\'..\'9\' || text[pos] == \'.\' || text[pos] == \'e\' || text[pos] == \'E\' || text[pos] == \'+\' || text[pos] == \'-\')) pos++',
|
|
2115
|
+
' if (pos == start) fail("expected a number")',
|
|
2116
|
+
' return text.substring(start, pos).toDoubleOrNull() ?: fail("bad number")',
|
|
2117
|
+
' }',
|
|
2118
|
+
'',
|
|
2119
|
+
' // plain-digit fast paths: the value when the token at pos is an integer',
|
|
2120
|
+
' // in range, null when it needs the double slow path. nothing is',
|
|
2121
|
+
' // consumed on null, so the caller falls back to nextDouble.',
|
|
2122
|
+
' fun nextIntOrNull(): Int? {',
|
|
2123
|
+
' var i = pos',
|
|
2124
|
+
' val negative = i < text.length && text[i] == \'-\'',
|
|
2125
|
+
' if (negative) i++',
|
|
2126
|
+
' var value = 0L',
|
|
2127
|
+
' val digits = i',
|
|
2128
|
+
' while (i < text.length && text[i] in \'0\'..\'9\') {',
|
|
2129
|
+
' value = value * 10 + (text[i] - \'0\')',
|
|
2130
|
+
' if (value > 2147483648L) return null',
|
|
2131
|
+
' i++',
|
|
2132
|
+
' }',
|
|
2133
|
+
' if (i == digits) return null',
|
|
2134
|
+
' if (i < text.length && (text[i] == \'.\' || text[i] == \'e\' || text[i] == \'E\')) return null',
|
|
2135
|
+
' if (!negative && value > 2147483647L) return null',
|
|
2136
|
+
' pos = i',
|
|
2137
|
+
' return if (negative) -value.toInt() else value.toInt()',
|
|
2138
|
+
' }',
|
|
2139
|
+
'',
|
|
2140
|
+
' fun nextLongOrNull(): Long? {',
|
|
2141
|
+
' var i = pos',
|
|
2142
|
+
' val negative = i < text.length && text[i] == \'-\'',
|
|
2143
|
+
' if (negative) i++',
|
|
2144
|
+
' var value = 0L',
|
|
2145
|
+
' val digits = i',
|
|
2146
|
+
' while (i < text.length && text[i] in \'0\'..\'9\') {',
|
|
2147
|
+
' val digit = (text[i] - \'0\').toLong()',
|
|
2148
|
+
' if (value > (9007199254740991L - digit) / 10) return null',
|
|
2149
|
+
' value = value * 10 + digit',
|
|
2150
|
+
' i++',
|
|
2151
|
+
' }',
|
|
2152
|
+
' if (i == digits) return null',
|
|
2153
|
+
' if (i < text.length && (text[i] == \'.\' || text[i] == \'e\' || text[i] == \'E\')) return null',
|
|
2154
|
+
' pos = i',
|
|
2155
|
+
' return if (negative) -value else value',
|
|
2156
|
+
' }',
|
|
2157
|
+
'',
|
|
2158
|
+
' fun nextBoolean(): Boolean {',
|
|
2159
|
+
' skipSpace()',
|
|
2160
|
+
' if (text.startsWith("true", pos)) {',
|
|
2161
|
+
' pos += 4',
|
|
2162
|
+
' return true',
|
|
2163
|
+
' }',
|
|
2164
|
+
' if (text.startsWith("false", pos)) {',
|
|
2165
|
+
' pos += 5',
|
|
2166
|
+
' return false',
|
|
2167
|
+
' }',
|
|
2168
|
+
' fail("expected a boolean")',
|
|
2169
|
+
' }',
|
|
2170
|
+
'',
|
|
2171
|
+
' fun nextNull() {',
|
|
2172
|
+
' skipSpace()',
|
|
2173
|
+
' if (!text.startsWith("null", pos)) fail("expected null")',
|
|
2174
|
+
' pos += 4',
|
|
2175
|
+
' }',
|
|
2176
|
+
'',
|
|
2177
|
+
' fun skipValue() {',
|
|
2178
|
+
' when (peek()) {',
|
|
2179
|
+
` ${tokenClass}.BEGIN_OBJECT -> {`,
|
|
2180
|
+
' beginObject()',
|
|
2181
|
+
' while (hasNext()) {',
|
|
2182
|
+
' nextName()',
|
|
2183
|
+
' skipValue()',
|
|
2184
|
+
' }',
|
|
2185
|
+
' endObject()',
|
|
2186
|
+
' }',
|
|
2187
|
+
` ${tokenClass}.BEGIN_ARRAY -> {`,
|
|
2188
|
+
' beginArray()',
|
|
2189
|
+
' while (hasNext()) {',
|
|
2190
|
+
' skipValue()',
|
|
2191
|
+
' }',
|
|
2192
|
+
' endArray()',
|
|
2193
|
+
' }',
|
|
2194
|
+
` ${tokenClass}.STRING -> nextString()`,
|
|
2195
|
+
` ${tokenClass}.NUMBER -> nextDouble()`,
|
|
2196
|
+
` ${tokenClass}.BOOLEAN -> nextBoolean()`,
|
|
2197
|
+
` ${tokenClass}.NULL -> nextNull()`,
|
|
2198
|
+
' else -> fail("unexpected value")',
|
|
2199
|
+
' }',
|
|
2200
|
+
' }',
|
|
2201
|
+
'',
|
|
2202
|
+
' fun endDocument() {',
|
|
2203
|
+
' skipSpace()',
|
|
2204
|
+
' if (pos != text.length) fail("trailing characters")',
|
|
2205
|
+
' }',
|
|
2206
|
+
'}',
|
|
2207
|
+
'',
|
|
2208
|
+
`private fun readString(reader: ${readerClass}, path: String): String {`,
|
|
2209
|
+
` if (reader.peek() != ${tokenClass}.STRING) error("$path: expected string")`,
|
|
2210
|
+
' return reader.nextString()',
|
|
2211
|
+
'}',
|
|
2212
|
+
`private fun readBoolean(reader: ${readerClass}, path: String): Boolean {`,
|
|
2213
|
+
` if (reader.peek() != ${tokenClass}.BOOLEAN) error("$path: expected boolean")`,
|
|
2214
|
+
' return reader.nextBoolean()',
|
|
2215
|
+
'}',
|
|
2216
|
+
`private fun readDouble(reader: ${readerClass}, path: String): Double {`,
|
|
2217
|
+
` if (reader.peek() != ${tokenClass}.NUMBER) error("$path: expected number")`,
|
|
2218
|
+
' val result = reader.nextDouble()',
|
|
1763
2219
|
' if (!result.isFinite()) error("$path: expected finite number")',
|
|
1764
2220
|
' return result',
|
|
1765
2221
|
'}',
|
|
1766
|
-
|
|
1767
|
-
|
|
2222
|
+
`private fun readInt(reader: ${readerClass}, path: String): Int {`,
|
|
2223
|
+
` if (reader.peek() != ${tokenClass}.NUMBER) error("$path: expected number")`,
|
|
2224
|
+
' reader.nextIntOrNull()?.let { return it }',
|
|
2225
|
+
' val result = readDouble(reader, path)',
|
|
1768
2226
|
' if (result % 1.0 != 0.0 || result < Int.MIN_VALUE || result > Int.MAX_VALUE) error("$path: expected Int")',
|
|
1769
2227
|
' return result.toInt()',
|
|
1770
2228
|
'}',
|
|
1771
|
-
|
|
1772
|
-
|
|
2229
|
+
`private fun readLong(reader: ${readerClass}, path: String): Long {`,
|
|
2230
|
+
` if (reader.peek() != ${tokenClass}.NUMBER) error("$path: expected number")`,
|
|
2231
|
+
' reader.nextLongOrNull()?.let { return it }',
|
|
2232
|
+
' val result = readDouble(reader, path)',
|
|
1773
2233
|
' if (result % 1.0 != 0.0 || result < -9007199254740991.0 || result > 9007199254740991.0) error("$path: expected safe Long")',
|
|
1774
2234
|
' return result.toLong()',
|
|
1775
2235
|
'}',
|
|
1776
|
-
'
|
|
1777
|
-
'
|
|
1778
|
-
|
|
2236
|
+
'// the host sends true for each callback the element passed; any other',
|
|
2237
|
+
'// value reads as absent.',
|
|
2238
|
+
`private fun readOptBoolean(reader: ${readerClass}): Boolean {`,
|
|
2239
|
+
` if (reader.peek() != ${tokenClass}.BOOLEAN) {`,
|
|
2240
|
+
' reader.skipValue()',
|
|
2241
|
+
' return false',
|
|
2242
|
+
' }',
|
|
2243
|
+
' return reader.nextBoolean()',
|
|
2244
|
+
'}',
|
|
2245
|
+
`private fun <T> readList(reader: ${readerClass}, path: String, read: (${readerClass}, String) -> T): List<T> {`,
|
|
2246
|
+
` if (reader.peek() != ${tokenClass}.BEGIN_ARRAY) error("$path: expected array")`,
|
|
2247
|
+
' reader.beginArray()',
|
|
2248
|
+
' val result = mutableListOf<T>()',
|
|
2249
|
+
' var index = 0',
|
|
2250
|
+
' while (reader.hasNext()) {',
|
|
2251
|
+
' result.add(read(reader, "$path[$index]"))',
|
|
2252
|
+
' index++',
|
|
2253
|
+
' }',
|
|
2254
|
+
' reader.endArray()',
|
|
2255
|
+
' return result',
|
|
1779
2256
|
'}',
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
'
|
|
2257
|
+
`private fun <T> readMap(reader: ${readerClass}, path: String, read: (${readerClass}, String) -> T): Map<String, T> {`,
|
|
2258
|
+
` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("$path: expected object")`,
|
|
2259
|
+
' reader.beginObject()',
|
|
2260
|
+
' val result = mutableMapOf<String, T>()',
|
|
2261
|
+
' while (reader.hasNext()) {',
|
|
2262
|
+
' val key = reader.nextName()',
|
|
2263
|
+
' result[key] = read(reader, "$path.$key")',
|
|
2264
|
+
' }',
|
|
2265
|
+
' reader.endObject()',
|
|
2266
|
+
' return result',
|
|
1783
2267
|
'}',
|
|
1784
2268
|
// the encoder writes ISO with milliseconds and reads with or without
|
|
1785
2269
|
// them, so swift, kotlin and javascript dates all parse everywhere.
|
|
1786
2270
|
...(needsDate
|
|
1787
2271
|
? [
|
|
1788
|
-
|
|
1789
|
-
' val text = readString(
|
|
2272
|
+
`private fun readDate(reader: ${readerClass}, path: String): java.util.Date {`,
|
|
2273
|
+
' val text = readString(reader, path)',
|
|
1790
2274
|
' for (pattern in arrayOf("yyyy-MM-dd\'T\'HH:mm:ss.SSS\'Z\'", "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'")) {',
|
|
1791
2275
|
' try {',
|
|
1792
2276
|
' return java.text.SimpleDateFormat(pattern, java.util.Locale.US).apply { timeZone = java.util.TimeZone.getTimeZone("UTC"); isLenient = false }.parse(text) ?: error("$path: expected date")',
|
|
@@ -1800,9 +2284,9 @@ export function renderKotlinSourceGlue(
|
|
|
1800
2284
|
: []),
|
|
1801
2285
|
...(needsData
|
|
1802
2286
|
? [
|
|
1803
|
-
|
|
2287
|
+
`private fun readData(reader: ${readerClass}, path: String): ByteArray {`,
|
|
1804
2288
|
' try {',
|
|
1805
|
-
' return android.util.Base64.decode(readString(
|
|
2289
|
+
' return android.util.Base64.decode(readString(reader, path), android.util.Base64.NO_WRAP)',
|
|
1806
2290
|
' } catch (decodeError: IllegalArgumentException) {',
|
|
1807
2291
|
' error("$path: expected base64 data")',
|
|
1808
2292
|
' }',
|
|
@@ -1815,7 +2299,9 @@ export function renderKotlinSourceGlue(
|
|
|
1815
2299
|
`@Keep class OneNativeSource_${sourceId}: OneNativeSourceDispatch {`,
|
|
1816
2300
|
` override val contractHash = ${JSON.stringify(hash)}`,
|
|
1817
2301
|
' override suspend fun call(module: String, method: String, argsJson: String): String {',
|
|
1818
|
-
|
|
2302
|
+
` val reader = ${readerClass}(argsJson)`,
|
|
2303
|
+
` if (reader.peek() != ${tokenClass}.BEGIN_ARRAY) error("args must be a JSON array")`,
|
|
2304
|
+
' reader.beginArray()',
|
|
1819
2305
|
' return when (module) {',
|
|
1820
2306
|
...contract.modules.flatMap((module) => [
|
|
1821
2307
|
` ${JSON.stringify(`${sourceId}.${module.name}`)} -> when (method) {`,
|
|
@@ -1828,7 +2314,9 @@ export function renderKotlinSourceGlue(
|
|
|
1828
2314
|
: `{ ${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()) }`
|
|
1829
2315
|
return [
|
|
1830
2316
|
` ${JSON.stringify(method.name)} -> {`,
|
|
1831
|
-
` require(
|
|
2317
|
+
` require(!reader.hasNext()) { "${sourceId}.${module.name}.${method.name} subscribes; it takes no arguments" }`,
|
|
2318
|
+
' reader.endArray()',
|
|
2319
|
+
' reader.endDocument()',
|
|
1832
2320
|
` ${qualified(module.name)}.${method.name}(${closure})`,
|
|
1833
2321
|
' "null"',
|
|
1834
2322
|
' }',
|
|
@@ -1836,11 +2324,18 @@ export function renderKotlinSourceGlue(
|
|
|
1836
2324
|
}
|
|
1837
2325
|
const lines = [
|
|
1838
2326
|
` ${JSON.stringify(method.name)} -> {`,
|
|
1839
|
-
` require(args.length() == ${method.parameters.length}) { "${sourceId}.${module.name}.${method.name} expects ${method.parameters.length} arguments" }`,
|
|
1840
2327
|
]
|
|
2328
|
+
const arityMessage = `${sourceId}.${module.name}.${method.name} expects ${method.parameters.length} arguments`
|
|
1841
2329
|
method.parameters.forEach((parameter, index) => {
|
|
1842
|
-
lines.push(` val arg${index} = ${value(parameter.nativeType,
|
|
2330
|
+
lines.push(` val arg${index} = ${value(parameter.nativeType, 'reader', JSON.stringify(`args[${index}]`))}`)
|
|
2331
|
+
// positional args have no key loop to eat commas, so each arg but
|
|
2332
|
+
// the last requires one here; a short array fails as an arity miss.
|
|
2333
|
+
if (index < method.parameters.length - 1) {
|
|
2334
|
+
lines.push(` require(reader.hasNext()) { ${JSON.stringify(arityMessage)} }`)
|
|
2335
|
+
}
|
|
1843
2336
|
})
|
|
2337
|
+
lines.push(` require(!reader.hasNext()) { ${JSON.stringify(arityMessage)} }`)
|
|
2338
|
+
lines.push(' reader.endArray()', ' reader.endDocument()')
|
|
1844
2339
|
const call = `${qualified(module.name)}.${method.name}(${method.parameters.map((_, index) => `arg${index}`).join(', ')})`
|
|
1845
2340
|
if (method.result === 'void') {
|
|
1846
2341
|
lines.push(` ${call}`, ' "null"')
|
|
@@ -1861,7 +2356,7 @@ export function renderKotlinSourceGlue(
|
|
|
1861
2356
|
' }',
|
|
1862
2357
|
'}',
|
|
1863
2358
|
'',
|
|
1864
|
-
...(contract.views.length === 0 ? [] : viewGlue(sourceId, contract, hash, value, encode)),
|
|
2359
|
+
...(contract.views.length === 0 ? [] : viewGlue(sourceId, contract, hash, value, encode, qualifiedType)),
|
|
1865
2360
|
].join('\n')
|
|
1866
2361
|
return { hash, source }
|
|
1867
2362
|
}
|
|
@@ -1872,36 +2367,246 @@ function viewGlue(
|
|
|
1872
2367
|
sourceId: string,
|
|
1873
2368
|
contract: NativeSourceContract,
|
|
1874
2369
|
hash: string,
|
|
1875
|
-
value: (type: string,
|
|
1876
|
-
encode: (type: string, accessor: string) => string
|
|
2370
|
+
value: (type: string, reader: string, path: string) => string,
|
|
2371
|
+
encode: (type: string, accessor: string) => string,
|
|
2372
|
+
qualifiedType: (type: string) => string
|
|
1877
2373
|
): string[] {
|
|
1878
2374
|
const qualifier = contract.packageName ? `${contract.packageName}.` : ''
|
|
2375
|
+
const cap = (name: string) => name.slice(0, 1).toUpperCase() + name.slice(1)
|
|
2376
|
+
const readerClass = `OneJsonReader_${sourceId}`
|
|
2377
|
+
const tokenClass = `OneJsonToken_${sourceId}`
|
|
2378
|
+
// "$seq" etc as kotlin string literals: the dollar escapes or the compiler
|
|
2379
|
+
// reads it as a template of the seq/base locals.
|
|
2380
|
+
const SEQ_KEY = '"\\$seq"'
|
|
2381
|
+
const BASE_KEY = '"\\$base"'
|
|
2382
|
+
const LISTS_KEY = '"\\$lists"'
|
|
2383
|
+
const listElement = (type: string): string | null => {
|
|
2384
|
+
const inner = type.endsWith('?') ? type.slice(0, -1) : type
|
|
2385
|
+
return inner.startsWith('List<') && inner.endsWith('>') ? inner.slice(5, -1) : null
|
|
2386
|
+
}
|
|
2387
|
+
// one untyped holder per view, kept in the host-owned keyed state: full
|
|
2388
|
+
// documents fill every field, change documents merge into them, and every
|
|
2389
|
+
// composition renders from them, so a re-created composition shows the
|
|
2390
|
+
// same content without re-applying.
|
|
2391
|
+
const holders = contract.views.flatMap((view) => [
|
|
2392
|
+
`private class Held${view.name}_${sourceId} {`,
|
|
2393
|
+
...view.props.flatMap((prop) => {
|
|
2394
|
+
if (prop.callback) {
|
|
2395
|
+
return prop.nativeType.endsWith('?') ? [` var has${cap(prop.name)}: Boolean = false`] : []
|
|
2396
|
+
}
|
|
2397
|
+
const declared = qualifiedType(prop.nativeType)
|
|
2398
|
+
if (prop.optional || prop.nativeType.endsWith('?')) return [` var ${prop.name}: ${declared} = null`]
|
|
2399
|
+
const zero = unboxedZero(prop.nativeType)
|
|
2400
|
+
if (zero !== null) return [` var ${prop.name}: ${declared} = ${zero}`]
|
|
2401
|
+
if (prop.nativeType === 'String') return [` var ${prop.name}: String = ""`]
|
|
2402
|
+
return [` var ${prop.name}: ${declared}? = null`]
|
|
2403
|
+
}),
|
|
2404
|
+
'}',
|
|
2405
|
+
])
|
|
2406
|
+
// one $lists merger per view list prop, top-level so its temporaries can
|
|
2407
|
+
// never collide with prop-named locals the way branch-inline merge did.
|
|
2408
|
+
const mergers = contract.views.flatMap((view) =>
|
|
2409
|
+
view.props.flatMap((prop) => {
|
|
2410
|
+
if (prop.callback) return []
|
|
2411
|
+
const elem = listElement(prop.nativeType)
|
|
2412
|
+
if (elem === null) return []
|
|
2413
|
+
const element = qualifiedType(elem)
|
|
2414
|
+
return [
|
|
2415
|
+
`private fun mergeLists${view.name}${cap(prop.name)}(reader: ${readerClass}, old: List<${element}>?, prop: String): List<${element}>? {`,
|
|
2416
|
+
` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("lists.$prop: expected object")`,
|
|
2417
|
+
' reader.beginObject()',
|
|
2418
|
+
' var length = -1',
|
|
2419
|
+
` var set: List<Pair<Int, ${element}>>? = null`,
|
|
2420
|
+
' while (reader.hasNext()) {',
|
|
2421
|
+
' when (reader.selectName("length", "set")) {',
|
|
2422
|
+
` 0 -> length = readInt(reader, "lists.$prop.length")`,
|
|
2423
|
+
` 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 }`,
|
|
2424
|
+
' else -> reader.skipValue()',
|
|
2425
|
+
' }',
|
|
2426
|
+
' }',
|
|
2427
|
+
' reader.endObject()',
|
|
2428
|
+
' if (length < 0) error("lists.$prop: needs length")',
|
|
2429
|
+
// no held list, or a patch that does not fit it, is what a change
|
|
2430
|
+
// on a mismatched base looks like: return null so apply refuses and
|
|
2431
|
+
// the host resyncs. the caller errors when the base matches and the
|
|
2432
|
+
// merge still failed, which is a real protocol bug.
|
|
2433
|
+
' val current = old ?: return null',
|
|
2434
|
+
' val entries = set ?: emptyList()',
|
|
2435
|
+
' for (j in 1 until entries.size) require(entries[j].first > entries[j - 1].first) { "lists.$prop: set out of order" }',
|
|
2436
|
+
` val fresh = ArrayList<${element}>(length)`,
|
|
2437
|
+
' var s = 0',
|
|
2438
|
+
' for (i in 0 until length) {',
|
|
2439
|
+
' if (s < entries.size && entries[s].first == i) { fresh.add(entries[s].second); s++ }',
|
|
2440
|
+
' else if (i < current.size) fresh.add(current[i])',
|
|
2441
|
+
' else return null',
|
|
2442
|
+
' }',
|
|
2443
|
+
' return fresh',
|
|
2444
|
+
'}',
|
|
2445
|
+
]
|
|
2446
|
+
})
|
|
2447
|
+
)
|
|
1879
2448
|
return [
|
|
2449
|
+
...holders,
|
|
2450
|
+
...mergers,
|
|
2451
|
+
'',
|
|
1880
2452
|
`@Keep class OneNativeSourceViews_${sourceId}: OneNativeSourceViewDispatch {`,
|
|
1881
2453
|
` override val contractHash = ${JSON.stringify(hash)}`,
|
|
2454
|
+
' override fun apply(view: String, propsJson: String, state: OneNativeSourceKeyedState): Boolean {',
|
|
2455
|
+
` val reader = ${readerClass}(propsJson)`,
|
|
2456
|
+
` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("props must be a JSON object")`,
|
|
2457
|
+
' reader.beginObject()',
|
|
2458
|
+
' when (view) {',
|
|
2459
|
+
...contract.views.flatMap((view) => {
|
|
2460
|
+
const holder = `Held${view.name}_${sourceId}`
|
|
2461
|
+
const lists = view.props.filter((prop) => !prop.callback && listElement(prop.nativeType) !== null)
|
|
2462
|
+
const options = [SEQ_KEY, BASE_KEY, LISTS_KEY, ...view.props.map((prop) => JSON.stringify(prop.name))].join(', ')
|
|
2463
|
+
return [
|
|
2464
|
+
` ${JSON.stringify(view.name)} -> {`,
|
|
2465
|
+
` var held = state.views[${JSON.stringify(view.name)}] as? ${holder}`,
|
|
2466
|
+
// the envelope plus the props decoded from this document; presence
|
|
2467
|
+
// flags gate the change commit so absent props keep held values.
|
|
2468
|
+
' var seq = -1',
|
|
2469
|
+
' var hasSeq = false',
|
|
2470
|
+
' var base = -1',
|
|
2471
|
+
' var hasBase = false',
|
|
2472
|
+
' var sawLists = false',
|
|
2473
|
+
...view.props.flatMap((prop) => {
|
|
2474
|
+
if (prop.callback) {
|
|
2475
|
+
return prop.nativeType.endsWith('?') ? [` var ${prop.name}Doc: Boolean? = null`] : []
|
|
2476
|
+
}
|
|
2477
|
+
const declared = qualifiedType(prop.nativeType)
|
|
2478
|
+
if (prop.optional || prop.nativeType.endsWith('?')) {
|
|
2479
|
+
return [` var ${prop.name}: ${declared} = null`, ` var has${cap(prop.name)} = false`]
|
|
2480
|
+
}
|
|
2481
|
+
const zero = unboxedZero(prop.nativeType)
|
|
2482
|
+
if (zero !== null) return [` var ${prop.name} = ${zero}`, ` var has${cap(prop.name)} = false`]
|
|
2483
|
+
return [` var ${prop.name}: ${declared}? = null`]
|
|
2484
|
+
}),
|
|
2485
|
+
...lists.map((prop) => ` var ${prop.name}MergeFailed = false`),
|
|
2486
|
+
' while (reader.hasNext()) {',
|
|
2487
|
+
` when (reader.selectName(${options})) {`,
|
|
2488
|
+
' 0 -> { seq = readInt(reader, "seq"); hasSeq = true }',
|
|
2489
|
+
' 1 -> { base = readInt(reader, "base"); hasBase = true }',
|
|
2490
|
+
' 2 -> {',
|
|
2491
|
+
' sawLists = true',
|
|
2492
|
+
` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("lists: expected object")`,
|
|
2493
|
+
' reader.beginObject()',
|
|
2494
|
+
' while (reader.hasNext()) {',
|
|
2495
|
+
` when (reader.selectName(${lists.map((prop) => JSON.stringify(prop.name)).join(', ')})) {`,
|
|
2496
|
+
...lists.map((prop, listsIndex) =>
|
|
2497
|
+
` ${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)} }`
|
|
2498
|
+
),
|
|
2499
|
+
' else -> reader.skipValue()',
|
|
2500
|
+
' }',
|
|
2501
|
+
' }',
|
|
2502
|
+
' reader.endObject()',
|
|
2503
|
+
' }',
|
|
2504
|
+
...view.props.map((prop, index) => {
|
|
2505
|
+
const arm = index + 3
|
|
2506
|
+
if (prop.callback) {
|
|
2507
|
+
return prop.nativeType.endsWith('?')
|
|
2508
|
+
? ` ${arm} -> ${prop.name}Doc = readOptBoolean(reader)`
|
|
2509
|
+
: ` ${arm} -> reader.skipValue()`
|
|
2510
|
+
}
|
|
2511
|
+
const path = JSON.stringify(`props.${prop.name}`)
|
|
2512
|
+
const read = `${prop.name} = ${value(prop.nativeType, 'reader', path)}`
|
|
2513
|
+
const needsFlag = prop.optional || prop.nativeType.endsWith('?') || unboxedZero(prop.nativeType) !== null
|
|
2514
|
+
return needsFlag
|
|
2515
|
+
? ` ${arm} -> { ${read}; has${cap(prop.name)} = true }`
|
|
2516
|
+
: ` ${arm} -> ${read}`
|
|
2517
|
+
}),
|
|
2518
|
+
' else -> reader.skipValue()',
|
|
2519
|
+
' }',
|
|
2520
|
+
' }',
|
|
2521
|
+
' reader.endObject()',
|
|
2522
|
+
' reader.endDocument()',
|
|
2523
|
+
' if (hasBase && !hasSeq) error("seq: required")',
|
|
2524
|
+
' if (sawLists && !hasBase) error("lists: needs a change document")',
|
|
2525
|
+
// full documents fill every field; a change already applied is a
|
|
2526
|
+
// no-op, one based on the held seq merges and advances it, and
|
|
2527
|
+
// anything else refuses so the host asks for a resync.
|
|
2528
|
+
' if (!hasBase) {',
|
|
2529
|
+
...view.props.flatMap((prop) => {
|
|
2530
|
+
if (prop.callback || prop.optional) return []
|
|
2531
|
+
if (prop.nativeType.endsWith('?') || unboxedZero(prop.nativeType) !== null) {
|
|
2532
|
+
return [` if (!has${cap(prop.name)}) error("props.${prop.name}: required")`]
|
|
2533
|
+
}
|
|
2534
|
+
return []
|
|
2535
|
+
}),
|
|
2536
|
+
` val h = held ?: ${holder}().also { state.views[${JSON.stringify(view.name)}] = it }`,
|
|
2537
|
+
...view.props.flatMap((prop) => {
|
|
2538
|
+
if (prop.callback) {
|
|
2539
|
+
return prop.nativeType.endsWith('?') ? [` h.has${cap(prop.name)} = ${prop.name}Doc == true`] : []
|
|
2540
|
+
}
|
|
2541
|
+
if (prop.optional || prop.nativeType.endsWith('?') || unboxedZero(prop.nativeType) !== null) {
|
|
2542
|
+
return [` h.${prop.name} = ${prop.name}`]
|
|
2543
|
+
}
|
|
2544
|
+
return [` h.${prop.name} = (${prop.name} ?: error("props.${prop.name}: required"))`]
|
|
2545
|
+
}),
|
|
2546
|
+
' if (hasSeq) state.seq = seq',
|
|
2547
|
+
' state.revision++',
|
|
2548
|
+
' return true',
|
|
2549
|
+
' }',
|
|
2550
|
+
' if (seq == state.seq) return true',
|
|
2551
|
+
' val h = held',
|
|
2552
|
+
' if (base == state.seq && h != null) {',
|
|
2553
|
+
...view.props.flatMap((prop) => {
|
|
2554
|
+
if (prop.callback) {
|
|
2555
|
+
return prop.nativeType.endsWith('?') ? [` if (${prop.name}Doc != null) h.has${cap(prop.name)} = ${prop.name}Doc`] : []
|
|
2556
|
+
}
|
|
2557
|
+
// a failed merge on a matching base is a protocol bug: host and
|
|
2558
|
+
// js agree on the seq but not on the list. on a mismatched base
|
|
2559
|
+
// apply already refused, so this never fires for a resync.
|
|
2560
|
+
const mergeFailed =
|
|
2561
|
+
listElement(prop.nativeType) !== null
|
|
2562
|
+
? [` if (${prop.name}MergeFailed) error("lists.${prop.name}: cannot merge")`]
|
|
2563
|
+
: []
|
|
2564
|
+
if (prop.optional || prop.nativeType.endsWith('?') || unboxedZero(prop.nativeType) !== null) {
|
|
2565
|
+
return [...mergeFailed, ` if (has${cap(prop.name)}) h.${prop.name} = ${prop.name}`]
|
|
2566
|
+
}
|
|
2567
|
+
return [...mergeFailed, ` if (${prop.name} != null) h.${prop.name} = ${prop.name}`]
|
|
2568
|
+
}),
|
|
2569
|
+
' state.seq = seq',
|
|
2570
|
+
' state.revision++',
|
|
2571
|
+
' return true',
|
|
2572
|
+
' }',
|
|
2573
|
+
' return false',
|
|
2574
|
+
' }',
|
|
2575
|
+
]
|
|
2576
|
+
}),
|
|
2577
|
+
' else -> error("composable $view is not linked; rebuild the app")',
|
|
2578
|
+
' }',
|
|
2579
|
+
' }',
|
|
1882
2580
|
' @Composable',
|
|
1883
|
-
' override fun Content(view: String,
|
|
1884
|
-
' val props = JSONObject(propsJson)',
|
|
2581
|
+
' override fun Content(view: String, state: OneNativeSourceKeyedState, emit: (String, String) -> Unit) {',
|
|
1885
2582
|
' when (view) {',
|
|
1886
|
-
...contract.views.flatMap((view) =>
|
|
1887
|
-
`
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2583
|
+
...contract.views.flatMap((view) => {
|
|
2584
|
+
const holder = `Held${view.name}_${sourceId}`
|
|
2585
|
+
return [
|
|
2586
|
+
` ${JSON.stringify(view.name)} -> {`,
|
|
2587
|
+
' // the revision read resubscribes this scope on every apply',
|
|
2588
|
+
' val revision = state.revision',
|
|
2589
|
+
` val held = state.views[${JSON.stringify(view.name)}] as? ${holder}`,
|
|
2590
|
+
' held?.let { h ->',
|
|
2591
|
+
` ${qualifier}${view.name}(`,
|
|
2592
|
+
...view.props.map((prop) => {
|
|
2593
|
+
if (prop.callback) {
|
|
2594
|
+
const args = prop.callback.map((_, index) => `a${index}`)
|
|
2595
|
+
const types = prop.callback.map((arg) => arg.nativeType)
|
|
2596
|
+
const body = `emit(${JSON.stringify(prop.name)}, JSONArray()${args.map((arg, index) => `.put(${encode(types[index], arg)})`).join('')}.toString())`
|
|
2597
|
+
const lambda = `{ ${args.length > 0 ? `${args.join(', ')} -> ` : ''}${body} }`
|
|
2598
|
+
return ` ${prop.name} = ${prop.nativeType.endsWith('?') ? `if (h.has${cap(prop.name)}) (${lambda}) else null` : lambda},`
|
|
2599
|
+
}
|
|
2600
|
+
if (prop.optional || prop.nativeType.endsWith('?') || unboxedZero(prop.nativeType) !== null || prop.nativeType === 'String') {
|
|
2601
|
+
return ` ${prop.name} = h.${prop.name},`
|
|
2602
|
+
}
|
|
2603
|
+
return ` ${prop.name} = h.${prop.name}!!,`
|
|
2604
|
+
}),
|
|
2605
|
+
' )',
|
|
2606
|
+
' }',
|
|
2607
|
+
' }',
|
|
2608
|
+
]
|
|
2609
|
+
}),
|
|
1905
2610
|
' else -> error("composable $view is not linked; rebuild the app")',
|
|
1906
2611
|
' }',
|
|
1907
2612
|
' }',
|