1 # YAML for Bend: read YAML text into values and write values back as YAML. 2 # 3 # import ./yaml.bend as Y 4 # import ./value.bend as V 5 # 6 # Y.load("a: [1, 2]") # Done{V.Mapping{[V.Entry{V.Str{"a"}, V.Seq{...}}]}} 7 # 8 # load reads under the YAML 1.2 Core schema; load.v11 reads the YAML 1.1 9 # types (yes/no booleans, 0 octal, base 60, timestamps, "<<" merges). 10 # Errors say what went wrong and where: T.error.show(e) is "mapping values 11 # are not allowed in this context at line 1 column 5". 12 13 import Base 14 15 import ./token.bend as T 16 17 import ./parse.bend as P 18 19 import ./value.bend as V 20 21 import ./compose.bend as C 22 23 import ./emit.bend as E 24 25 def when(-A: Type, hit: Bool, yes: Unit -> A, no: Unit -> A) -> A: 26 match hit: 27 case True{}: 28 yes(Unit{}) 29 case False{}: 30 no(Unit{}) 31 32 def doc.vals(ds: List<&2, C.Doc>) -> List<&2, V.Yaml>: 33 match ds: 34 case Nil{}: 35 [] 36 case Con{d, rest}: 37 match d: 38 case C.Doc{v, at}: 39 v <> doc.vals(rest) 40 41 def all.of(r: Result<&2, &2, T.Error, List<&2, C.Doc>>) -> Result<&2, &2, T.Error, List<&2, V.Yaml>>: 42 match r: 43 case Fail{e}: 44 Fail{e} 45 case Done{ds}: 46 Done{doc.vals(ds)} 47 48 def one.of(r: Result<&2, &2, T.Error, List<&2, C.Doc>>) -> Result<&2, &2, T.Error, V.Yaml>: 49 match r: 50 case Fail{e}: 51 Fail{e} 52 case Done{ds}: 53 match ds: 54 case Nil{}: 55 Done{V.Null{}} 56 case Con{d, rest}: 57 match d: 58 case C.Doc{v, at}: 59 match rest: 60 case Nil{}: 61 Done{v} 62 case Con{other, more}: 63 match other: 64 case C.Doc{w, second}: 65 Fail{T.Error{"found a second document where load expects one; use load.all", second, T.Content{}}} 66 67 # A text past the size limit is refused before it is read. 68 def sized(+lim: C.Limits, +text: String, read: Unit -> Result<&2, &2, T.Error, List<&2, C.Doc>>) -> Result<&2, &2, T.Error, List<&2, C.Doc>>: 69 when(Result<&2, &2, T.Error, List<&2, C.Doc>>, C.longer(text, C.limits.size(lim)), 70 u => Fail{C.limit.error("found a text longer than the size limit of " ++ Nat.show(C.limits.size(lim)) ++ " characters", T.Mark{0n, 0n, 0n})}, 71 read) 72 73 def docs(+s: C.Schema, +lim: C.Limits, +text: String) -> Result<&2, &2, T.Error, List<&2, C.Doc>>: 74 sized(lim, text, u => C.compose(P.events(text, C.limits.depth(lim)), s, lim)) 75 76 # Every document in +text+ under schema +s+, within limits +lim+. 77 def load.all.with(s: C.Schema, lim: C.Limits, text: String) -> Result<&2, &2, T.Error, List<&2, V.Yaml>>: 78 all.of(docs(s, lim, text)) 79 80 # The one document in +text+ under schema +s+, within limits +lim+: null 81 # when there is none, an error when there are more. 82 def load.with(s: C.Schema, lim: C.Limits, text: String) -> Result<&2, &2, T.Error, V.Yaml>: 83 one.of(docs(s, lim, text)) 84 85 # The one document in +text+ under the YAML 1.2 Core schema, within the 86 # default limits: null when there is none, an error when there are more. 87 # A repeated key is an error. 88 def load(text: String) -> Result<&2, &2, T.Error, V.Yaml>: 89 load.with(C.Core{}, C.limits(), text) 90 91 # Every document in +text+, under the Core schema. 92 def load.all(text: String) -> Result<&2, &2, T.Error, List<&2, V.Yaml>>: 93 load.all.with(C.Core{}, C.limits(), text) 94 95 # load, reading the YAML 1.1 types. 96 def load.v11(text: String) -> Result<&2, &2, T.Error, V.Yaml>: 97 load.with(C.Yaml11{}, C.limits(), text) 98 99 # load.all, reading the YAML 1.1 types. 100 def load.all.v11(text: String) -> Result<&2, &2, T.Error, List<&2, V.Yaml>>: 101 load.all.with(C.Yaml11{}, C.limits(), text) 102 103 # The default limits: C.Limits{size, depth, aliases, digits} with a text of 104 # at most 1048576 characters, collections nested at most 256 deep, aliases 105 # copying at most 100000 nodes into a document, and numbers in bases other 106 # than ten at most 1000 characters long. 107 def limits() -> C.Limits: 108 C.limits() 109 110 # The events of +text+, as the parser emits them, and the error that ended 111 # them early, if one did. Flow collections nest at most 256 deep. 112 def events(text: String) -> P.Parsed: 113 P.events(text, C.limits.depth(C.limits())) 114 115 # +v+ as YAML text in block style, which load reads back as +v+. 116 def dump(v: V.Yaml) -> String: 117 E.dump(v) 118 119 # Values as a stream of documents, each after "---". 120 def dump.all(vs: List<&2, V.Yaml>) -> String: 121 E.dump.all(vs)