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. 4 import Base 5 import ./manifest.bend as M 6 import ../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 10 def 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 15 def 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. 22 def vendor.word(vend: Bool) -> String: 23 match vend: 24 case True{}: 25 "true" 26 case False{}: 27 "" 28 29 # the keys a source contributes 30 def 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.]` section 41 def 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 46 def 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.]` section. No hash: a tool is not a package. 54 def 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 59 def 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 67 def 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 74 def 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 79 def 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 87 def 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 95 def 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 104 def 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 109 def 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 114 def 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 124 def 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. 134 def 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 140 def 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 148 def 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. 160 def 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. 168 def 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 182 def 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 193 def 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 199 def 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. 210 def 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 220 def 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 225 def 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. 238 def 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 244 def 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"