dhall-ruby/README.md -rw-r--r-- 13.0 KiB View raw
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# Dhall for Ruby

This is a Ruby implementation of the Dhall configuration language.  Dhall is a powerful, but safe and non-Turing-complete configuration language.  For more information, see: https://dhall-lang.org

## Versioning and Standard Compliance

This project follows semantic versioning, and every tagged version claims to adhere to the version of the dhall-lang standard that is linked in the dhall-lang submodule.

For the purposes of considering what is a "breaking change" only the API as documented in this README is considered, regardless of any other exposed parts of the library.  Anything not documented here may change at any time, but backward-incompatible changes to anything documented here will be accompanied by a major-version increment.

## Installation

Add this line to your application's Gemfile:

    gem "dhall"

And then execute:

    bundle

Or install it yourself as:

    gem install dhall

## Load Expressions

    require "dhall"
    Dhall.load("1 + 1").then do |value|
      value # => #<Dhall::Natural value=2>
    end

    Dhall.load("./path/to/config.dhall").then do |config|
      # ... use config from file
    end

    Dhall.load("https://example.com/config.dhall").then do |config|
      # ... use config from URL
    end

`Dhall.load` will parse a Dhall expression, resolve imports, check that the types are correct, and fully normalize.  The result is returned as a `Promise` to enable using import resolvers that use async I/O.

### Non-Async Load

Wherever possible, you should use the `Promise` API and treat `Dhall.load` as an async operation.  *If* that is not possible, or *if* you know you are using a resolver that is not async, or *if* you know that there are no imports in your expression, you may use the escape hatch:

    Dhall.load("1 + 1").sync # => #<Dhall::Natural value=2>

**This will block the thread it is run from until the whole load operation is complete.  Never call `#sync` from an async context.**

### Timeout

It is possible for malicious entities to craft Dhall expressions which take an unreasonable amount of time to load.  To protect against this, `Dhall.load` implements a timeout mechanism with a default of 10 seconds.  You may specify an alternate timeout like so:

	Dhall.load("1 + 1", timeout: 1)               # 1 second timeout
	Dhall.load("1 + 1", timeout: 0.1)             # 0.1 second timeut
	Dhall.load("1 + 1", timeout: Float::INFINITY) # Never timeout

### Customizing Import Resolution

You may optionally pass `Dhall.load` a resolver that will be used to resolve all imports during the load process:

    Dhall.load(expr, resolver: some_resolver)

There are a few provided resolvers for you to choose from:

* `Dhall::Resolvers::Default` supports loading from http, https, local path, and IPFS sources.  IPFS imports will come from the local mountpoint, if present, with automatic fallbacks to the local gateway, if present, and finally a public gateway.
* `Dhall::Resolvers::Standard` should be used if you want strict dhall-lang standard compliance.  It supports loading from http, https, and local paths.
* `Dhall::Resolvers::LocalOnly` only allows imports from local paths.
* `Dhall::Resolvers::None` will not allow any imports.

It is possible to customize these options further, or provide your own resolver, but this is undocumented for now.

## Function

A Dhall expression may be a function, which can be used like any other Ruby proc:

    Dhall.load("\\(x: Natural) -> x + 1").then do |f|
      f.call(1)     # => #<Dhall::Natural value=2>
      f[1]          # => #<Dhall::Natural value=2>
      [1,2].map(&f) # => [#<Dhall::Natural value=2>, #<Dhall::Natural value=3>]
      f.to_proc     # => #<Proc:0xXXXX (lambda)>
    end

A curried function may be called either curried or uncurried:

    Dhall.load("\\(x: Natural) -> \\(y: Natural) -> x + y").then do |f|
      f.call(1).call(1) # => #<Dhall::Natural value=2>
      f.call(1, 1)      # => #<Dhall::Natural value=2>
    end

## Boolean

A Dhall expression may be a boolean, which supports some common messages:

    Dhall.load("True").then do |bool|
      bool & false             # => false
      bool | false             # => #<Dhall::Bool value=true>
      !bool                    # => #<Dhall::Bool value=false>
      bool === true            # => true
      bool.reduce(true, false) # => true
      bool.to_s                # => "True"
    end

If you need an actual instance of `TrueClass` or `FalseClass`, the suggested method is `bool === true`.

## Natural

