dhall 0.1.0 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +31 -4
- data/bin/dhall-compile +111 -0
- data/bin/json-to-dhall +1 -1
- data/bin/yaml-to-dhall +1 -1
- data/dhall.gemspec +5 -1
- data/lib/dhall.rb +25 -11
- data/lib/dhall/as_dhall.rb +132 -33
- data/lib/dhall/ast.rb +512 -210
- data/lib/dhall/binary.rb +102 -24
- data/lib/dhall/builtins.rb +227 -247
- data/lib/dhall/coder.rb +199 -0
- data/lib/dhall/normalize.rb +93 -48
- data/lib/dhall/parser.citrus +177 -83
- data/lib/dhall/parser.rb +199 -118
- data/lib/dhall/resolve.rb +200 -48
- data/lib/dhall/typecheck.rb +292 -129
- data/lib/dhall/types.rb +19 -0
- data/lib/dhall/util.rb +142 -38
- metadata +63 -4
- data/lib/dhall/visitor.rb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5b0efbcf23922a25863898fa6ac51fc625f5ecd1590f7b95858ea736de8e091b
|
4
|
+
data.tar.gz: 96685b1fca760158f9a84ca6c7b8a8fdc1d0203c1f25af94d989c492af82e533
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1785d0b615378c128b3729b6d4d11df4f4c8da9beb07435b265cf1ff8f38583e4abb1ee5877029c474451acd465df1361175fdb8fab0d32358740765f339072e
|
7
|
+
data.tar.gz: 1ab4e4a5bc480ca0774429ce56d6007c5efc8e3009e8219c91c71b18821e2453573d23d1ece119ebf9c36e369cacf0ffeb1d681e869044e98200354feed04d63
|
data/README.md
CHANGED
@@ -6,13 +6,13 @@ This is a Ruby implementation of the Dhall configuration language. Dhall is a p
|
|
6
6
|
|
7
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
8
|
|
9
|
-
For the purposes of considering what is a "breaking change" only the API as documented in this
|
9
|
+
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.
|
10
10
|
|
11
11
|
## Installation
|
12
12
|
|
13
13
|
Add this line to your application's Gemfile:
|
14
14
|
|
15
|
-
gem
|
15
|
+
gem "dhall"
|
16
16
|
|
17
17
|
And then execute:
|
18
18
|
|
@@ -45,7 +45,15 @@ Wherever possible, you should use the `Promise` API and treat `Dhall.load` as an
|
|
45
45
|
|
46
46
|
Dhall.load("1 + 1").sync # => #<Dhall::Natural value=2>
|
47
47
|
|
48
|
-
**This will block the thread it is run from until the whole load operation is complete. Never call
|
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
|
+
### Timeout
|
51
|
+
|
52
|
+
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:
|
53
|
+
|
54
|
+
Dhall.load("1 + 1", timeout: 1) # 1 second timeout
|
55
|
+
Dhall.load("1 + 1", timeout: 0.1) # 0.1 second timeut
|
56
|
+
Dhall.load("1 + 1", timeout: Float::INFINITY) # Never timeout
|
49
57
|
|
50
58
|
### Customizing Import Resolution
|
51
59
|
|
@@ -174,7 +182,7 @@ A Dhall expression may be a list of other expressions. Lists are `Enumerable` a
|
|
174
182
|
list[100] # => #<Dhall::OptionalNone value_type=...>
|
175
183
|
list.reverse # => #<Dhall::List elements=[#<Dhall::Natural value=2>, #<Dhall::Natural value=1>] element_type=...>
|
176
184
|
list.join(",") # => "1,2"
|
177
|
-
list.to_a # => [1
|
185
|
+
list.to_a # => [#<Dhall::Natural value=1>, #<Dhall::Natural value=2>]
|
178
186
|
end
|
179
187
|
|
180
188
|
## Record
|
@@ -241,6 +249,25 @@ You may wish to convert your existing Ruby objects to Dhall expressions. This c
|
|
241
249
|
|
242
250
|
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
251
|
|
252
|
+
If your object is already set up to customise its YAML serialization using `#encode_with`, the default `#as_dhall` implementation will use that.
|
253
|
+
|
254
|
+
When you want a full replacement for `YAML.safe_load` you can use the `Dhall::Coder` API:
|
255
|
+
|
256
|
+
Dhall::Coder.dump(1) # => "\x82\x0F\x01"
|
257
|
+
Dhall::Coder.load("\x82\x0F\x01") # => 1
|
258
|
+
Dhall::Coder.dump(Object.new) # => ArgumentError
|
259
|
+
|
260
|
+
coder = Dhall::Coder.new(safe: Object)
|
261
|
+
coder.load_async(coder.dump(Object.new)).then do |value|
|
262
|
+
value # => #<Object:0x...>
|
263
|
+
end
|
264
|
+
|
265
|
+
**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.**
|
266
|
+
|
267
|
+
Both `Dhall::Coder` and all instances of `Dhall::Coder` are compatible to drop-in for `ActiveRecord::Base#serialize` like so in your models:
|
268
|
+
|
269
|
+
serialize :column, Dhall::Coder
|
270
|
+
|
244
271
|
## Porting from YAML or JSON Configuration
|
245
272
|
|
246
273
|
To aid in converting your existing configurations or serialized data, there are included some experimental scripts:
|
data/bin/dhall-compile
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "dhall"
|
5
|
+
require "optparse"
|
6
|
+
|
7
|
+
@extension = ".dhallb"
|
8
|
+
|
9
|
+
def compile(source)
|
10
|
+
Dhall.load(
|
11
|
+
source,
|
12
|
+
timeout: Float::INFINITY,
|
13
|
+
resolver: Dhall::Resolvers::Default.new(
|
14
|
+
max_depth: Float::INFINITY
|
15
|
+
)
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
module FilenameWriter
|
20
|
+
def self.write(_, out, dhall)
|
21
|
+
warn out
|
22
|
+
out.dirname.mkpath
|
23
|
+
out.write(dhall.to_binary)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module CacheWriter
|
28
|
+
def self.write(output_directory, out, dhall)
|
29
|
+
base = "1220#{dhall.digest.hexdigest}"
|
30
|
+
out = out.dirname + base
|
31
|
+
if output_directory
|
32
|
+
out = output_directory + base
|
33
|
+
out.dirname.mkpath
|
34
|
+
end
|
35
|
+
warn out
|
36
|
+
out.write(dhall.to_cbor)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def compile_file(file_path, relative_to: Pathname.new("."))
|
41
|
+
$stderr.print "#{file_path} => "
|
42
|
+
out = file_path.sub_ext(@extension)
|
43
|
+
if @output_directory
|
44
|
+
out = @output_directory + out.relative_path_from(relative_to)
|
45
|
+
end
|
46
|
+
compile(file_path.expand_path).then do |dhall|
|
47
|
+
@writer.write(@output_directory, out, dhall)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
@writer = FilenameWriter
|
52
|
+
# rubocop:disable Metrics/BlockLength
|
53
|
+
opt_parser = OptionParser.new do |opts|
|
54
|
+
opts.banner = "Usage: dhall-compile [options] [-] [files_and_dirs]"
|
55
|
+
|
56
|
+
opts.on(
|
57
|
+
"-oDIRECTORY",
|
58
|
+
"--output-directory DIRECTORY",
|
59
|
+
"Write output to this directory"
|
60
|
+
) do |dir|
|
61
|
+
@output_directory = Pathname.new(dir)
|
62
|
+
end
|
63
|
+
|
64
|
+
opts.on(
|
65
|
+
"-e [EXTENSION]",
|
66
|
+
"--extension [EXTENSION]",
|
67
|
+
"Use this extension for files (default .dhallb)"
|
68
|
+
) do |ext|
|
69
|
+
@extension = ext ? ".#{ext}" : ""
|
70
|
+
end
|
71
|
+
|
72
|
+
opts.on(
|
73
|
+
"-c",
|
74
|
+
"--cache",
|
75
|
+
"Write output in standard dhall file cache format"
|
76
|
+
) do
|
77
|
+
@extension = ""
|
78
|
+
@writer = CacheWriter
|
79
|
+
end
|
80
|
+
|
81
|
+
opts.on("-h", "--help", "Show this usage information") do
|
82
|
+
warn opts
|
83
|
+
exit
|
84
|
+
end
|
85
|
+
end
|
86
|
+
# rubocop:enable Metrics/BlockLength
|
87
|
+
|
88
|
+
opt_parser.parse!
|
89
|
+
|
90
|
+
if ARGV.empty?
|
91
|
+
warn opt_parser
|
92
|
+
exit 0
|
93
|
+
end
|
94
|
+
|
95
|
+
ARGV.map(&Pathname.method(:new)).each do |path|
|
96
|
+
if !path.exist? && path.to_s == "-"
|
97
|
+
warn "Compiling STDIN to STDOUT"
|
98
|
+
compile(STDIN.read).then(&STDOUT.method(:write)).sync
|
99
|
+
elsif path.file?
|
100
|
+
compile_file(path, relative_to: path.dirname).sync
|
101
|
+
elsif path.directory?
|
102
|
+
warn "Recursively compiling #{path}"
|
103
|
+
path.find.flat_map do |child|
|
104
|
+
next if !child.file? || child.extname == ".dhallb"
|
105
|
+
compile_file(child, relative_to: path).sync
|
106
|
+
end
|
107
|
+
else
|
108
|
+
warn "#{path} may not exist"
|
109
|
+
exit 1
|
110
|
+
end
|
111
|
+
end
|
data/bin/json-to-dhall
CHANGED
data/bin/yaml-to-dhall
CHANGED
data/dhall.gemspec
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |spec|
|
4
4
|
spec.name = "dhall"
|
5
|
-
spec.version =
|
5
|
+
spec.version = `git describe --always --dirty`
|
6
6
|
spec.authors = ["Stephen Paul Weber"]
|
7
7
|
spec.email = ["dev@singpolyma.net"]
|
8
8
|
spec.license = "GPL-3.0"
|
@@ -25,12 +25,16 @@ Gem::Specification.new do |spec|
|
|
25
25
|
spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
|
26
26
|
spec.require_paths = ["lib"]
|
27
27
|
|
28
|
+
spec.add_dependency "base32", "~> 0.3.2"
|
28
29
|
spec.add_dependency "cbor", "~> 0.5.9.3"
|
29
30
|
spec.add_dependency "citrus", "~> 3.0"
|
31
|
+
spec.add_dependency "lazy_object", "~> 0.0.3"
|
32
|
+
spec.add_dependency "multihashes", "~> 0.1.3"
|
30
33
|
spec.add_dependency "promise.rb", "~> 0.7.4"
|
31
34
|
spec.add_dependency "value_semantics", "~> 3.0"
|
32
35
|
|
33
36
|
spec.add_development_dependency "abnf", "~> 0.0.1"
|
37
|
+
spec.add_development_dependency "minitest-fail-fast", "~> 0.1.0"
|
34
38
|
spec.add_development_dependency "simplecov", "~> 0.16.1"
|
35
39
|
spec.add_development_dependency "webmock", "~> 3.5"
|
36
40
|
end
|
data/lib/dhall.rb
CHANGED
@@ -4,34 +4,48 @@ require "dhall/as_dhall"
|
|
4
4
|
require "dhall/ast"
|
5
5
|
require "dhall/binary"
|
6
6
|
require "dhall/builtins"
|
7
|
+
require "dhall/coder"
|
7
8
|
require "dhall/normalize"
|
8
9
|
require "dhall/parser"
|
9
10
|
require "dhall/resolve"
|
10
11
|
require "dhall/typecheck"
|
12
|
+
require "dhall/types"
|
11
13
|
|
12
14
|
module Dhall
|
13
15
|
using Dhall::AsDhall
|
14
16
|
|
15
|
-
def self.load(
|
17
|
+
def self.load(
|
18
|
+
source,
|
19
|
+
resolver: Resolvers::Default.new,
|
20
|
+
timeout: 10
|
21
|
+
)
|
22
|
+
deadline = Util::Deadline.for_timeout(timeout)
|
16
23
|
Promise.resolve(nil).then {
|
17
|
-
load_raw(source).resolve(
|
24
|
+
load_raw(source.to_s, timeout: timeout).resolve(
|
25
|
+
resolver: resolver.with_deadline(deadline)
|
26
|
+
)
|
18
27
|
}.then do |resolved|
|
19
|
-
|
28
|
+
deadline.timeout_block do
|
29
|
+
TypeChecker.for(resolved).annotate(TypeChecker::Context.new).normalize
|
30
|
+
end
|
20
31
|
end
|
21
32
|
end
|
22
33
|
|
23
|
-
def self.load_raw(source)
|
24
|
-
|
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
|
34
|
+
def self.load_raw(source, timeout: 10)
|
35
|
+
source = Util.text_or_binary(source)
|
30
36
|
|
31
|
-
|
37
|
+
Util::Deadline.for_timeout(timeout).timeout_block do
|
38
|
+
if source.encoding == Encoding::BINARY
|
39
|
+
from_binary(source)
|
40
|
+
else
|
41
|
+
Parser.parse(source).value
|
42
|
+
end
|
43
|
+
end
|
32
44
|
end
|
33
45
|
|
34
46
|
def self.dump(o)
|
35
47
|
CBOR.encode(o.as_dhall)
|
36
48
|
end
|
49
|
+
|
50
|
+
class TimeoutException < StandardError; end
|
37
51
|
end
|
data/lib/dhall/as_dhall.rb
CHANGED
@@ -1,41 +1,108 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "ostruct"
|
4
|
+
require "psych"
|
4
5
|
|
5
6
|
module Dhall
|
6
7
|
module AsDhall
|
7
8
|
TAGS = {
|
8
|
-
::
|
9
|
+
::Array => "List",
|
9
10
|
::FalseClass => "Bool",
|
10
|
-
::Integer => "Integer",
|
11
11
|
::Float => "Double",
|
12
|
+
::Hash => "Record",
|
13
|
+
::Integer => "Integer",
|
14
|
+
::Integer => "Integer",
|
12
15
|
::NilClass => "None",
|
13
16
|
::String => "Text",
|
14
17
|
::TrueClass => "Bool"
|
15
18
|
}.freeze
|
16
19
|
|
17
|
-
def self.tag_for(o
|
20
|
+
def self.tag_for(o)
|
18
21
|
return "Natural" if o.is_a?(::Integer) && !o.negative?
|
19
22
|
|
20
23
|
TAGS.fetch(o.class) do
|
21
|
-
|
24
|
+
o.class.name
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class AnnotatedExpressionList
|
29
|
+
attr_reader :type
|
30
|
+
attr_reader :exprs
|
31
|
+
|
32
|
+
def self.from(type_annotation)
|
33
|
+
if type_annotation.nil?
|
34
|
+
new(nil, [nil])
|
35
|
+
else
|
36
|
+
new(type_annotation.type, [type_annotation.value])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def initialize(type, exprs)
|
41
|
+
@type = type
|
42
|
+
@exprs = exprs
|
43
|
+
end
|
44
|
+
|
45
|
+
def +(other)
|
46
|
+
raise "#{type} != #{other.type}" if type != other.type
|
47
|
+
self.class.new(type, exprs + other.exprs)
|
22
48
|
end
|
23
49
|
end
|
24
50
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
51
|
+
class UnionInferer
|
52
|
+
def initialize(tagged={})
|
53
|
+
@tagged = tagged
|
54
|
+
end
|
55
|
+
|
56
|
+
def union_type
|
57
|
+
UnionType.new(alternatives: Hash[@tagged.map { |k, v| [k, v.type] }])
|
58
|
+
end
|
59
|
+
|
60
|
+
def union_for(expr)
|
61
|
+
if expr.is_a?(Enum)
|
62
|
+
tag = expr.tag
|
63
|
+
expr = nil
|
64
|
+
else
|
65
|
+
tag = @tagged.keys.find { |k| @tagged[k].exprs.include?(expr) }
|
66
|
+
end
|
67
|
+
expr = expr.extract if expr.is_a?(Union)
|
68
|
+
Union.from(union_type, tag, expr)
|
69
|
+
end
|
70
|
+
|
71
|
+
def with(tag, type_annotation)
|
72
|
+
anno = AnnotatedExpressionList.from(type_annotation)
|
73
|
+
if @tagged.key?(tag) && @tagged[tag].type != anno.type
|
74
|
+
disambiguate_against(tag, anno)
|
75
|
+
else
|
76
|
+
self.class.new(@tagged.merge(tag => anno) { |_, x, y| x + y })
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def disambiguate_against(tag, anno)
|
81
|
+
self.class.new(
|
82
|
+
@tagged.reject { |k, _| k == tag }.merge(
|
83
|
+
"#{tag}_#{@tagged[tag].type.digest.hexdigest}" => @tagged[tag],
|
84
|
+
"#{tag}_#{anno.type.digest.hexdigest}" => anno
|
85
|
+
)
|
86
|
+
)
|
33
87
|
end
|
34
88
|
end
|
35
89
|
|
36
90
|
refine ::String do
|
37
91
|
def as_dhall
|
38
|
-
|
92
|
+
if encoding == Encoding::BINARY
|
93
|
+
bytes.as_dhall
|
94
|
+
else
|
95
|
+
Text.new(value: self)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
refine ::Symbol do
|
101
|
+
def as_dhall
|
102
|
+
Dhall::Enum.new(
|
103
|
+
tag: to_s,
|
104
|
+
alternatives: Dhall::UnionType.new(alternatives: {})
|
105
|
+
)
|
39
106
|
end
|
40
107
|
end
|
41
108
|
|
@@ -128,17 +195,28 @@ module Dhall
|
|
128
195
|
|
129
196
|
class Union
|
130
197
|
def initialize(values, exprs, types)
|
131
|
-
@
|
198
|
+
@tags, @types = values.zip(types).map { |(value, type)|
|
199
|
+
if type.is_a?(UnionType) && type.alternatives.length == 1
|
200
|
+
type.alternatives.to_a.first
|
201
|
+
else
|
202
|
+
[AsDhall.tag_for(value), type]
|
203
|
+
end
|
204
|
+
}.transpose
|
132
205
|
@exprs = exprs
|
133
|
-
@
|
206
|
+
@inferer = UnionInferer.new
|
134
207
|
end
|
135
208
|
|
136
209
|
def list
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
210
|
+
final_inferer =
|
211
|
+
@tags
|
212
|
+
.zip(@exprs, @types)
|
213
|
+
.reduce(@inferer) do |inferer, (tag, expr, type)|
|
214
|
+
inferer.with(
|
215
|
+
tag,
|
216
|
+
type.nil? ? nil : TypeAnnotation.new(value: expr, type: type)
|
217
|
+
)
|
218
|
+
end
|
219
|
+
List.new(elements: @exprs.map(&final_inferer.method(:union_for)))
|
142
220
|
end
|
143
221
|
end
|
144
222
|
end
|
@@ -165,31 +243,52 @@ module Dhall
|
|
165
243
|
|
166
244
|
refine ::OpenStruct do
|
167
245
|
def as_dhall
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
alternatives: UnionType.new(alternatives: {})
|
246
|
+
expr = to_h.as_dhall
|
247
|
+
type = TypeChecker.for(expr).annotate(TypeChecker::Context.new).type
|
248
|
+
Union.from(
|
249
|
+
UnionType.new(alternatives: { "OpenStruct" => type }),
|
250
|
+
"OpenStruct",
|
251
|
+
expr
|
175
252
|
)
|
176
253
|
end
|
177
254
|
end
|
178
255
|
|
179
|
-
refine ::
|
256
|
+
refine ::Psych::Coder do
|
180
257
|
def as_dhall
|
181
|
-
|
182
|
-
|
183
|
-
|
258
|
+
case type
|
259
|
+
when :seq
|
260
|
+
seq
|
261
|
+
when :map
|
262
|
+
map
|
263
|
+
else
|
264
|
+
scalar
|
265
|
+
end.as_dhall
|
266
|
+
end
|
267
|
+
end
|
184
268
|
|
185
|
-
|
269
|
+
refine ::Object do
|
270
|
+
def as_dhall
|
186
271
|
tag = self.class.name
|
272
|
+
expr = Util.psych_coder_from(tag, self).as_dhall
|
273
|
+
type = TypeChecker.for(expr).annotate(TypeChecker::Context.new).type
|
187
274
|
Union.from(
|
188
275
|
UnionType.new(alternatives: { tag => type }),
|
189
276
|
tag,
|
190
|
-
|
277
|
+
expr
|
191
278
|
)
|
192
279
|
end
|
193
280
|
end
|
281
|
+
|
282
|
+
refine ::Proc do
|
283
|
+
def as_dhall
|
284
|
+
FunctionProxy.new(self)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
refine ::Method do
|
289
|
+
def as_dhall
|
290
|
+
to_proc.as_dhall
|
291
|
+
end
|
292
|
+
end
|
194
293
|
end
|
195
294
|
end
|