Build YAML in Gleam!

Hello, Joe!

Louis Pilfold 7c37b6a2

+23
.github/workflows/test.yml
··· 1 + name: test 2 + 3 + on: 4 + push: 5 + branches: 6 + - master 7 + - main 8 + pull_request: 9 + 10 + jobs: 11 + test: 12 + runs-on: ubuntu-latest 13 + steps: 14 + - uses: actions/checkout@v3 15 + - uses: erlef/setup-beam@v1 16 + with: 17 + otp-version: "26.0.2" 18 + gleam-version: "1.0.0-rc1" 19 + rebar3-version: "3" 20 + # elixir-version: "1.15.4" 21 + - run: gleam deps download 22 + - run: gleam test 23 + - run: gleam format --check src test
+4
.gitignore
··· 1 + *.beam 2 + *.ez 3 + /build 4 + erl_crash.dump
+25
README.md
··· 1 + # cymbal 2 + 3 + [![Package Version](https://img.shields.io/hexpm/v/cymbal)](https://hex.pm/packages/cymbal) 4 + [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/cymbal/) 5 + 6 + ```sh 7 + gleam add cymbal 8 + ``` 9 + ```gleam 10 + import cymbal 11 + 12 + pub fn main() { 13 + // TODO: An example of the project in use 14 + } 15 + ``` 16 + 17 + Further documentation can be found at <https://hexdocs.pm/cymbal>. 18 + 19 + ## Development 20 + 21 + ```sh 22 + gleam run # Run the project 23 + gleam test # Run the tests 24 + gleam shell # Run an Erlang shell 25 + ```
+19
gleam.toml
··· 1 + name = "cymbal" 2 + version = "1.0.0" 3 + 4 + # Fill out these fields if you intend to generate HTML documentation or publish 5 + # your project to the Hex package manager. 6 + # 7 + # description = "" 8 + # licences = ["Apache-2.0"] 9 + # repository = { type = "github", user = "username", repo = "project" } 10 + # links = [{ title = "Website", href = "https://gleam.run" }] 11 + # 12 + # For a full reference of all the available options, you can have a look at 13 + # https://gleam.run/writing-gleam/gleam-toml/. 14 + 15 + [dependencies] 16 + gleam_stdlib = "~> 0.34 or ~> 1.0" 17 + 18 + [dev-dependencies] 19 + gleeunit = "~> 1.0"
+11
manifest.toml
··· 1 + # This file was generated by Gleam 2 + # You typically do not need to edit this file 3 + 4 + packages = [ 5 + { name = "gleam_stdlib", version = "0.34.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "1FB8454D2991E9B4C0C804544D8A9AD0F6184725E20D63C3155F0AEB4230B016" }, 6 + { name = "gleeunit", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D364C87AFEB26BDB4FB8A5ABDE67D635DC9FA52D6AB68416044C35B096C6882D" }, 7 + ] 8 + 9 + [requirements] 10 + gleam_stdlib = { version = "~> 0.34 or ~> 1.0" } 11 + gleeunit = { version = "~> 1.0" }
+213
src/cymbal.gleam
··· 1 + import gleam/int 2 + import gleam/float 3 + import gleam/string 4 + 5 + /// A YAML document which can be converted into a string using the `encode` 6 + /// function. 7 + /// 8 + pub opaque type Yaml { 9 + Int(Int) 10 + Float(Float) 11 + String(String) 12 + Array(List(Yaml)) 13 + Block(List(#(String, Yaml))) 14 + } 15 + 16 + /// Convert a YAML document into a string. 17 + /// 18 + pub fn encode(document: Yaml) -> String { 19 + let start = case own_line(document) { 20 + True -> "---" 21 + False -> "---\n" 22 + } 23 + 24 + en(start, 0, document) <> "\n" 25 + } 26 + 27 + /// Create a YAML document from an int. 28 + /// 29 + pub fn int(i: Int) -> Yaml { 30 + Int(i) 31 + } 32 + 33 + /// Create a YAML document from a float. 34 + /// 35 + pub fn float(i: Float) -> Yaml { 36 + Float(i) 37 + } 38 + 39 + /// Create a YAML document from a string. 40 + /// 41 + pub fn string(i: String) -> Yaml { 42 + String(i) 43 + } 44 + 45 + /// Create a YAML document from a list of YAML documents. 46 + /// 47 + pub fn array(i: List(Yaml)) -> Yaml { 48 + Array(i) 49 + } 50 + 51 + /// Create a YAML document from a list of named YAML values. 52 + /// 53 + pub fn block(i: List(#(String, Yaml))) -> Yaml { 54 + Block(i) 55 + } 56 + 57 + fn en(acc: String, in: Int, doc: Yaml) -> String { 58 + case doc { 59 + Int(i) -> acc <> int.to_string(i) 60 + Float(i) -> acc <> float.to_string(i) 61 + String(i) -> en_string(acc, i) 62 + Array(i) -> en_array(acc, in, i) 63 + Block(i) -> en_block(acc, in, i) 64 + } 65 + } 66 + 67 + fn en_array(acc: String, in: Int, docs: List(Yaml)) -> String { 68 + case docs { 69 + [] -> acc 70 + [doc, ..docs] -> 71 + acc 72 + |> string.append("\n") 73 + |> indent(in) 74 + |> string.append(case own_line(doc) { 75 + True -> "-" 76 + False -> "- " 77 + }) 78 + |> en(in + 1, doc) 79 + |> en_array(in, docs) 80 + } 81 + } 82 + 83 + fn en_block(acc: String, in: Int, docs: List(#(String, Yaml))) -> String { 84 + case docs { 85 + [] -> acc 86 + [#(name, doc), ..docs] -> 87 + acc 88 + |> string.append("\n") 89 + |> indent(in) 90 + |> en_string(name) 91 + |> string.append(case own_line(doc) { 92 + True -> ":" 93 + False -> ": " 94 + }) 95 + |> en(in + 1, doc) 96 + |> en_block(in, docs) 97 + } 98 + } 99 + 100 + fn indent(acc: String, i: Int) -> String { 101 + acc <> string.repeat(" ", i) 102 + } 103 + 104 + fn is_simple_string(s: String) -> Bool { 105 + case s { 106 + "0" <> _ 107 + | "1" <> _ 108 + | "2" <> _ 109 + | "3" <> _ 110 + | "4" <> _ 111 + | "5" <> _ 112 + | "6" <> _ 113 + | "7" <> _ 114 + | "8" <> _ 115 + | "9" <> _ -> False 116 + _ -> is_simple_string_rest(s) 117 + } 118 + } 119 + 120 + fn is_simple_string_rest(s: String) -> Bool { 121 + case s { 122 + "" -> True 123 + "0" <> s 124 + | "1" <> s 125 + | "2" <> s 126 + | "3" <> s 127 + | "4" <> s 128 + | "5" <> s 129 + | "6" <> s 130 + | "7" <> s 131 + | "8" <> s 132 + | "9" <> s 133 + | "a" <> s 134 + | "b" <> s 135 + | "c" <> s 136 + | "d" <> s 137 + | "e" <> s 138 + | "f" <> s 139 + | "g" <> s 140 + | "h" <> s 141 + | "i" <> s 142 + | "j" <> s 143 + | "k" <> s 144 + | "l" <> s 145 + | "m" <> s 146 + | "n" <> s 147 + | "o" <> s 148 + | "p" <> s 149 + | "q" <> s 150 + | "r" <> s 151 + | "s" <> s 152 + | "t" <> s 153 + | "u" <> s 154 + | "v" <> s 155 + | "w" <> s 156 + | "x" <> s 157 + | "y" <> s 158 + | "z" <> s 159 + | "A" <> s 160 + | "B" <> s 161 + | "C" <> s 162 + | "D" <> s 163 + | "E" <> s 164 + | "F" <> s 165 + | "G" <> s 166 + | "H" <> s 167 + | "I" <> s 168 + | "J" <> s 169 + | "K" <> s 170 + | "L" <> s 171 + | "M" <> s 172 + | "N" <> s 173 + | "O" <> s 174 + | "P" <> s 175 + | "Q" <> s 176 + | "R" <> s 177 + | "S" <> s 178 + | "T" <> s 179 + | "U" <> s 180 + | "V" <> s 181 + | "W" <> s 182 + | "X" <> s 183 + | "Y" <> s 184 + | "Z" <> s 185 + | "_" <> s -> is_simple_string_rest(s) 186 + _ -> False 187 + } 188 + } 189 + 190 + fn en_string(acc: String, i: String) -> String { 191 + case is_simple_string(i) { 192 + True -> acc <> i 193 + False -> en_quoted_string(acc, i) 194 + } 195 + } 196 + 197 + fn en_quoted_string(acc: String, i: String) -> String { 198 + acc 199 + <> "\"" 200 + <> { 201 + i 202 + |> string.replace("\\", "\\\\") 203 + |> string.replace("\"", "\\\"") 204 + } 205 + <> "\"" 206 + } 207 + 208 + fn own_line(doc: Yaml) -> Bool { 209 + case doc { 210 + Int(_) | Float(_) | String(_) -> False 211 + Array(_) | Block(_) -> True 212 + } 213 + }
+188
test/cymbal_test.gleam
··· 1 + import cymbal.{array, block, float, int, string} 2 + import gleeunit 3 + import gleeunit/should 4 + 5 + pub fn main() { 6 + gleeunit.main() 7 + } 8 + 9 + pub fn encode_int_test() { 10 + int(123) 11 + |> cymbal.encode 12 + |> should.equal( 13 + "--- 14 + 123 15 + ", 16 + ) 17 + } 18 + 19 + pub fn encode_float_test() { 20 + float(123.45) 21 + |> cymbal.encode 22 + |> should.equal( 23 + "--- 24 + 123.45 25 + ", 26 + ) 27 + } 28 + 29 + pub fn encode_string_test() { 30 + string("hello") 31 + |> cymbal.encode 32 + |> should.equal( 33 + "--- 34 + hello 35 + ", 36 + ) 37 + } 38 + 39 + pub fn encode_dash_string_test() { 40 + string("hello-world") 41 + |> cymbal.encode 42 + |> should.equal( 43 + "--- 44 + \"hello-world\" 45 + ", 46 + ) 47 + } 48 + 49 + pub fn encode_string_with_quote_test() { 50 + string("\"") 51 + |> cymbal.encode 52 + |> should.equal( 53 + "--- 54 + \"\\\"\" 55 + ", 56 + ) 57 + } 58 + 59 + pub fn encode_string_with_escaped_quote_test() { 60 + string("\\") 61 + |> cymbal.encode 62 + |> should.equal( 63 + "--- 64 + \"\\\\\" 65 + ", 66 + ) 67 + } 68 + 69 + pub fn encode_array_test() { 70 + array([ 71 + int(1), 72 + int(2), 73 + int(3), 74 + int(4), 75 + int(5), 76 + int(6), 77 + int(7), 78 + int(8), 79 + int(9), 80 + int(10), 81 + ]) 82 + |> cymbal.encode 83 + |> should.equal( 84 + "--- 85 + - 1 86 + - 2 87 + - 3 88 + - 4 89 + - 5 90 + - 6 91 + - 7 92 + - 8 93 + - 9 94 + - 10 95 + ", 96 + ) 97 + } 98 + 99 + pub fn encode_array_nested_test() { 100 + array([ 101 + int(1), 102 + int(2), 103 + array([ 104 + int(3), 105 + int(4), 106 + int(5), 107 + array([int(6), int(7)]), 108 + int(8), 109 + int(9), 110 + int(10), 111 + ]), 112 + ]) 113 + |> cymbal.encode 114 + |> should.equal( 115 + "--- 116 + - 1 117 + - 2 118 + - 119 + - 3 120 + - 4 121 + - 5 122 + - 123 + - 6 124 + - 7 125 + - 8 126 + - 9 127 + - 10 128 + ", 129 + ) 130 + } 131 + 132 + pub fn encode_block_test() { 133 + block([ 134 + #("it1", int(1)), 135 + #("it2", int(2)), 136 + #("it3", int(3)), 137 + #("it4", int(4)), 138 + #("it5", int(5)), 139 + ]) 140 + |> cymbal.encode 141 + |> should.equal( 142 + "--- 143 + it1: 1 144 + it2: 2 145 + it3: 3 146 + it4: 4 147 + it5: 5 148 + ", 149 + ) 150 + } 151 + 152 + pub fn encode_nested_block_test() { 153 + block([ 154 + #("it1", int(1)), 155 + #("it2", int(2)), 156 + #( 157 + "nested1", 158 + block([ 159 + #("it3", int(3)), 160 + #("it4", int(4)), 161 + #( 162 + "nested2", 163 + block([#("it3", int(3)), #("it4", int(4)), #("it5", int(5))]), 164 + ), 165 + #("it5", int(5)), 166 + ]), 167 + ), 168 + #("it6", int(6)), 169 + #("it7", int(7)), 170 + ]) 171 + |> cymbal.encode 172 + |> should.equal( 173 + "--- 174 + it1: 1 175 + it2: 2 176 + nested1: 177 + it3: 3 178 + it4: 4 179 + nested2: 180 + it3: 3 181 + it4: 4 182 + it5: 5 183 + it5: 5 184 + it6: 6 185 + it7: 7 186 + ", 187 + ) 188 + }