lawspec 0.9.0 → 0.10.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.
Files changed (42) hide show
  1. package/API-MIGRATION.md +78 -2
  2. package/LANGUAGE.md +10 -7
  3. package/NATIVE-BINDINGS.md +1046 -0
  4. package/PRIMITIVES.md +1 -1
  5. package/README.md +73 -23
  6. package/REFINEMENTS.md +1 -1
  7. package/RELEASE-0.10.md +60 -0
  8. package/api.mjs +23 -3
  9. package/bin/lawspec.mjs +17 -4
  10. package/build.json +56 -36
  11. package/core.wasm +0 -0
  12. package/examples/native-payments/go/example/payments/domain.go +38 -0
  13. package/examples/native-payments/go/example/payments/native_generators_test.go +13 -0
  14. package/examples/native-payments/go/lawspec.json +136 -0
  15. package/examples/native-payments/haskell/lawspec.json +149 -0
  16. package/examples/native-payments/haskell/src/PaymentsDomain.hs +21 -0
  17. package/examples/native-payments/haskell/test/PaymentGenerators.hs +12 -0
  18. package/examples/native-payments/java/lawspec.json +165 -0
  19. package/examples/native-payments/java/src/main/java/domain/PaymentsDomain.java +32 -0
  20. package/examples/native-payments/java/src/test/java/domain/PaymentGenerators.java +16 -0
  21. package/examples/native-payments/javascript/lawspec.json +149 -0
  22. package/examples/native-payments/javascript/src/payments_domain.mjs +44 -0
  23. package/examples/native-payments/javascript/test/lawspec_generators.mjs +6 -0
  24. package/examples/native-payments/kotlin/lawspec.json +165 -0
  25. package/examples/native-payments/kotlin/src/main/kotlin/domain/PaymentsDomain.kt +17 -0
  26. package/examples/native-payments/kotlin/src/test/kotlin/domain/PaymentGenerators.kt +12 -0
  27. package/examples/native-payments/python/lawspec.json +149 -0
  28. package/examples/native-payments/python/src/payments_domain.py +60 -0
  29. package/examples/native-payments/python/tests/lawspec_generators.py +15 -0
  30. package/examples/native-payments/rust/lawspec.json +167 -0
  31. package/examples/native-payments/rust/src/domain.rs +37 -0
  32. package/examples/native-payments/rust/src/lib.rs +2 -0
  33. package/examples/native-payments/rust/tests/support/lawspec_generators.rs +11 -0
  34. package/examples/native-payments/typescript/lawspec.json +149 -0
  35. package/examples/native-payments/typescript/src/payments_domain.ts +42 -0
  36. package/examples/native-payments/typescript/test/lawspec_generators.ts +8 -0
  37. package/examples/specs/payments.lawspec +76 -0
  38. package/examples-command.mjs +5 -0
  39. package/files.mjs +6 -6
  40. package/index.d.ts +53 -2
  41. package/native-examples.mjs +81 -0
  42. package/package.json +2 -2
