lawspec 0.9.0 → 0.11.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/API-MIGRATION.md +78 -2
- package/HASKELL.md +1 -0
- package/KOTLIN.md +5 -0
- package/LANGUAGE.md +70 -11
- package/NATIVE-BINDINGS.md +1049 -0
- package/PRIMITIVES.md +1 -1
- package/README.md +86 -23
- package/REFINEMENTS.md +6 -1
- package/RELEASE-0.10.md +60 -0
- package/RELEASE-0.11.md +62 -0
- package/api.mjs +23 -3
- package/bin/lawspec.mjs +17 -4
- package/build.json +60 -38
- package/core.wasm +0 -0
- package/examples/native-payments/go/example/payments/domain.go +38 -0
- package/examples/native-payments/go/example/payments/native_generators_test.go +13 -0
- package/examples/native-payments/go/lawspec.json +136 -0
- package/examples/native-payments/haskell/lawspec.json +149 -0
- package/examples/native-payments/haskell/src/PaymentsDomain.hs +21 -0
- package/examples/native-payments/haskell/test/PaymentGenerators.hs +12 -0
- package/examples/native-payments/java/lawspec.json +165 -0
- package/examples/native-payments/java/src/main/java/domain/PaymentsDomain.java +32 -0
- package/examples/native-payments/java/src/test/java/domain/PaymentGenerators.java +16 -0
- package/examples/native-payments/javascript/lawspec.json +149 -0
- package/examples/native-payments/javascript/src/payments_domain.mjs +44 -0
- package/examples/native-payments/javascript/test/lawspec_generators.mjs +6 -0
- package/examples/native-payments/kotlin/lawspec.json +165 -0
- package/examples/native-payments/kotlin/src/main/kotlin/domain/PaymentsDomain.kt +17 -0
- package/examples/native-payments/kotlin/src/test/kotlin/domain/PaymentGenerators.kt +12 -0
- package/examples/native-payments/python/lawspec.json +149 -0
- package/examples/native-payments/python/src/payments_domain.py +60 -0
- package/examples/native-payments/python/tests/lawspec_generators.py +15 -0
- package/examples/native-payments/rust/lawspec.json +167 -0
- package/examples/native-payments/rust/src/domain.rs +37 -0
- package/examples/native-payments/rust/src/lib.rs +2 -0
- package/examples/native-payments/rust/tests/support/lawspec_generators.rs +11 -0
- package/examples/native-payments/typescript/lawspec.json +149 -0
- package/examples/native-payments/typescript/src/payments_domain.ts +42 -0
- package/examples/native-payments/typescript/test/lawspec_generators.ts +8 -0
- package/examples/specs/indexed_families.lawspec +59 -0
- package/examples/specs/payments.lawspec +76 -0
- package/examples-command.mjs +5 -0
- package/files.mjs +6 -6
- package/index.d.ts +53 -2
- package/native-examples.mjs +81 -0
- package/package.json +2 -2
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
unit example.indexed
|
|
2
|
+
|
|
3
|
+
-- A list indexed by its length. Each constructor states how it changes the
|
|
4
|
+
-- index; nOfVec is the checked measure that recomputes it from a value.
|
|
5
|
+
type Vec (n :: Natural) (a :: Type) is
|
|
6
|
+
| VNil where n = 0
|
|
7
|
+
| VCons head :: a tail :: Vec m a where n = m + 1
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
-- A binary tree indexed by its number of values.
|
|
11
|
+
type Tree (n :: Natural) (a :: Type) is
|
|
12
|
+
| Tip where n = 0
|
|
13
|
+
| Bin left :: Tree l a value :: a right :: Tree r a where n = l + r + 1
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
-- Result indices are checked contracts over the native implementations.
|
|
17
|
+
replicate :: (n :: Natural where n < 64) -> (x :: Int8) -> (r :: Vec n Int8)
|
|
18
|
+
append :: (xs :: Vec n Int8) -> (ys :: Vec m Int8) -> (r :: Vec (n + m) Int8)
|
|
19
|
+
zip :: (xs :: Vec n Int8) -> (ys :: Vec n Bool) -> (r :: Vec n Bool)
|
|
20
|
+
flatten :: (tree :: Tree n Int8) -> (r :: Vec n Int8)
|
|
21
|
+
|
|
22
|
+
law `replicate has the requested length` is
|
|
23
|
+
definition is
|
|
24
|
+
`for all` (n :: Natural where n < 16) (x :: Int8) . nOfVec (replicate n x) = n
|
|
25
|
+
end
|
|
26
|
+
example `three copies` is
|
|
27
|
+
n = 3
|
|
28
|
+
x = 7
|
|
29
|
+
expect nOfVec (replicate n x) = 3
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
law `append adds lengths` is
|
|
34
|
+
definition is
|
|
35
|
+
`for all` (xs :: Vec n Int8) (ys :: Vec m Int8) .
|
|
36
|
+
nOfVec (append xs ys) = n + m
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
-- A fixed index is generated directly rather than filtered.
|
|
41
|
+
law `vectors of three elements` is
|
|
42
|
+
definition is
|
|
43
|
+
`for all` (xs :: Vec 3 Int8) . nOfVec (append xs xs) = 6
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
-- A shared index constrains the second input by the first.
|
|
48
|
+
law `zip preserves a shared length` is
|
|
49
|
+
definition is
|
|
50
|
+
`for all` (xs :: Vec n Int8) (ys :: Vec n Bool) . nOfVec (zip xs ys) = n
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
-- Generation splits the remaining index across both subtrees.
|
|
55
|
+
law `flattening five values` is
|
|
56
|
+
definition is
|
|
57
|
+
`for all` (tree :: Tree 5 Int8) . nOfVec (flatten tree) = 5
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -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
|
package/examples-command.mjs
CHANGED
|
@@ -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,
|
|
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
|
|
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 (
|
|
153
|
+
if (manifestContent !== previousText)
|
|
154
154
|
changes.push({
|
|
155
155
|
action: previousText === null ? "create" : "update",
|
|
156
156
|
file: manifestPath,
|
|
157
|
-
relative:
|
|
158
|
-
content:
|
|
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[];
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {readFile, readdir, mkdir} from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {templates, commands, setup} from './templates.mjs';
|
|
4
|
+
import {safePath, planWrites, applyWrites} from './files.mjs';
|
|
5
|
+
|
|
6
|
+
async function projectFiles(directory, prefix = '') {
|
|
7
|
+
const files = {};
|
|
8
|
+
for (const entry of await readdir(directory, {withFileTypes: true})) {
|
|
9
|
+
const name = prefix + entry.name;
|
|
10
|
+
const location = new URL(entry.name + (entry.isDirectory() ? '/' : ''), directory);
|
|
11
|
+
if (entry.isDirectory()) Object.assign(files, await projectFiles(location, name + '/'));
|
|
12
|
+
else if (entry.isFile()) files[name] = await readFile(location, 'utf8');
|
|
13
|
+
else throw new Error(`Unsupported bundled example entry: ${name}`);
|
|
14
|
+
}
|
|
15
|
+
return files;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function exportNativePayments(options, selected) {
|
|
19
|
+
const root = await safePath(process.cwd(), options.output || 'native_payments');
|
|
20
|
+
await mkdir(root, {recursive: true});
|
|
21
|
+
const law = await readFile(new URL('./examples/specs/payments.lawspec', import.meta.url), 'utf8');
|
|
22
|
+
const plans = [];
|
|
23
|
+
const results = [];
|
|
24
|
+
for (const target of selected) {
|
|
25
|
+
const directory = await safePath(root, target);
|
|
26
|
+
await mkdir(directory, {recursive: true});
|
|
27
|
+
const files = {
|
|
28
|
+
...templates(target, {minify: options.minify === true}),
|
|
29
|
+
...await projectFiles(new URL(`./examples/native-payments/${target}/`, import.meta.url)),
|
|
30
|
+
'laws/payments.lawspec': law,
|
|
31
|
+
};
|
|
32
|
+
const config = JSON.parse(files['lawspec.json']);
|
|
33
|
+
config.machineBits = options.machineBits ?? 64;
|
|
34
|
+
files['lawspec.json'] = JSON.stringify(config, null, options.minify ? undefined : 2) + '\n';
|
|
35
|
+
files['README.md'] = `# Native payments: ${target}
|
|
36
|
+
|
|
37
|
+
This project checks application-owned payment types against the shared LawSpec
|
|
38
|
+
model. Money maps to Price, currency variants have application names, and the
|
|
39
|
+
archive preserves distinct present and absent payments.
|
|
40
|
+
|
|
41
|
+
## Run
|
|
42
|
+
|
|
43
|
+
Install LawSpec and the native dependencies described below, then run:
|
|
44
|
+
|
|
45
|
+
\`\`\`sh
|
|
46
|
+
lawspec check
|
|
47
|
+
lawspec generate
|
|
48
|
+
${commands[target]}
|
|
49
|
+
\`\`\`
|
|
50
|
+
|
|
51
|
+
${setup[target]}
|
|
52
|
+
|
|
53
|
+
JavaScript and TypeScript projects require \`npm install\` before testing.
|
|
54
|
+
The specification includes exact decimal arithmetic and explicit USD/GBP examples.
|
|
55
|
+
The native price generator deliberately samples only EUR values between 1.00 and
|
|
56
|
+
2.00; examples and deterministic boundaries still check the other cases. Its
|
|
57
|
+
framework's mapping operation preserves the native shrinker.
|
|
58
|
+
|
|
59
|
+
The application types use native exact decimals where available and LawSpec's
|
|
60
|
+
framework-independent numeric runtime otherwise. Run generation before compiling
|
|
61
|
+
the application so that runtime support is present.
|
|
62
|
+
|
|
63
|
+
## Change the application
|
|
64
|
+
|
|
65
|
+
Edit the domain implementation or generator, then run the native tests again.
|
|
66
|
+
For example, change the fee from 0.2 to 0.3: the exact-decimal law must fail.
|
|
67
|
+
Edit \`lawspec.json\` to change native bindings and rerun \`lawspec generate\`.
|
|
68
|
+
|
|
69
|
+
All exported project files are user-owned. Re-exporting preserves your edits;
|
|
70
|
+
it reports changed bundled versions for review. Compiler-generated files use a
|
|
71
|
+
separate ownership manifest and remain protected against accidental overwrites.
|
|
72
|
+
`;
|
|
73
|
+
const artifacts = Object.entries(files).map(([path, content]) => ({path, content, ownership: 'user'}));
|
|
74
|
+
const plan = await planWrites(directory, artifacts, {manifest: '.lawspec/example.json'});
|
|
75
|
+
plans.push(plan);
|
|
76
|
+
results.push({target, directory, example: 'payments', files: artifacts.map(({path, ownership}) => ({path, ownership})),
|
|
77
|
+
changes: plan.changes.length, preservedAdapters: plan.preserved, adapterUpdates: plan.adapterUpdates});
|
|
78
|
+
}
|
|
79
|
+
await applyWrites(plans);
|
|
80
|
+
return results;
|
|
81
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lawspec",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "State the law once. Check it everywhere.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
"bin": { "lawspec": "bin/lawspec.mjs" },
|
|
9
9
|
"exports": { ".": { "types": "./index.d.ts", "import": "./api.mjs" } },
|
|
10
10
|
"types": "index.d.ts",
|
|
11
|
-
"files": ["*.mjs", "*.json", "index.d.ts", "bin", "core.wasm", "core_jsffi.js", "README.md", "PRIMITIVES.md", "REFINEMENTS.md", "RUST.md", "JAVA.md", "PYTHON.md", "GO.md", "HASKELL.md", "KOTLIN.md", "WEB.md", "LANGUAGE.md", "API-MIGRATION.md", "RELEASE-0.9.md", "LICENSE", "starter.lawspec", "examples"],
|
|
11
|
+
"files": ["*.mjs", "*.json", "index.d.ts", "bin", "core.wasm", "core_jsffi.js", "README.md", "NATIVE-BINDINGS.md", "PRIMITIVES.md", "REFINEMENTS.md", "RUST.md", "JAVA.md", "PYTHON.md", "GO.md", "HASKELL.md", "KOTLIN.md", "WEB.md", "LANGUAGE.md", "API-MIGRATION.md", "RELEASE-0.9.md", "RELEASE-0.10.md", "RELEASE-0.11.md", "LICENSE", "starter.lawspec", "examples"],
|
|
12
12
|
"scripts": { "test": "node --test test/*.test.mjs" }
|
|
13
13
|
}
|