A Dhall expression may be a natural (positive) number, which supports some common messages:

    Dhall.load("1").then do |nat|
      nat + 1   # => #<Dhall::Natural value=2>
      1 + nat   # => #<Dhall::Natural value=2>
      nat * 2   # => #<Dhall::Natural value=2>
      2 * nat   # => #<Dhall::Natural value=2>
      nat === 1 # => true
      nat.zero? # => false
      nat.even? # => false
      nat.odd?  # => true
      nat.pred  # => #<Dhall::Natural value=0>
      nat.to_s  # => "1"
      nat.to_i  # => 1
    end

## Integer

A Dhall expression may be an integer (positive or negative).  Dhall integers are opaque, and support fewer operations than naturals:

    Dhall.load("+1").then do |int|
      int === 1 # => true
      int.to_s  # "+1"
      int.to_i  # 1
    end

## Double

A Dhall expression may be a double-precision floating point number.  Dhall doubles are opaque, and support fewer operations than naturals:

    Dhall.load("1.0").then do |double|
      double === 1.0 # => true
      double.to_s    # "1.0"
      double.to_f    # 1.0
    end

## Text

A Dhall expression may be a string of text, which supports some common messages:

    Dhall.load("\"abc\"").then do |text|
      text === "abc" # => true
      text.to_s      # "abc"
    end

## Optional

A Dhall expression may be optionally present, like so:

    Dhall.load("Some 1").then do |some|
      some.map { |x| x + 1 }             # => #<Dhall::Optional value=#<Dhall::Natural value=2> value_type=nil>
      some.map(type: dhall_type) { ... } # => #<Dhall::Optional value=... value_type=dhall_type>
      some.reduce(nil) { |x| x }         # => #<Dhall::Natural value=1>
      some.to_s                          # => 1
    end

    Dhall.load("None Natural").then do |none|
      none.map { |x| x + 1 }             # => #<Dhall::OptionalNone ...>
      none.map(type: dhall_type) { ... } # => #<Dhall::OptionalNone value_type=dhall_type>
      none.reduce(nil) { |x| x }         # => nil
      none.to_s                          # => ""
    end

## List

A Dhall expression may be a list of other expressions.  Lists are `Enumerable` and support all operations that entails, with some special cases:

    Dhall.load("[1,2]").then do |list|
      list.map { |x| x + 1 }             # => #<Dhall::List elements=[#<Dhall::Natural value=2>, #<Dhall::Natural value=3>] element_type=nil>
      list.map(type: dhall_type) { ... } # => #<Dhall::List elements=[...] element_type=dhall_type>
      list.reduce(nil) { |x, _| x }      # => #<Dhall::Natural value=1>
      list.first                         # => #<Dhall::Optional value=#<Dhall::Natural value=1> value_type=...>
      list.last                          # => #<Dhall::Optional value=#<Dhall::Natural value=2> value_type=...>
      list[0]                            # => #<Dhall::Optional value=#<Dhall::Natural value=1> value_type=...>
      list[100]                          # => #<Dhall::OptionalNone value_type=...>
      list.reverse                       # => #<Dhall::List elements=[#<Dhall::Natural value=2>, #<Dhall::Natural value=1>] element_type=...>
      list.join(",")                     # => "1,2"
      list.to_a                          # => [#<Dhall::Natural value=1>, #<Dhall::Natural value=2>]
    end

## Record

A Dhall expression may be a record of keys mapped to other expressions.  Records are `Enumerable` and support many common operations:

    Dhall.load("{ a = 1 }").then do |rec|
      rec["a"]                      # => #<Dhall::Natural value=1>
      rec[:a]                       # => #<Dhall::Natural value=1>
      rec["b"]                      # => nil
      rec.fetch("a")                # => #<Dhall::Natural value=1>
      rec.fetch(:a)                 # => #<Dhall::Natural value=1>
      rec.fetch(:b)                 # => raise KeyError
      rec.dig(:a)                   # => #<Dhall::Natural value=1>
      rec.dig(:b)                   # => nil
      rec.slice(:a)                 # => #<Dhall::Record a=#<Dhall::Natural value=1>>
      rec.slice                     # => #<Dhall::EmptyRecord >
      rec.keys                      # => ["a"]
      rec.values                    # => [#<Dhall::Natural value=1>]
      rec.map { |k, v| [k, v + 1] } # => #<Dhall::Record a=#<Dhall::Natural value=2>>
      rec.merge(b: 2)               # => #<Dhall::Record a=#<Dhall::Natural value=1> b=#<Dhall::Natural value=2>>
      rec.deep_merge(b: 2)          # => #<Dhall::Record a=#<Dhall::Natural value=1> b=#<Dhall::Natural value=2>>
    end