@@ -0,0 +1,167 @@
1
+ {
2
+ "version": 1,
3
+ "sources": [
4
+ "laws"
5
+ ],
6
+ "machineBits": 64,
7
+ "targets": [
8
+ {
9
+ "language": "rust",
10
+ "root": ".",
11
+ "nativeBindings": {
12
+ "rustCrate": "lawspec_example",
13
+ "types": [
14
+ {
15
+ "type": "example.payments::type::Currency",
16
+ "native": [
17
+ "crate",
18
+ "domain",
19
+ "CurrencyCode"
20
+ ],
21
+ "constructors": [
22
+ {
23
+ "constructor": "USD",
24
+ "native": [
25
+ "crate",
26
+ "domain",
27
+ "CurrencyCode",
28
+ "Dollars"
29
+ ],
30
+ "style": "unit",
31
+ "fields": []
32
+ },
33
+ {
34
+ "constructor": "EUR",
35
+ "native": [
36
+ "crate",
37
+ "domain",
38
+ "CurrencyCode",
39
+ "Euros"
40
+ ],
41
+ "style": "unit",
42
+ "fields": []
43
+ },
44
+ {
45
+ "constructor": "GBP",
46
+ "native": [
47
+ "crate",
48
+ "domain",
49
+ "CurrencyCode",
50
+ "Pounds"
51
+ ],
52
+ "style": "unit",
53
+ "fields": []
54
+ }
55
+ ]
56
+ },
57
+ {
58
+ "type": "example.payments::type::Money",
59
+ "native": [
60
+ "crate",
61
+ "domain",
62
+ "Price"
63
+ ],
64
+ "constructors": [
65
+ {
66
+ "constructor": "Money",
67
+ "native": [
68
+ "crate",
69
+ "domain",
70
+ "Price"
71
+ ],
72
+ "style": "record",
73
+ "fields": [
74
+ {
75
+ "field": "amount",
76
+ "native": "major"
77
+ },
78
+ {
79
+ "field": "currency",
80
+ "native": "unit"
81
+ }
82
+ ]
83
+ }
84
+ ]
85
+ },
86
+ {
87
+ "type": "example.payments::type::Payment",
88
+ "native": [
89
+ "crate",
90
+ "domain",
91
+ "PaymentStatus"
92
+ ],
93
+ "constructors": [
94
+ {
95
+ "constructor": "Paid",
96
+ "native": [
97
+ "crate",
98
+ "domain",
99
+ "PaymentStatus",
100
+ "Settled"
101
+ ],
102
+ "style": "variant",
103
+ "fields": [
104
+ {
105
+ "field": "value",
106
+ "native": "price"
107
+ }
108
+ ]
109
+ },
110
+ {
111
+ "constructor": "Declined",
112
+ "native": [
113
+ "crate",
114
+ "domain",
115
+ "PaymentStatus",
116
+ "Rejected"
117
+ ],
118
+ "style": "variant",
119
+ "fields": [
120
+ {
121
+ "field": "reason",
122
+ "native": "explanation"
123
+ }
124
+ ]
125
+ }
126
+ ]
127
+ }
128
+ ],
129
+ "functions": [
130
+ {
131
+ "declaration": "example.payments::addFee",
132
+ "native": [
133
+ "crate",
134
+ "domain",
135
+ "apply_fee"
136
+ ]
137
+ },
138
+ {
139
+ "declaration": "example.payments::roundTrip",
140
+ "native": [
141
+ "crate",
142
+ "domain",
143
+ "restore"
144
+ ]
145
+ },
146
+ {
147
+ "declaration": "example.payments::archive",
148
+ "native": [
149
+ "crate",
150
+ "domain",
151
+ "store"
152
+ ]
153
+ }
154
+ ],
155
+ "generators": [
156
+ {
157
+ "type": "example.payments::type::Money",
158
+ "factory": [
159
+ "lawspec_generators",
160
+ "prices"
161
+ ]
162
+ }
163
+ ]
164
+ }
165
+ }
166
+ ]
167
+ }
@@ -0,0 +1,37 @@
1
+ // Application-owned types: no dependency on generated lawspec_data types.
2
+ use crate::lawspec_runtime::Decimal;
3
+
4
+ #[derive(Clone, Debug)]
5
+ pub enum CurrencyCode {
6
+ Dollars,
7
+ Euros,
8
+ Pounds,
9
+ }
10
+
11
+ #[derive(Clone, Debug)]
12
+ pub struct Price {
13
+ pub major: Decimal,
14
+ pub unit: CurrencyCode,
15
+ }
16
+
17
+ #[derive(Clone, Debug)]
18
+ pub enum PaymentStatus {
19
+ Settled { price: Price },
20
+ Rejected { explanation: String },
21
+ }
22
+
23
+ pub fn apply_fee(mut price: Price) -> Price {
24
+ price.major = price
25
+ .major
26
+ .add(&Decimal::new(2.into(), (-1).into()))
27
+ .unwrap();
28
+ price
29
+ }
30
+
31
+ pub fn restore(payment: PaymentStatus) -> PaymentStatus {
32
+ payment
33
+ }
34
+
35
+ pub fn store(payments: Vec<Option<PaymentStatus>>) -> Vec<Option<PaymentStatus>> {
36
+ payments
37
+ }
@@ -0,0 +1,2 @@
1
+ include!("lawspec_modules.rs");
2
+ pub mod domain;
@@ -0,0 +1,11 @@
1
+ use lawspec_example::domain::{CurrencyCode, Price};
2
+ use lawspec_example::lawspec_runtime::Decimal;
3
+ use proptest::prelude::*;
4
+
5
+ // The native range strategy retains its shrinker through this mapping.
6
+ pub fn prices() -> impl Strategy<Value = Price> {
7
+ (100i32..=200).prop_map(|cents| Price {
8
+ major: Decimal::new(cents.into(), (-2).into()),
9
+ unit: CurrencyCode::Euros,
10
+ })
11
+ }
@@ -0,0 +1,149 @@
1
+ {
2
+ "version": 1,
3
+ "sources": [
4
+ "laws"
5
+ ],
6
+ "machineBits": 64,
7
+ "targets": [
8
+ {
9
+ "language": "typescript",
10
+ "root": ".",
11
+ "nativeBindings": {
12
+ "types": [
13
+ {
14
+ "type": "example.payments::type::Currency",
15
+ "native": [
16
+ "payments_domain",
17
+ "CurrencyCode"
18
+ ],
19
+ "constructors": [
20
+ {
21
+ "constructor": "USD",
22
+ "native": [
23
+ "payments_domain",
24
+ "Dollars"
25
+ ],
26
+ "style": "unit",
27
+ "fields": []
28
+ },
29
+ {
30
+ "constructor": "EUR",
31
+ "native": [
32
+ "payments_domain",
33
+ "Euros"
34
+ ],
35
+ "style": "unit",
36
+ "fields": []
37
+ },
38
+ {
39
+ "constructor": "GBP",
40
+ "native": [
41
+ "payments_domain",
42
+ "Pounds"
43
+ ],
44
+ "style": "unit",
45
+ "fields": []
46
+ }
47
+ ]
48
+ },
49
+ {
50
+ "type": "example.payments::type::Money",
51
+ "native": [
52
+ "payments_domain",
53
+ "Price"
54
+ ],
55
+ "constructors": [
56
+ {
57
+ "constructor": "Money",
58
+ "native": [
59
+ "payments_domain",
60
+ "Price"
61
+ ],
62
+ "style": "record",
63
+ "fields": [
64
+ {
65
+ "field": "amount",
66
+ "native": "major"
67
+ },
68
+ {
69
+ "field": "currency",
70
+ "native": "unit"
71
+ }
72
+ ]
73
+ }
74
+ ]
75
+ },
76
+ {
77
+ "type": "example.payments::type::Payment",
78
+ "native": [
79
+ "payments_domain",
80
+ "PaymentStatus"
81
+ ],
82
+ "constructors": [
83
+ {
84
+ "constructor": "Paid",
85
+ "native": [
86
+ "payments_domain",
87
+ "Settled"
88
+ ],
89
+ "style": "variant",
90
+ "fields": [
91
+ {
92
+ "field": "value",
93
+ "native": "price"
94
+ }
95
+ ]
96
+ },
97
+ {
98
+ "constructor": "Declined",
99
+ "native": [
100
+ "payments_domain",
101
+ "Rejected"
102
+ ],
103
+ "style": "variant",
104
+ "fields": [
105
+ {
106
+ "field": "reason",
107
+ "native": "explanation"
108
+ }
109
+ ]
110
+ }
111
+ ]
112
+ }
113
+ ],
114
+ "functions": [
115
+ {
116
+ "declaration": "example.payments::addFee",
117
+ "native": [
118
+ "payments_domain",
119
+ "apply_fee"
120
+ ]
121
+ },
122
+ {
123
+ "declaration": "example.payments::roundTrip",
124
+ "native": [
125
+ "payments_domain",
126
+ "restore"
127
+ ]
128
+ },
129
+ {
130
+ "declaration": "example.payments::archive",
131
+ "native": [
132
+ "payments_domain",
133
+ "store"
134
+ ]
135
+ }
136
+ ],
137
+ "generators": [
138
+ {
139
+ "type": "example.payments::type::Money",
140
+ "factory": [
141
+ "lawspec_generators",
142
+ "prices"
143
+ ]
144
+ }
145
+ ]
146
+ }
147
+ }
148
+ ]
149
+ }
@@ -0,0 +1,42 @@
1
+ // Application-owned domain types; no generated domain declarations.
2
+ import * as ls from './lawspec_runtime.js';
3
+
4
+ export class CurrencyCode {}
5
+ export class Dollars extends CurrencyCode {}
6
+ export class Euros extends CurrencyCode {}
7
+ export class Pounds extends CurrencyCode {}
8
+
9
+ export class Price {
10
+ major: ls.Decimal;
11
+ unit: CurrencyCode;
12
+ constructor({unit, major}: {unit: CurrencyCode; major: ls.Decimal}) {
13
+ this.unit = unit;
14
+ this.major = major;
15
+ }
16
+ }
17
+ export class PaymentStatus {}
18
+ export class Settled extends PaymentStatus {
19
+ price: Price;
20
+ constructor({price}: {price: Price}) {
21
+ super();
22
+ this.price = price;
23
+ }
24
+ }
25
+ export class Rejected extends PaymentStatus {
26
+ explanation: string;
27
+ constructor({explanation}: {explanation: string}) {
28
+ super();
29
+ this.explanation = explanation;
30
+ }
31
+ }
32
+ export function apply_fee(price: Price): Price {
33
+ const major = ls.binary('+', price.major, new ls.Decimal(2n, -1n),
34
+ 'Decimal', 'Decimal') as ls.Decimal;
35
+ return new Price({major, unit: price.unit});
36
+ }
37
+ export function restore<T>(payment: T): T {
38
+ return payment;
39
+ }
40
+ export function store<T>(payments: T[]): T[] {
41
+ return payments;
42
+ }
@@ -0,0 +1,8 @@
1
+ import * as fc from 'fast-check';
2
+ import * as ls from '../src/lawspec_runtime.js';
3
+ import {Price, Euros} from '../src/payments_domain.js';
4
+
5
+ export function prices(): fc.Arbitrary<Price> {
6
+ return fc.integer({min:100, max:200}).map(cents =>
7
+ new Price({major:new ls.Decimal(BigInt(cents), -2n), unit:new Euros()}));
8
+ }
@@ -0,0 +1,76 @@
1
+ unit example.payments
2
+
3
+ -- The executable acceptance model for native domain bindings in 0.10.
4
+ -- Applications may call Money "Price", amount "major", and currency "unit";
5
+ -- those representation choices must not alter these laws.
6
+ type Currency is USD | EUR | GBP end
7
+
8
+ type Money is
9
+ Money amount :: Decimal currency :: Currency
10
+ end
11
+
12
+ type Payment is
13
+ Paid value :: Money
14
+ Declined reason :: Text
15
+ end
16
+
17
+ -- These are adapter functions supplied by the application.
18
+ addFee :: Money -> Money
19
+ roundTrip :: Payment -> Payment
20
+ archive :: List (Maybe Payment) -> List (Maybe Payment)
21
+
22
+ -- The model is exact, independent of host decimal rounding settings.
23
+ definition feeModel (money :: Money) :: Money is
24
+ match money with
25
+ | Money amount currency -> Money (amount + 0.2) currency
26
+ end
27
+ end
28
+
29
+ law `fees preserve currency and exact decimal value` is
30
+ definition is
31
+ `for all` (money :: Money) . addFee money = feeModel money
32
+ end
33
+ example `decimal tenths` is
34
+ money = Money 0.1 USD
35
+ expect addFee money = Money 0.3 USD
36
+ end
37
+ example `small amount is not rounded away` is
38
+ money = Money 0.000000000000000000000000000001 EUR
39
+ expect addFee money = Money 0.200000000000000000000000000001 EUR
40
+ end
41
+ example `beyond binary floating precision` is
42
+ money = Money 9007199254740993.1 GBP
43
+ expect addFee money = Money 9007199254740993.3 GBP
44
+ end
45
+ end
46
+
47
+ law `payment variants and payloads survive native conversion` is
48
+ definition is
49
+ `for all` (payment :: Payment) . roundTrip payment = payment
50
+ end
51
+ example `successful payment` is
52
+ payment = Paid (Money 12.34 EUR)
53
+ expect roundTrip payment = Paid (Money 12.34 EUR)
54
+ end
55
+ example `declined payment` is
56
+ payment = Declined "card declined"
57
+ expect roundTrip payment = Declined "card declined"
58
+ end
59
+ end
60
+
61
+ law `archives preserve order duplicates and absence` is
62
+ definition is
63
+ `for all` (payments :: List (Maybe Payment)) . archive payments = payments
64
+ end
65
+ example `empty archive` is
66
+ payments = []
67
+ expect archive payments = []
68
+ end
69
+ example `missing is different from a declined payment` is
70
+ payments = [Nothing, Just (Declined ""), Just (Paid (Money 0.1 USD)),
71
+ Just (Paid (Money 0.1 USD))]
72
+ expect archive payments = [Nothing, Just (Declined ""),
73
+ Just (Paid (Money 0.1 USD)),
74
+ Just (Paid (Money 0.1 USD))]
75
+ end
76
+ end
@@ -3,11 +3,16 @@ import path from "node:path";
3
3
  import { createCompiler } from "./api.mjs";
