src/ledger/render.bend

ezx@1.10.0.0 · 245 lines · 0 definitions

1# ledger/render: a ledger written back out as ez.toml, and the two edits the
2# command line makes to it. Rendering is from the model, so the file it writes
3# is the file it would read back.
4import Base
5import ezx@1.10.0.0/src/ledger/manifest.bend as M
6import ezx@1.10.0.0/src/toml/toml.bend as T
7
8# a key line, or nothing at all when the value is empty, so an absent tag,
9# entry or bin leaves no trace
10def line(key: String, +val: String) -> String:
11  Bool.pick(String, String.is_empty(val), "", key ++ " = \"" ++ val ++ "\"\n")
12
13# a key line whose value is written bare, as a TOML boolean is, or nothing at
14# all when the value is empty
15def line.bare(key: String, +val: String) -> String:
16  Bool.pick(String, String.is_empty(val), "", key ++ " = " ++ val ++ "\n")
17
18# `true` when the tree is committed, and nothing when it is not, so the key
19# is absent rather than written as an empty string. It is written bare: in
20# TOML `vendor = "true"` is a string, and a reader other than ez, such as
21# nix's `builtins.fromTOML`, would take it for one. ez reads both spellings.
22def vendor.word(vend: Bool) -> String:
23  match vend:
24    case True{}:
25      "true"
26    case False{}:
27      ""
28
29# the keys a source contributes
30def source(src: M.Source) -> String:
31  match src:
32    case M.Hub{named}:
33      line("hub", named)
34    case M.Git{url, rev, tag, root, nar, vend}:
35      line("git", url) ++ line("rev", rev) ++ line("tag", tag) ++
36      line("root", root) ++ line("narHash", nar) ++ line.bare("vendor", vendor.word(vend))
37    case M.Member{path, named}:
38      line.bare("workspace", "true") ++ line("path", path) ++ line("hub", named)
39
40# one `[deps.<name>]` section
41def dep(dependency: M.Dep) -> String:
42  M.Dep{n, h, e, s} = dependency
43  "\n[deps." ++ n ++ "]\n" ++ line("hash", h) ++ source(s) ++ line("entry", e)
44
45# every dependency, in the order the ledger holds them
46def deps(ds: List<&2, M.Dep>) -> String:
47  match ds:
48    case []:
49      ""
50    case h <> t:
51      dep(h) ++ deps(t)
52
53# one `[tools.<name>]` section. No hash: a tool is not a package.
54def tool(pin: M.Tool) -> String:
55  M.Tool{n, e, b, s} = pin
56  "\n[tools." ++ n ++ "]\n" ++ source(s) ++ line("entry", e) ++ line("bin", b)
57
58# every pinned CLI, in the order the ledger holds them
59def tools(ts: List<&2, M.Tool>) -> String:
60  match ts:
61    case []:
62      ""
63    case h <> t:
64      tool(h) ++ tools(t)
65
66# the ledger as ez assembles it, in the TOML ez wrote before eztoml 0.4
67def text(manifest: M.Manifest) -> String:
68  M.Manifest{n, e, b, h, pa, pv, ds, ts} = manifest
69  "[package]\n" ++ line("name", n) ++ line("entry", e) ++ line("bin", b) ++
70    line("hub", h) ++ line("publish-as", pa) ++ line("version", pv) ++
71    deps(ds) ++ tools(ts)
72
73# the ledger as an ez.toml: ez's text, written as eztoml writes it
74def show(manifest: M.Manifest) -> String:
75  T.normal(text(manifest))
76
77# a ledger that was read, as an ez.toml; an unreadable one renders as nothing,
78# so a failed read can never overwrite a good file with a guess
79def render(ledger: M.Read) -> String:
80  match ledger:
81    case M.Bad{_why}:
82      ""
83    case M.Good{m}:
84      show(m)
85
86# a dependency kept or dropped
87def without.put(hit: Bool, dependency: M.Dep, rest: List<&2, M.Dep>) -> List<&2, M.Dep>:
88  match hit:
89    case True{}:
90      rest
91    case False{}:
92      dependency <> rest
93
94# every dependency but the one named
95def without(ds: List<&2, M.Dep>, +name: String) -> List<&2, M.Dep>:
96  match ds:
97    case []:
98      []
99    case +h <> t:
100      without.put(String.eq(M.dep.name(h), name), h, without(t, name))
101
102# the ledger with a dependency added, replacing any of the same name, so adding
103# twice is the same as adding once
104def add(manifest: M.Manifest, +dependency: M.Dep) -> M.Manifest:
105  M.Manifest{n, e, b, h, pa, pv, ds, ts} = manifest
106  M.Manifest{n, e, b, h, pa, pv, List.append(&2, M.Dep, without(ds, M.dep.name(dependency)), [dependency]), ts}
107
108# the ledger with a dependency dropped
109def remove(manifest: M.Manifest, name: String) -> M.Manifest:
110  M.Manifest{n, e, b, h, pa, pv, ds, ts} = manifest
111  M.Manifest{n, e, b, h, pa, pv, without(ds, name), ts}
112
113# a git source with its tree committed
114def vendored(src: M.Source) -> M.Source:
115  match src:
116    case M.Hub{named}:
117      M.Hub{named}
118    case M.Git{url, rev, tag, root, nar, _vend}:
119      M.Git{url, rev, tag, root, nar, True{}}
120    case M.Member{path, named}:
121      M.Member{path, named}
122
123# a source that keeps a vendor bit the ledger already set, or is left as given
124def revend(keep: Bool, src: M.Source) -> M.Source:
125  match keep:
126    case True{}:
127      vendored(src)
128    case False{}:
129      src
130
131# a dependency added again keeps the `vendor = true` its entry already had.
132# The command line has no way to say it, so a re-add that dropped it would
133# drop the tree from the allowlist without anyone having asked.
134def readd(ds: List<&2, M.Dep>, dependency: M.Dep) -> M.Dep:
135  M.Dep{+n, h, e, s} = dependency
136  M.Dep{n, h, e, revend(M.source.vendor(M.source.dep(M.find(ds, n))), s)}
137
138# the ledger with a dependency added, keeping the vendor bit of the entry it
139# replaces
140def add.keep(manifest: M.Manifest, dependency: M.Dep) -> M.Manifest:
141  M.Manifest{n, e, b, h, pa, pv, +ds, ts} = manifest
142  add(M.Manifest{n, e, b, h, pa, pv, ds, ts}, readd(ds, dependency))
143
144# ---------------------------------------------------------------------------
145# what a ledger may hold to be written
146
147# whether a text holds no copy of a char
148def clear(text: String, +ch: Char) -> Bool:
149  match text:
150    case SNil{}:
151      True{}
152    case SCon{h, t}:
153      rest = clear(t, ch)
154      Bool.and(Bool.not(Char.is_eq(h, ch)), rest)
155
156# whether a name or value can be written as it is: it holds no `"`, no `\`
157# and no newline. The pinned eztoml reader (v0.1.0) neither writes nor reads
158# escapes, a newline would end the line, and a quote or a backslash would make
159# the file mean something else to every other TOML reader, nix's among them.
160def clean(+text: String) -> Bool:
161  Bool.and(clear(text, '"'), Bool.and(clear(text, '\\'), clear(text, '\n')))
162
163# whether a source reads back as itself. A git source names its repo and the
164# directory its paths are written from: the reader takes a section with no
165# `git` for a hub package and one with no `root` for the top of the checkout.
166# A workspace member's is `workspace = true`, with the directory and the name
167# only the joint ledger gives it.
168def source.ok(src: M.Source) -> Bool:
169  match src:
170    case M.Hub{+named}:
171      clean(named)
172    case M.Git{+url, rev, tag, +root, nar, _vend}:
173      Bool.and(Bool.and(Bool.not(String.is_empty(url)), Bool.not(String.is_empty(root))),
174        Bool.and(Bool.and(clean(url), clean(rev)),
175          Bool.and(clean(tag), Bool.and(clean(root), clean(nar)))))
176    case M.Member{+path, +named}:
177      Bool.and(clean(path), clean(named))
178
179# whether a dependency's hash can be written: one is there, which the reader
180# asks every dependency for but a workspace member's, whose ledger records
181# none and whose hash only the joint ledger gives
182def hash.ok(src: M.Source, +hash: String) -> Bool:
183  match src:
184    case M.Hub{_named}:
185      Bool.and(Bool.not(String.is_empty(hash)), clean(hash))
186    case M.Git{_url, _rev, _tag, _root, _nar, _vend}:
187      Bool.and(Bool.not(String.is_empty(hash)), clean(hash))
188    case M.Member{_path, _named}:
189      clean(hash)
190
191# whether a dependency does: it has a hash, but for a workspace member, and
192# every name and value is clean
193def dep.ok(dependency: M.Dep) -> Bool:
194  M.Dep{+n, +h, +e, +s} = dependency
195  Bool.and(Bool.and(clean(n), hash.ok(s, h)),
196    Bool.and(clean(e), source.ok(s)))
197
198# whether every dependency does
199def deps.ok(ds: List<&2, M.Dep>) -> Bool:
200  match ds:
201    case []:
202      True{}
203    case h <> t:
204      rest = deps.ok(t)
205      Bool.and(dep.ok(h), rest)
206
207# whether a pinned CLI's source does. A tool is never imported, so the reader
208# keeps no hub name for one, and one written with a name would read back
209# without it.
210def tool.src.ok(src: M.Source) -> Bool:
211  match src:
212    case M.Hub{named}:
213      String.is_empty(named)
214    case M.Git{url, rev, tag, root, nar, vend}:
215      source.ok(M.Git{url, rev, tag, root, nar, vend})
216    case M.Member{_path, _named}:
217      False{}
218
219# whether a pinned CLI does
220def tool.ok(pin: M.Tool) -> Bool:
221  M.Tool{+n, +e, +b, s} = pin
222  Bool.and(clean(n), Bool.and(Bool.and(clean(e), clean(b)), tool.src.ok(s)))
223
224# whether every pinned CLI does
225def tools.ok(ts: List<&2, M.Tool>) -> Bool:
226  match ts:
227    case []:
228      True{}
229    case h <> t:
230      rest = tools.ok(t)
231      Bool.and(tool.ok(h), rest)
232
233# whether a ledger can be written so that it reads back as itself (EZ-LED-4):
234# every name and value is clean, `publish-as` and `version` among them, every
235# dependency has a hash, and every git source names its repo and its root.
236# Every command that writes ez.toml refuses a ledger that is not, so every
237# ledger ez writes is one of these.
238def renderable(manifest: M.Manifest) -> Bool:
239  M.Manifest{+n, +e, +b, +h, +pa, +pv, ds, ts} = manifest
240  Bool.and(Bool.and(Bool.and(clean(n), clean(e)), Bool.and(clean(b), clean(h))),
241    Bool.and(Bool.and(clean(pa), clean(pv)), Bool.and(deps.ok(ds), tools.ok(ts))))
242
243# why a ledger that is not renderable is not written
244def unrenderable() -> String:
245  "ez: ez.toml cannot be written: a name or value holds a quote, a backslash or a newline, a dependency has no hash, or a git source names no repo or no root"