~singpolyma/dhall-ruby

ref: 2a3d4e2a5a2024f679162666427676293c1b851a dhall-ruby/lib/dhall/coder.rb -rw-r--r-- 3.6 KiB
2a3d4e2aStephen Paul Weber Hoist single-label enum into array 2 years ago
                                                                                
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
# frozen_string_literal: true

require "psych"

module Dhall
	class Coder
		JSON_LIKE = [
			::Array, ::Hash,
			::TrueClass, ::FalseClass, ::NilClass,
			::Integer, ::Float, ::String
		].freeze

		class Verifier
			def initialize(*classes)
				@classes = classes
				@matcher = ValueSemantics::Either.new(classes)
			end

			def verify_class(klass, op)
				if @classes.any? { |safe| klass <= safe }
					klass
				else
					raise ArgumentError, "#{op} does not match "\
					                     "#{@classes.inspect}: #{klass}"
				end
			end

			def verify(obj, op)
				if @matcher === obj
					obj
				else
					raise ArgumentError, "#{op} does not match "\
					                     "#{@classes.inspect}: #{obj.inspect}"
				end
			end
		end

		def self.load(source, transform_keys: :to_s)
			new.load(source, transform_keys: transform_keys)
		end

		def self.dump(obj)
			new.dump(obj)
		end

		def initialize(default: nil, safe: JSON_LIKE)
			@default = default
			@verifier = Verifier.new(*Array(safe))
			@verifier.verify(default, "default value")
		end

		def load_async(source, op="load_async", transform_keys: :to_s)
			return Promise.resolve(@default) if source.nil?
			return Promise.resolve(source) unless source.is_a?(String)

			Dhall.load(source).then do |expr|
				decode(expr, op, transform_keys: transform_keys)
			end
		end

		def load(source, transform_keys: :to_s)
			load_async(source, "load", transform_keys: transform_keys).sync
		end

		module ToRuby
			refine Expression do
				def to_ruby
					self
				end
			end

			refine Natural do
				alias_method :to_ruby, :to_i
			end

			refine Integer do
				alias_method :to_ruby, :to_i
			end

			refine Double do
				alias_method :to_ruby, :to_f
			end

			refine Text do
				alias_method :to_ruby, :to_s
			end

			refine Bool do
				def to_ruby
					self === true
				end
			end

			refine Record do
				def to_ruby(&decode)
					Hash[to_h.map { |k, v| [k, decode[v]] }]
				end
			end

			refine EmptyRecord do
				def to_ruby
					{}
				end
			end

			refine List do
				def to_ruby(&decode)
					to_a.map(&decode)
				end
			end

			refine Optional do
				def to_ruby(&decode)
					reduce(nil, &decode)
				end
			end

			refine Function do
				def to_ruby(&decode)
					->(*args) { decode[expr.call(*args)] }
				end
			end

			refine Enum do
				def to_ruby
					extract == :None ? nil : extract
				end
			end

			refine Union do
				def to_ruby
					if tag.match(/\A\p{Upper}/) && Object.const_defined?(tag)
						yield extract, Object.const_get(tag)
					else
						yield extract
					end
				end
			end

			refine TypeAnnotation do
				def to_ruby
					yield value
				end
			end
		end

		using ToRuby

		module InitWith
			refine Object do
				def init_with(coder)
					coder.map.each do |k, v|
						instance_variable_set(:"@#{k}", v)
					end
				end
			end
		end

		using InitWith

		def revive(klass, expr, op="revive", transform_keys: :to_s)
			@verifier.verify_class(klass, op)
			return klass.from_dhall(expr) if klass.respond_to?(:from_dhall)

			klass.allocate.tap do |o|
				o.init_with(Util.psych_coder_for(
					klass.name,
					decode(expr, op, transform_keys: transform_keys)
				))
			end
		end

		def decode(expr, op="decode", klass: nil, transform_keys: :to_s)
			return revive(klass, expr, op, transform_keys: transform_keys) if klass
			@verifier.verify(
				Util.transform_keys(
					expr.to_ruby { |dexpr, dklass|
						decode(dexpr, op, klass: dklass, transform_keys: transform_keys)
					},
					&transform_keys
				),
				op
			)
		end

		def dump(obj)
			return if obj.nil?

			Dhall.dump(@verifier.verify(obj, "dump"))
		end
	end
end