## Union

A Dhall expression may be a union or enum.  These support both a way to handle each case, and a less safe method to extract a dynamically typed object:

    Dhall.load("< one | two >.one").then do |enum|
      enum.to_s                   # => "one"
      enum.reduce(one: 1, two: 2) # => 1
      enum.extract                # :one
    end

    Dhall.load("< Natural: Natural | Text: Text >.Natural 1").then do |union|
      union.to_s                                # => "1"
      union.reduce(Natural: :to_i, Text: :to_i) # => 1
      union.extract                             # => #<Dhall::Natural value=1>
    end

## Serializing Expressions

Dhall expressions may be serialized to a binary format for consumption by machines:

    expression.to_binary

If you are writing out an expression for later editing by a human, you should get [the Dhall command line tools](https://github.com/dhall-lang/dhall-haskell/releases) for your platform to make these easier to work with.  You can pretty print the binary format for human editing like so:

    dhall decode < path/to/binary/expression.dhallb

## Semantic Hash

Dhall expressions support creating a "semantic hash" that is the same for all expressions with the same normal form.  This makes it very useful as a cache key or an integrity check, since formatting changes to the source code will not change the hash:

    expression.cache_key

## Serializing Ruby Objects

You may wish to convert your existing Ruby objects to Dhall expressions.  This can be done using the AsDhall refinement:

    using Dhall::AsDhall
    1.as_dhall  # => #<Dhall::Natural value=1>
    {}.as_dhall # => #<Dhall::EmptyRecord >

Many methods on Dhall expressions call `#as_dhall` on their arguments, so you can define it on your own objects to produce a custom serialization.

If your object is already set up to customise its YAML serialization using `#encode_with`, the default `#as_dhall` implementation will use that.

When you want a full replacement for `YAML.safe_load` you can use the `Dhall::Coder` API:

    Dhall::Coder.dump(1)               # => "\x82\x0F\x01"
    Dhall::Coder.load("\x82\x0F\x01")  # => 1
    Dhall::Coder.dump(Object.new)      # => ArgumentError

    coder = Dhall::Coder.new(safe: Object)
    coder.load_async(coder.dump(Object.new)).then do |value|
      value # => #<Object:0x...>
    end

**Warning: calling `Dhall::Coder.load` or `Dhall::Coder#load` on an expression with imports will perform synchronous IO. See the warnings for the `#sync` method above.**

Both `Dhall::Coder` and all instances of `Dhall::Coder` are compatible to drop-in for `ActiveRecord::Base#serialize` like so in your models:

    serialize :column, Dhall::Coder

## Porting from YAML or JSON Configuration

To aid in converting your existing configurations or serialized data, there are included some experimental scripts:

    bundle exec json-to-dhall < path/to/config.json | dhall decode
    bundle exec yaml-to-dhall < path/to/config.yaml | dhall decode

## Getting Help

If you have any questions about this library, or wish to report a bug, please send email to: dev@singpolyma.net

## Contributing

If you wish to develop locally on this library, you will need to pull submodules and run make to generate the parser:

    git clone --recursive https://git.sr.ht/~singpolyma/dhall-ruby
    cd dhall-ruby
    make

Tests can be run with one of:

    make unit # Faster
    make test # Complete

If you have code or patches you wish to contribute, the maintainer's preferred mechanism is a git pull request.  Push your changes to a git repository somewhere, for example:

    git remote rename origin upstream
    git remote add origin git@git.sr.ht:~yourname/dhall-ruby
    git push -u origin master

Then generate the pull request:

    git fetch upstream master
    git request-pull -p upstream/master origin

And copy-paste the result into a plain-text email to: dev@singpolyma.net

You may alternately use a patch-based approach as described on https://git-send-email.io

Contributions follow an inbound=outbound model -- you (or your employer) keep all copyright on your patches, but agree to license them according to this project's COPYING file.