4
4
  import { targets } from "./templates.mjs";
5
5
  import { safePath, planWrites, applyWrites } from "./files.mjs";
6
+ import { exportNativePayments } from "./native-examples.mjs";
6
7
 
7
8
  export async function generateExamples(options) {
8
9
  const selected = options.target ? [options.target] : targets;
9
10
  if (selected.some((t) => !targets.includes(t)))
10
11
  throw new Error(`Unknown target: ${options.target}`);
12
+ if (options.example !== undefined) {
13
+ if (options.example !== "payments") throw new Error(`Unknown example: ${options.example}`);
14
+ return exportNativePayments(options, selected);
15
+ }
11
16
  const directory = new URL("./examples/specs/", import.meta.url);
12
17
  const sources = await Promise.all(
13
18
  (await readdir(directory))
package/files.mjs CHANGED
@@ -63,8 +63,8 @@ export async function atomicWrite(file, content, exclusive = false) {
63
63
  }
64
64
  }
65
65
  }
66
- export async function planWrites(root, artifacts) {
67
- const manifestPath = await safePath(root, ".lawspec/generated.json");
66
+ export async function planWrites(root, artifacts, {manifest = ".lawspec/generated.json"} = {}) {
67
+ const manifestPath = await safePath(root, manifest);
68
68
  const previousText = await readOptional(manifestPath);
69
69
  const previous =
70
70
  previousText === null
@@ -144,18 +144,18 @@ export async function planWrites(root, artifacts) {
144
144
  throw new Error(`Refusing to remove edited generated file: ${relative}`);
145
145
  changes.push({ action: "remove", file, relative, expected: old });
146
146
  }
147
- const manifest =
147
+ const manifestContent =
148
148
  JSON.stringify(
149
149
  { version: 1, files: next, adapters: adapterHashes },
150
150
  null,
151
151
  2,
152
152
  ) + "\n";
153
- if (manifest !== previousText)
153
+ if (manifestContent !== previousText)
154
154
  changes.push({
155
155
  action: previousText === null ? "create" : "update",
156
156
  file: manifestPath,
157
- relative: ".lawspec/generated.json",
158
- content: manifest,
157
+ relative: manifest,
158
+ content: manifestContent,
159
159
  expected: previousText,
160
160
  });
161
161
  return { root, changes, preserved, adapterUpdates };
package/index.d.ts CHANGED
@@ -162,11 +162,62 @@ export interface Refinement {
162
162
  definition: string;
163
163
  }
164
164
 
165
+ export type NativeReference = string[];
166
+
167
+ export interface NativeFieldBinding {
168
+ field: string;
169
+ native: string;
170
+ }
171
+
172
+ export interface NativeConstructorBinding {
173
+ constructor: string;
174
+ native: NativeReference;
175
+ fields?: NativeFieldBinding[];
176
+ style: 'record' | 'variant' | 'unit';
177
+ }
178
+
179
+ export interface NativeCodecBinding {
180
+ toNative: NativeReference;
181
+ fromNative: NativeReference;
182
+ }
183
+
184
+ export interface NativeTypeBinding {
185
+ type: string;
186
+ native: NativeReference;
187
+ constructors?: NativeConstructorBinding[];
188
+ codec?: NativeCodecBinding;
189
+ }
190
+
191
+ export interface NativeGeneratorBinding {
192
+ type: string;
193
+ factory: NativeReference;
194
+ stub?: boolean;
195
+ }
196
+
197
+ export interface NativeFunctionBinding {
198
+ declaration: string;
199
+ native: NativeReference;
200
+ }
201
+
202
+ export interface NativeGoImport {
203
+ alias: string;
204
+ path: string;
205
+ }
206
+
207
+ export interface NativeBindings {
208
+ types?: NativeTypeBinding[];
209
+ generators?: NativeGeneratorBinding[];
210
+ functions?: NativeFunctionBinding[];
211
+ rustCrate?: string;
212
+ goImports?: NativeGoImport[];
213
+ }
214
+
165
215
  export interface CheckRequest {
166
216
  sources: Source[];
167
- schemaVersion?: 3;
217
+ schemaVersion?: 3 | 4;
168
218
  machineBits?: 32 | 64;
169
219
  generation?: Partial<Generation>;
220
+ nativeBindings?: NativeBindings;
170
221
  }
171
222
 
172
223
  export interface GenerationRequest extends CheckRequest {
@@ -177,7 +228,7 @@ export interface GenerationRequest extends CheckRequest {
177
228
  }
178
229
 
179
230
  export interface Result {
180
- schemaVersion: 3;
231
+ schemaVersion: 3 | 4;
181
232
  machineBits?: 32 | 64;
182
233
  generation?: Generation;
183
234
  units?: Unit[];