dhall 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,283 @@
1
+ # Dhall for Ruby
2
+
3
+ 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
4
+
5
+ ## Versioning and Standard Compliance
6
+
7
+ 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.
8
+
9
+ For the purposes of considering what is a "breaking change" only the API as documented in this documentation 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.
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ gem 'dhall'
16
+
17
+ And then execute:
18
+
19
+ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ gem install dhall
24
+
25
+ ## Load Expressions
26
+
27
+ require "dhall"
28
+ Dhall.load("1 + 1").then do |value|
29
+ value # => #<Dhall::Natural value=2>
30
+ end
31
+
32
+ Dhall.load("./path/to/config.dhall").then do |config|
33
+ # ... use config from file
34
+ end
35
+
36
+ Dhall.load("https://example.com/config.dhall").then do |config|
37
+ # ... use config from URL
38
+ end
39
+
40
+ `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.
41
+
42
+ ### Non-Async Load
43
+
44
+ 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:
45
+
46
+ Dhall.load("1 + 1").sync # => #<Dhall::Natural value=2>
47
+
48
+ **This will block the thread it is run from until the whole load operation is complete. Never call #sync from an async context.**
49
+
50
+ ### Customizing Import Resolution
51
+
52
+ You may optionally pass `Dhall.load` a resolver that will be used to resolve all imports during the load process:
53
+
54
+ Dhall.load(expr, resolver: some_resolver)
55
+
56
+ There are a few provided resolvers for you to choose from:
57
+
58
+ * `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.
59
+ * `Dhall::Resolvers::Standard` should be used if you want strict dhall-lang standard compliance. It supports loading from http, https, and local paths.
60
+ * `Dhall::Resolvers::LocalOnly` only allows imports from local paths.
61
+ * `Dhall::Resolvers::None` will not allow any imports.
62
+
63
+ It is possible to customize these options further, or provide your own resolver, but this is undocumented for now.
64
+
65
+ ## Function
66
+
67
+ A Dhall expression may be a function, which can be used like any other Ruby proc:
68
+
69
+ Dhall.load("\\(x: Natural) -> x + 1").then do |f|
70
+ f.call(1) # => #<Dhall::Natural value=2>
71
+ f[1] # => #<Dhall::Natural value=2>
72
+ [1,2].map(&f) # => [#<Dhall::Natural value=2>, #<Dhall::Natural value=3>]
73
+ f.to_proc # => #<Proc:0xXXXX (lambda)>
74
+ end
75
+
76
+ A curried function may be called either curried or uncurried:
77
+
78
+ Dhall.load("\\(x: Natural) -> \\(y: Natural) -> x + y").then do |f|
79
+ f.call(1).call(1) # => #<Dhall::Natural value=2>
80
+ f.call(1, 1) # => #<Dhall::Natural value=2>
81
+ end
82
+
83
+ ## Boolean
84
+
85
+ A Dhall expression may be a boolean, which supports some common messages:
86
+
87
+ Dhall.load("True").then do |bool|
88
+ bool & false # => false
89
+ bool | false # => #<Dhall::Bool value=true>
90
+ !bool # => #<Dhall::Bool value=false>
91
+ bool === true # => true
92
+ bool.reduce(true, false) # => true
93
+ bool.to_s # => "True"
94
+ end
95
+
96
+ If you need an actual instance of `TrueClass` or `FalseClass`, the suggested method is `bool === true`.
97
+
98
+ ## Natural
99
+
100
+ A Dhall expression may be a natural (positive) number, which supports some common messages:
101
+
102
+ Dhall.load("1").then do |nat|
103
+ nat + 1 # => #<Dhall::Natural value=2>
104
+ 1 + nat # => #<Dhall::Natural value=2>
105
+ nat * 2 # => #<Dhall::Natural value=2>
106
+ 2 * nat # => #<Dhall::Natural value=2>
107
+ nat === 1 # => true
108
+ nat.zero? # => false
109
+ nat.even? # => false
110
+ nat.odd? # => true
111
+ nat.pred # => #<Dhall::Natural value=0>
112
+ nat.to_s # => "1"
113
+ nat.to_i # => 1
114
+ end
115
+
116
+ ## Integer
117
+
118
+ A Dhall expression may be an integer (positive or negative). Dhall integers are opaque, and support fewer operations than naturals:
119
+
120
+ Dhall.load("+1").then do |int|
121
+ int === 1 # => true
122
+ int.to_s # "+1"
123
+ int.to_i # 1
124
+ end
125
+
126
+ ## Double
127
+
128
+ A Dhall expression may be a double-precision floating point number. Dhall doubles are opaque, and support fewer operations than naturals:
129
+
130
+ Dhall.load("1.0").then do |double|
131
+ double === 1.0 # => true
132
+ double.to_s # "1.0"
133
+ double.to_f # 1.0
134
+ end
135
+
136
+ ## Text
137
+
138
+ A Dhall expression may be a string of text, which supports some common messages:
139
+
140
+ Dhall.load("\"abc\"").then do |text|
141
+ text === "abc" # => true
142
+ text.to_s # "abc"
143
+ end
144
+
145
+ ## Optional
146
+
147
+ A Dhall expression may be optionally present, like so:
148
+
149
+ Dhall.load("Some 1").then do |some|
150
+ some.map { |x| x + 1 } # => #<Dhall::Optional value=#<Dhall::Natural value=2> value_type=nil>
151
+ some.map(type: dhall_type) { ... } # => #<Dhall::Optional value=... value_type=dhall_type>
152
+ some.reduce(nil) { |x| x } # => #<Dhall::Natural value=1>
153
+ some.to_s # => 1
154
+ end
155
+
156
+ Dhall.load("None Natural").then do |none|
157
+ none.map { |x| x + 1 } # => #<Dhall::OptionalNone ...>
158
+ none.map(type: dhall_type) { ... } # => #<Dhall::OptionalNone value_type=dhall_type>
159
+ none.reduce(nil) { |x| x } # => nil
160
+ none.to_s # => ""
161
+ end
162
+
163
+ ## List
164
+
165
+ A Dhall expression may be a list of other expressions. Lists are `Enumerable` and support all operations that entails, with some special cases:
166
+
167
+ Dhall.load("[1,2]").then do |list|
168
+ list.map { |x| x + 1 } # => #<Dhall::List elements=[#<Dhall::Natural value=2>, #<Dhall::Natural value=3>] element_type=nil>
169
+ list.map(type: dhall_type) { ... } # => #<Dhall::List elements=[...] element_type=dhall_type>
170
+ list.reduce(nil) { |x, _| x } # => #<Dhall::Natural value=1>
171
+ list.first # => #<Dhall::Optional value=#<Dhall::Natural value=1> value_type=...>
172
+ list.last # => #<Dhall::Optional value=#<Dhall::Natural value=2> value_type=...>
173
+ list[0] # => #<Dhall::Optional value=#<Dhall::Natural value=1> value_type=...>
174
+ list[100] # => #<Dhall::OptionalNone value_type=...>
175
+ list.reverse # => #<Dhall::List elements=[#<Dhall::Natural value=2>, #<Dhall::Natural value=1>] element_type=...>
176
+ list.join(",") # => "1,2"
177
+ list.to_a # => [1,2]
178
+ end
179
+
180
+ ## Record
181
+
182
+ A Dhall expression may be a record of keys mapped to other expressions. Records are `Enumerable` and support many common operations:
183
+
184
+ Dhall.load("{ a = 1 }").then do |rec|
185
+ rec["a"] # => #<Dhall::Natural value=1>
186
+ rec[:a] # => #<Dhall::Natural value=1>
187
+ rec["b"] # => nil
188
+ rec.fetch("a") # => #<Dhall::Natural value=1>
189
+ rec.fetch(:a) # => #<Dhall::Natural value=1>
190
+ rec.fetch(:b) # => raise KeyError
191
+ rec.dig(:a) # => #<Dhall::Natural value=1>
192
+ rec.dig(:b) # => nil
193
+ rec.slice(:a) # => #<Dhall::Record a=#<Dhall::Natural value=1>>
194
+ rec.slice # => #<Dhall::EmptyRecord >
195
+ rec.keys # => ["a"]
196
+ rec.values # => [#<Dhall::Natural value=1>]
197
+ rec.map { |k, v| [k, v + 1] } # => #<Dhall::Record a=#<Dhall::Natural value=2>>
198
+ rec.merge(b: 2) # => #<Dhall::Record a=#<Dhall::Natural value=1> b=#<Dhall::Natural value=2>>
199
+ rec.deep_merge(b: 2) # => #<Dhall::Record a=#<Dhall::Natural value=1> b=#<Dhall::Natural value=2>>
200
+ end
201
+
202
+ ## Union
203
+
204
+ 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:
205
+
206
+ Dhall.load("< one | two >.one").then do |enum|
207
+ enum.to_s # => "one"
208
+ enum.reduce(one: 1, two: 2) # => 1
209
+ enum.extract # :one
210
+ end
211
+
212
+ Dhall.load("< Natural: Natural | Text: Text >.Natural 1").then do |union|
213
+ union.to_s # => "1"
214
+ union.reduce(Natural: :to_i, Text: :to_i) # => 1
215
+ union.extract # => #<Dhall::Natural value=1>
216
+ end
217
+
218
+ ## Serializing Expressions
219
+
220
+ Dhall expressions may be serialized to a binary format for consumption by machines:
221
+
222
+ expression.to_binary
223
+
224
+ 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:
225
+
226
+ dhall decode < path/to/binary/expression.dhallb
227
+
228
+ ## Semantic Hash
229
+
230
+ 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:
231
+
232
+ expression.cache_key
233
+
234
+ ## Serializing Ruby Objects
235
+
236
+ You may wish to convert your existing Ruby objects to Dhall expressions. This can be done using the AsDhall refinement:
237
+
238
+ using Dhall::AsDhall
239
+ 1.as_dhall # => #<Dhall::Natural value=1>
240
+ {}.as_dhall # => #<Dhall::EmptyRecord >
241
+
242
+ 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.
243
+
244
+ ## Porting from YAML or JSON Configuration
245
+
246
+ To aid in converting your existing configurations or serialized data, there are included some experimental scripts:
247
+
248
+ bundle exec json-to-dhall < path/to/config.json | dhall decode
249
+ bundle exec yaml-to-dhall < path/to/config.yaml | dhall decode
250
+
251
+ ## Getting Help
252
+
253
+ If you have any questions about this library, or wish to report a bug, please send email to: dev@singpolyma.net
254
+
255
+ ## Contributing
256
+
257
+ If you wish to develop locally on this library, you will need to pull submodules and run make to generate the parser:
258
+
259
+ git clone --recursive https://git.sr.ht/~singpolyma/dhall-ruby
260
+ cd dhall-ruby
261
+ make
262
+
263
+ Tests can be run with one of:
264
+
265
+ make unit # Faster
266
+ make test # Complete
267
+
268
+ 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:
269
+
270
+ git remote rename origin upstream
271
+ git remote add origin git@git.sr.ht:~yourname/dhall-ruby
272
+ git push -u origin master
273
+
274
+ Then generate the pull request:
275
+
276
+ git fetch upstream master
277
+ git request-pull -p upstream/master origin
278
+
279
+ And copy-paste the result into a plain-text email to: dev@singpolyma.net
280
+
281
+ You may alternately use a patch-based approach as described on https://git-send-email.io
282
+
283
+ 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.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "dhall"
5
+ require "json"
6
+ using Dhall::AsDhall
7
+
8
+ STDOUT.write(CBOR.encode(JSON.parse(STDIN.read).as_dhall))
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "dhall"
5
+ require "yaml"
6
+ using Dhall::AsDhall
7
+
8
+ STDOUT.write(CBOR.encode(YAML.safe_load(STDIN.read).as_dhall))
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "dhall"
5
+ spec.version = "0.1.0"
6
+ spec.authors = ["Stephen Paul Weber"]
7
+ spec.email = ["dev@singpolyma.net"]
8
+ spec.license = "GPL-3.0"
9
+
10
+ spec.summary = "The non-repetitive alternative to YAML, in Ruby"
11
+ spec.description = "This is a Ruby implementation of the Dhall " \
12
+ "configuration language. Dhall is a powerful, " \
13
+ "but safe and non-Turing-complete configuration " \
14
+ "language. For more information, see: " \
15
+ "https://dhall-lang.org"
16
+ spec.homepage = "https://git.sr.ht/~singpolyma/dhall-ruby"
17
+
18
+ spec.files =
19
+ ["lib/dhall/parser.citrus"] +
20
+ `git ls-files -z`.split("\x00".b).reject do |f|
21
+ f.start_with?(".", "test/", "scripts/") ||
22
+ f == "Makefile" || f == "Gemfile"
23
+ end
24
+ spec.bindir = "bin"
25
+ spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
26
+ spec.require_paths = ["lib"]
27
+
28
+ spec.add_dependency "cbor", "~> 0.5.9.3"
29
+ spec.add_dependency "citrus", "~> 3.0"
30
+ spec.add_dependency "promise.rb", "~> 0.7.4"
31
+ spec.add_dependency "value_semantics", "~> 3.0"
32
+
33
+ spec.add_development_dependency "abnf", "~> 0.0.1"
34
+ spec.add_development_dependency "simplecov", "~> 0.16.1"
35
+ spec.add_development_dependency "webmock", "~> 3.5"
36
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dhall/as_dhall"
4
+ require "dhall/ast"
5
+ require "dhall/binary"
6
+ require "dhall/builtins"
7
+ require "dhall/normalize"
8
+ require "dhall/parser"
9
+ require "dhall/resolve"
10
+ require "dhall/typecheck"
11
+
12
+ module Dhall
13
+ using Dhall::AsDhall
14
+
15
+ def self.load(source, resolver: Resolvers::Default.new)
16
+ Promise.resolve(nil).then {
17
+ load_raw(source).resolve(resolver: resolver)
18
+ }.then do |resolved|
19
+ TypeChecker.for(resolved).annotate(TypeChecker::Context.new).normalize
20
+ end
21
+ end
22
+
23
+ def self.load_raw(source)
24
+ begin
25
+ return from_binary(source) if source.encoding == Encoding::BINARY
26
+ rescue Exception # rubocop:disable Lint/RescueException
27
+ # Parsing CBOR failed, so guess this is source text in standard UTF-8
28
+ return load_raw(source.force_encoding("UTF-8"))
29
+ end
30
+
31
+ Parser.parse(source.encode("UTF-8")).value
32
+ end
33
+
34
+ def self.dump(o)
35
+ CBOR.encode(o.as_dhall)
36
+ end
37
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ostruct"
4
+
5
+ module Dhall
6
+ module AsDhall
7
+ TAGS = {
8
+ ::Integer => "Integer",
9
+ ::FalseClass => "Bool",
10
+ ::Integer => "Integer",
11
+ ::Float => "Double",
12
+ ::NilClass => "None",
13
+ ::String => "Text",
14
+ ::TrueClass => "Bool"
15
+ }.freeze
16
+
17
+ def self.tag_for(o, type)
18
+ return "Natural" if o.is_a?(::Integer) && !o.negative?
19
+
20
+ TAGS.fetch(o.class) do
21
+ "#{o.class.name}_#{type.digest.hexdigest}"
22
+ end
23
+ end
24
+
25
+ def self.union_of(values_and_types)
26
+ z = [UnionType.new(alternatives: {}), []]
27
+ values_and_types.reduce(z) do |(ut, tags), (v, t)|
28
+ tag = tag_for(v, t)
29
+ [
30
+ ut.merge(UnionType.new(alternatives: { tag => t })),
31
+ tags + [tag]
32
+ ]
33
+ end
34
+ end
35
+
36
+ refine ::String do
37
+ def as_dhall
38
+ Text.new(value: self)
39
+ end
40
+ end
41
+
42
+ refine ::Integer do
43
+ def as_dhall
44
+ if negative?
45
+ Integer.new(value: self)
46
+ else
47
+ Natural.new(value: self)
48
+ end
49
+ end
50
+ end
51
+
52
+ refine ::Float do
53
+ def as_dhall
54
+ Double.new(value: self)
55
+ end
56
+ end
57
+
58
+ refine ::TrueClass do
59
+ def as_dhall
60
+ Bool.new(value: true)
61
+ end
62
+ end
63
+
64
+ refine ::FalseClass do
65
+ def as_dhall
66
+ Bool.new(value: false)
67
+ end
68
+ end
69
+
70
+ refine ::NilClass do
71
+ def as_dhall
72
+ raise(
73
+ "Cannot call NilClass#as_dhall directly, " \
74
+ "you probably want to create a Dhall::OptionalNone yourself."
75
+ )
76
+ end
77
+ end
78
+
79
+ module ExpressionList
80
+ def self.for(values, exprs)
81
+ types = exprs.map(&TypeChecker.method(:type_of))
82
+
83
+ if types.empty?
84
+ Empty
85
+ elsif types.include?(nil) && types.uniq.length <= 2
86
+ Optional
87
+ elsif types.uniq.length == 1
88
+ Mono
89
+ else
90
+ Union
91
+ end.new(values, exprs, types)
92
+ end
93
+
94
+ class Empty
95
+ def initialize(*); end
96
+
97
+ def list
98
+ EmptyList.new(element_type: UnionType.new(alternatives: {}))
99
+ end
100
+ end
101
+
102
+ class Optional
103
+ def initialize(_, exprs, types)
104
+ @type = types.compact.first
105
+ @exprs = exprs
106
+ end
107
+
108
+ def list
109
+ List.new(elements: @exprs.map do |x|
110
+ if x.nil?
111
+ Dhall::OptionalNone.new(value_type: @type)
112
+ else
113
+ Dhall::Optional.new(value: x)
114
+ end
115
+ end)
116
+ end
117
+ end
118
+
119
+ class Mono
120
+ def initialize(_, exprs, _)
121
+ @exprs = exprs
122
+ end
123
+
124
+ def list
125
+ List.new(elements: @exprs)
126
+ end
127
+ end
128
+
129
+ class Union
130
+ def initialize(values, exprs, types)
131
+ @values = values
132
+ @exprs = exprs
133
+ @types = types
134
+ end
135
+
136
+ def list
137
+ ut, tags = AsDhall.union_of(@values.zip(@types))
138
+
139
+ List.new(elements: @exprs.zip(tags).map do |(expr, tag)|
140
+ Dhall::Union.from(ut, tag, expr)
141
+ end)
142
+ end
143
+ end
144
+ end
145
+
146
+ refine ::Array do
147
+ def as_dhall
148
+ ExpressionList.for(self, map { |x| x&.as_dhall }).list
149
+ end
150
+ end
151
+
152
+ refine ::Hash do
153
+ def as_dhall
154
+ if empty?
155
+ EmptyRecord.new
156
+ else
157
+ Record.new(record: Hash[
158
+ reject { |_, v| v.nil? }
159
+ .map { |k, v| [k.to_s, v.as_dhall] }
160
+ .sort
161
+ ])
162
+ end
163
+ end
164
+ end
165
+
166
+ refine ::OpenStruct do
167
+ def as_dhall
168
+ annotation = TypeChecker
169
+ .for(to_h.as_dhall)
170
+ .annotate(TypeChecker::Context.new)
171
+ Union.new(
172
+ tag: "OpenStruct",
173
+ value: annotation,
174
+ alternatives: UnionType.new(alternatives: {})
175
+ )
176
+ end
177
+ end
178
+
179
+ refine ::Object do
180
+ def as_dhall
181
+ ivars = instance_variables.each_with_object({}) { |ivar, h|
182
+ h[ivar.to_s[1..-1]] = instance_variable_get(ivar)
183
+ }.as_dhall
184
+
185
+ type = TypeChecker.for(ivars).annotate(TypeChecker::Context.new).type
186
+ tag = self.class.name
187
+ Union.from(
188
+ UnionType.new(alternatives: { tag => type }),
189
+ tag,
190
+ ivars
191
+ )
192
+ end
193
+ end
194
+ end
195
+ end