lazy_graph 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rubocop.yml +81 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +562 -0
- data/Rakefile +4 -0
- data/examples/performance_tests.rb +117 -0
- data/lib/lazy_graph/builder/dsl.rb +315 -0
- data/lib/lazy_graph/builder.rb +138 -0
- data/lib/lazy_graph/builder_group.rb +57 -0
- data/lib/lazy_graph/context.rb +60 -0
- data/lib/lazy_graph/graph.rb +73 -0
- data/lib/lazy_graph/hash_utils.rb +87 -0
- data/lib/lazy_graph/lazy-graph.json +148 -0
- data/lib/lazy_graph/missing_value.rb +26 -0
- data/lib/lazy_graph/node/array_node.rb +67 -0
- data/lib/lazy_graph/node/derived_rules.rb +196 -0
- data/lib/lazy_graph/node/node_properties.rb +64 -0
- data/lib/lazy_graph/node/object_node.rb +113 -0
- data/lib/lazy_graph/node.rb +316 -0
- data/lib/lazy_graph/path_parser/path.rb +46 -0
- data/lib/lazy_graph/path_parser/path_group.rb +12 -0
- data/lib/lazy_graph/path_parser/path_part.rb +13 -0
- data/lib/lazy_graph/path_parser.rb +211 -0
- data/lib/lazy_graph/server.rb +86 -0
- data/lib/lazy_graph/stack_pointer.rb +56 -0
- data/lib/lazy_graph/version.rb +5 -0
- data/lib/lazy_graph.rb +32 -0
- data/logo.png +0 -0
- metadata +200 -0
@@ -0,0 +1,315 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LazyGraph
|
4
|
+
class Builder
|
5
|
+
# This module defines the DSL for building Lazy Graph JSON schemas.
|
6
|
+
# Supported helpers
|
7
|
+
# * object :name, **opts, &blk
|
8
|
+
# * object_conditional :name, **opts, &blk
|
9
|
+
# ⌙ matches &blk
|
10
|
+
# * array :name, **opts, &blk
|
11
|
+
# ⌙ items &blk
|
12
|
+
# * <primitive> :name, **opts, &blk
|
13
|
+
# * date :name, **opts, &blk
|
14
|
+
# * decimal :name, **opts, &blk
|
15
|
+
# * timestamp :name, **opts, &blk
|
16
|
+
# * time :name, **opts, &blk
|
17
|
+
#
|
18
|
+
module DSL
|
19
|
+
def additional_properties(value)
|
20
|
+
schema[:additionalProperties] = value
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
def required(*keys)
|
25
|
+
(schema[:required] ||= []).concat(keys.map(&:to_s)).uniq!
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def default(value)
|
30
|
+
schema[:default] = \
|
31
|
+
if value.is_a?(Hash)
|
32
|
+
HashUtils.deep_merge(schema.fetch(:default, {}), value)
|
33
|
+
elsif value.is_a?(Array)
|
34
|
+
schema.fetch(:default, []).concat(value).uniq!
|
35
|
+
else
|
36
|
+
value
|
37
|
+
end
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
def set_pattern_property(pattern, value)
|
42
|
+
pattern = pattern.to_s
|
43
|
+
properties = schema[:patternProperties] ||= {}
|
44
|
+
properties[pattern] = \
|
45
|
+
if properties.key?(pattern) && %i[object array].include?(properties[pattern][:type])
|
46
|
+
HashUtils.deep_merge(properties[pattern], value, key)
|
47
|
+
else
|
48
|
+
value
|
49
|
+
end
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
def set_property(key, value)
|
54
|
+
key = key.to_sym
|
55
|
+
properties = schema[:properties] ||= {}
|
56
|
+
properties[key] = \
|
57
|
+
if properties.key?(key) && %i[object array].include?(properties[key][:type])
|
58
|
+
HashUtils.deep_merge(properties[key], value, key)
|
59
|
+
else
|
60
|
+
value
|
61
|
+
end
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
def object_conditional(
|
66
|
+
name = nil, required: false, pattern_property: false, rule: nil,
|
67
|
+
default: nil, description: nil, **opts, &blk
|
68
|
+
)
|
69
|
+
new_object = {
|
70
|
+
type: :object,
|
71
|
+
properties: {},
|
72
|
+
additionalProperties: false,
|
73
|
+
**(!default.nil? ? { default: default } : {}),
|
74
|
+
**(description ? { description: description } : {}),
|
75
|
+
**opts
|
76
|
+
}
|
77
|
+
@prev_match_cases = @match_cases
|
78
|
+
@match_cases = []
|
79
|
+
|
80
|
+
yields(new_object, &blk)
|
81
|
+
|
82
|
+
object_names = @match_cases.map do |match_case|
|
83
|
+
rule = rule_from_when(match_case[:when_clause])
|
84
|
+
set_property(match_case[:name], { type: :object, rule: rule, **match_case[:schema] })
|
85
|
+
match_case[:name]
|
86
|
+
end
|
87
|
+
|
88
|
+
new_object[:rule] = rule_from_first_of(object_names)
|
89
|
+
@match_cases = @prev_match_cases
|
90
|
+
required(name) if required && default.nil? && rule.nil?
|
91
|
+
pattern_property ? set_pattern_property(name, new_object) : set_property(name, new_object)
|
92
|
+
end
|
93
|
+
|
94
|
+
def matches(name, invisible: true, **when_clause, &blk)
|
95
|
+
@match_cases << { name: name, when_clause: when_clause, schema: { invisible: invisible } }
|
96
|
+
yields(@match_cases.last[:schema], &blk)
|
97
|
+
end
|
98
|
+
|
99
|
+
def object(
|
100
|
+
name = nil, required: false, pattern_property: false, rule: nil,
|
101
|
+
default: nil, description: nil, **opts, &blk
|
102
|
+
)
|
103
|
+
rule ||= rule_from_when(opts.delete(:when)) if opts[:when]
|
104
|
+
rule ||= rule_from_first_of(opts.delete(:first_of)) if opts[:first_of]
|
105
|
+
new_object = {
|
106
|
+
type: :object,
|
107
|
+
properties: {},
|
108
|
+
additionalProperties: false,
|
109
|
+
|
110
|
+
**(!default.nil? ? { default: default } : {}),
|
111
|
+
**(description ? { description: description } : {}),
|
112
|
+
**(rule ? { rule: rule } : {}),
|
113
|
+
**opts
|
114
|
+
}
|
115
|
+
yields(new_object, &blk)
|
116
|
+
required(name) if required && default.nil? && rule.nil?
|
117
|
+
pattern_property ? set_pattern_property(name, new_object) : set_property(name, new_object)
|
118
|
+
end
|
119
|
+
|
120
|
+
def primitive(
|
121
|
+
name = nil, type, required: false, pattern_property: false,
|
122
|
+
default: nil, description: nil, enum: nil,
|
123
|
+
rule: nil, **additional_options, &blk
|
124
|
+
)
|
125
|
+
new_primitive = {
|
126
|
+
type: type,
|
127
|
+
**(enum ? { enum: enum } : {}),
|
128
|
+
**(!default.nil? ? { default: default } : {}),
|
129
|
+
**(description ? { description: description } : {}),
|
130
|
+
**(rule ? { rule: rule } : {}),
|
131
|
+
**additional_options
|
132
|
+
}
|
133
|
+
yields(new_primitive, &blk)
|
134
|
+
required(name) if required && default.nil? && rule.nil?
|
135
|
+
pattern_property ? set_pattern_property(name, new_primitive) : set_property(name, new_primitive)
|
136
|
+
end
|
137
|
+
|
138
|
+
def decimal(name = nil, required: false, pattern_property: false, default: nil, description: nil, rule: nil,
|
139
|
+
**opts, &blk)
|
140
|
+
# Define the decimal schema supporting multiple formats
|
141
|
+
new_decimal = {
|
142
|
+
anyOf: [
|
143
|
+
{
|
144
|
+
type: :string,
|
145
|
+
# Matches valid decimals with optional exponentials
|
146
|
+
pattern: '^-?(\\d+\\.\\d+|\\d+|\\d+e[+-]?\\d+|\\d+\\.\\d+e[+-]?\\d+)$'
|
147
|
+
|
148
|
+
},
|
149
|
+
# Allows both float and int
|
150
|
+
{
|
151
|
+
type: :number #
|
152
|
+
},
|
153
|
+
{
|
154
|
+
type: :integer
|
155
|
+
}
|
156
|
+
],
|
157
|
+
type: :decimal,
|
158
|
+
**(!default.nil? ? { default: default } : {}),
|
159
|
+
**(description ? { description: description } : {}),
|
160
|
+
**(rule ? { rule: rule } : {}),
|
161
|
+
**opts
|
162
|
+
}
|
163
|
+
yields(new_decimal, &blk)
|
164
|
+
required(name) if required && default.nil? && rule.nil?
|
165
|
+
pattern_property ? set_pattern_property(name, new_decimal) : set_property(name, new_decimal)
|
166
|
+
end
|
167
|
+
|
168
|
+
def timestamp(name = nil, required: false, pattern_property: false, default: nil, description: nil, rule: nil,
|
169
|
+
**opts, &blk)
|
170
|
+
new_timestamp = {
|
171
|
+
anyOf: [
|
172
|
+
{
|
173
|
+
type: :string,
|
174
|
+
# Matches ISO 8601 timestamp without timezone
|
175
|
+
pattern: '^\d{4}-\d{2}-\d{2}(T\d{2}(:\d{2}(:\d{2}(\.\d{1,3})?)?)?(Z|[+-]\d{2}(:\d{2})?)?)?$'
|
176
|
+
},
|
177
|
+
{
|
178
|
+
type: :number # Allows numeric epoch timestamps
|
179
|
+
},
|
180
|
+
{
|
181
|
+
type: :integer # Allows both float and int
|
182
|
+
}
|
183
|
+
],
|
184
|
+
type: :timestamp, # Custom extended type
|
185
|
+
**(!default.nil? ? { default: default } : {}),
|
186
|
+
**(description ? { description: description } : {}),
|
187
|
+
**(rule ? { rule: rule } : {}),
|
188
|
+
**opts
|
189
|
+
}
|
190
|
+
yields(new_timestamp, &blk)
|
191
|
+
required(name) if required && default.nil? && rule.nil?
|
192
|
+
pattern_property ? set_pattern_property(name, new_timestamp) : set_property(name, new_timestamp)
|
193
|
+
end
|
194
|
+
|
195
|
+
def time(name = nil, required: false, pattern_property: false, default: nil, description: nil, rule: nil,
|
196
|
+
**opts, &blk)
|
197
|
+
new_time = {
|
198
|
+
type: :time, # Custom extended type
|
199
|
+
# Matches HH:mm[:ss[.SSS]]
|
200
|
+
pattern: '^\\d{2}:\\d{2}(:\\d{2}(\\.\\d{1,3})?)?$',
|
201
|
+
**(!default.nil? ? { default: default } : {}),
|
202
|
+
**(description ? { description: description } : {}),
|
203
|
+
**(rule ? { rule: rule } : {}),
|
204
|
+
**opts
|
205
|
+
}
|
206
|
+
yields(new_time, &blk)
|
207
|
+
required(name) if required && default.nil? && rule.nil?
|
208
|
+
pattern_property ? set_pattern_property(name, new_time) : set_property(name, new_time)
|
209
|
+
end
|
210
|
+
|
211
|
+
def date(name = nil, required: false, pattern_property: false, default: nil, description: nil, rule: nil,
|
212
|
+
**opts, &blk)
|
213
|
+
new_date = {
|
214
|
+
anyOf: [
|
215
|
+
{
|
216
|
+
type: :string,
|
217
|
+
# Matches ISO 8601 date format
|
218
|
+
pattern: '^\\d{4}-\\d{2}-\\d{2}$'
|
219
|
+
}
|
220
|
+
],
|
221
|
+
type: :date, # Custom extended type
|
222
|
+
**(!default.nil? ? { default: default } : {}),
|
223
|
+
**(description ? { description: description } : {}),
|
224
|
+
**(rule ? { rule: rule } : {}),
|
225
|
+
**opts
|
226
|
+
}
|
227
|
+
yields(new_date, &blk)
|
228
|
+
required(name) if required && default.nil? && rule.nil?
|
229
|
+
pattern_property ? set_pattern_property(name, new_date) : set_property(name, new_date)
|
230
|
+
end
|
231
|
+
|
232
|
+
%i[boolean string const integer number null].each do |type|
|
233
|
+
define_method(type) do |
|
234
|
+
name = nil, required: false, pattern_property: false,
|
235
|
+
default: nil, description: nil, enum: nil, rule: nil, **additional_options, &blk|
|
236
|
+
primitive(
|
237
|
+
name, type, required: required, pattern_property: pattern_property, enum: enum,
|
238
|
+
default: default, rule: rule, description: description,
|
239
|
+
**additional_options, &blk
|
240
|
+
)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def dependencies(dependencies)
|
245
|
+
schema[:dependencies] = HashUtils.deep_merge(schema[:dependencies] || {}, dependencies)
|
246
|
+
end
|
247
|
+
|
248
|
+
def one_of(one_of)
|
249
|
+
schema[:oneOf] = (schema[:one_of] || []).concat(one_of).uniq
|
250
|
+
end
|
251
|
+
|
252
|
+
def any_of(any_of)
|
253
|
+
schema[:anyOf] = (schema[:any_of] || []).concat(any_of).uniq
|
254
|
+
end
|
255
|
+
|
256
|
+
def array(name = nil, required: false, pattern_property: false, default: nil, description: nil, rule: nil,
|
257
|
+
type: :object, **opts, &blk)
|
258
|
+
new_array = {
|
259
|
+
type: :array,
|
260
|
+
**(!default.nil? ? { default: default } : {}),
|
261
|
+
**(description ? { description: description } : {}),
|
262
|
+
**(rule ? { rule: rule } : {}),
|
263
|
+
**opts,
|
264
|
+
items: {
|
265
|
+
type: type,
|
266
|
+
properties: {},
|
267
|
+
additionalProperties: false
|
268
|
+
}
|
269
|
+
}
|
270
|
+
yields(new_array, &blk)
|
271
|
+
required(name) if required && default.nil? && rule.nil?
|
272
|
+
pattern_property ? set_pattern_property(name, new_array) : set_property(name, new_array)
|
273
|
+
end
|
274
|
+
|
275
|
+
def items(&blk)
|
276
|
+
yields(schema[:items], &blk)
|
277
|
+
end
|
278
|
+
|
279
|
+
def yields(other)
|
280
|
+
return unless block_given?
|
281
|
+
|
282
|
+
prev_schema = schema
|
283
|
+
self.schema = other
|
284
|
+
yield
|
285
|
+
self.schema = prev_schema
|
286
|
+
end
|
287
|
+
|
288
|
+
def rule_from_when(when_clause)
|
289
|
+
inputs = when_clause.keys
|
290
|
+
conditions = when_clause
|
291
|
+
rule = "{#{when_clause.keys.map { |k| "#{k}: #{k}}" }.join(', ')}"
|
292
|
+
{
|
293
|
+
inputs: inputs,
|
294
|
+
conditions: conditions,
|
295
|
+
rule: rule
|
296
|
+
}
|
297
|
+
end
|
298
|
+
|
299
|
+
def rule_from_first_of(prop_list)
|
300
|
+
{
|
301
|
+
inputs: prop_list,
|
302
|
+
rule: "itself.get_first_of(:#{prop_list.join(', :')})"
|
303
|
+
}
|
304
|
+
end
|
305
|
+
|
306
|
+
def depends_on(*dependencies)
|
307
|
+
@resolved_dependencies ||= Hash.new do |h, k|
|
308
|
+
send(k) # Load dependency once
|
309
|
+
h[k] = true
|
310
|
+
end
|
311
|
+
dependencies.each(&@resolved_dependencies.method(:[]))
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Subclass LazyGraph::Builder to create new builder classes
|
4
|
+
# which can be used to easily build a rule-set to be used as a LazyGraph.
|
5
|
+
#
|
6
|
+
require_relative 'builder/dsl'
|
7
|
+
|
8
|
+
module LazyGraph
|
9
|
+
class Builder
|
10
|
+
include DSL
|
11
|
+
# This class is responsible for piece-wise building of rules,
|
12
|
+
# as a combined schema definition.
|
13
|
+
attr_accessor :schema
|
14
|
+
|
15
|
+
def initialize(schema: { type: 'object', properties: {} }) = @schema = schema
|
16
|
+
def build!(debug: false, validate: false) = @schema.to_lazy_graph(debug: debug, validate: validate, helpers: self.class.helper_modules)
|
17
|
+
def context(value, debug: false, validate: false) = build!(debug: debug, validate: validate).context(value)
|
18
|
+
def eval!(context, *value, debug: false, validate: false) = context(context, validate: validate, debug: debug).query(*value)
|
19
|
+
|
20
|
+
def self.rules_module(name, schema = { type: 'object', properties: {} }, &blk)
|
21
|
+
rules_modules[:properties][name.to_sym] = { type: :object, properties: schema }
|
22
|
+
module_body_func_name = :"_#{name}"
|
23
|
+
define_method(module_body_func_name, &blk)
|
24
|
+
define_method(name) do |**args, &inner_blk|
|
25
|
+
send(module_body_func_name, **args, &inner_blk)
|
26
|
+
self
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class << self
|
31
|
+
attr_reader :helper_modules
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.register_helper_module(mod)
|
35
|
+
(@helper_modules ||= []) << mod
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.clear_rules_modules!
|
39
|
+
@rules_modules = nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.clear_helper_modules!
|
43
|
+
@helper_modules = nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.rules_modules
|
47
|
+
@rules_modules ||= {
|
48
|
+
type: :object,
|
49
|
+
properties: {},
|
50
|
+
additionalProperties: false
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.usage
|
55
|
+
{
|
56
|
+
modules_options: rules_modules[:properties].map do |k, v|
|
57
|
+
{
|
58
|
+
name: k.to_s,
|
59
|
+
properties: v[:properties],
|
60
|
+
required: v[:required]
|
61
|
+
}
|
62
|
+
end,
|
63
|
+
context_sample_schema: rules_modules[:properties].keys.reduce(new) do |acc, (k, _v)|
|
64
|
+
acc.send(k, **{})
|
65
|
+
end.schema
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.eval!(modules:, context:, query:, debug: false, validate: false)
|
70
|
+
context_result = (@eval_cache ||= {})[[modules, context, query, debug, validate].hash] ||= begin
|
71
|
+
invalid_modules = modules.reject { |k, _v| rules_modules[:properties].key?(k.to_sym) }
|
72
|
+
return format_error_response('Invalid Modules', invalid_modules.keys.join(',')) unless invalid_modules.empty?
|
73
|
+
|
74
|
+
error = validate_modules(modules)
|
75
|
+
|
76
|
+
return format_error_response('Invalid Module Option', error) unless error.empty?
|
77
|
+
|
78
|
+
builder = build_modules(modules)
|
79
|
+
return builder if builder.is_a?(Hash)
|
80
|
+
|
81
|
+
evaluate_context(builder, context, debug: debug, validate: validate)
|
82
|
+
end
|
83
|
+
|
84
|
+
@eval_cache.delete(@eval_cache.keys.first) if @eval_cache.size > 1000
|
85
|
+
|
86
|
+
return context_result if context_result.is_a?(Hash) && context_result[:type] == :error
|
87
|
+
|
88
|
+
{
|
89
|
+
type: :success,
|
90
|
+
result: context_result.query(*(query || ''))
|
91
|
+
}
|
92
|
+
|
93
|
+
rescue SystemStackError => e
|
94
|
+
LazyGraph.logger.error(e.message)
|
95
|
+
LazyGraph.logger.error(e.backtrace)
|
96
|
+
{
|
97
|
+
type: :error,
|
98
|
+
message: 'Recursive Query Detected',
|
99
|
+
detail: "Problem query path: #{query}"
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
private_class_method def self.method_missing(method_name, *args, &block) = new.send(method_name, *args, &block)
|
104
|
+
private_class_method def self.respond_to_missing?(_, _ = false) = true
|
105
|
+
|
106
|
+
private_class_method def self.validate_modules(input_options)
|
107
|
+
JSON::Validator.validate!(rules_modules, input_options)
|
108
|
+
''
|
109
|
+
rescue StandardError => e
|
110
|
+
e.message
|
111
|
+
end
|
112
|
+
|
113
|
+
private_class_method def self.format_error_response(message, detail)
|
114
|
+
{
|
115
|
+
type: :error,
|
116
|
+
message: message,
|
117
|
+
**(detail.to_s =~ /Schema: / ? { location: detail[/^Schema: #(.*)$/, 1] } : {}),
|
118
|
+
detail: detail.to_s
|
119
|
+
}
|
120
|
+
end
|
121
|
+
|
122
|
+
private_class_method def self.build_modules(input_options)
|
123
|
+
input_options.reduce(new.additional_properties(false)) do |acc, (k, v)|
|
124
|
+
acc.send(k, **v.to_h.transform_keys(&:to_sym))
|
125
|
+
end
|
126
|
+
rescue ArgumentError => e
|
127
|
+
format_error_response('Invalid Module Argument', e.message)
|
128
|
+
LazyGraph.logger.error(e.backtrace)
|
129
|
+
end
|
130
|
+
|
131
|
+
private_class_method def self.evaluate_context(builder, context, debug: false, validate: false)
|
132
|
+
builder.context(context, debug: debug, validate: validate)
|
133
|
+
rescue ArgumentError => e
|
134
|
+
format_error_response('Invalid Context Input', e.message)
|
135
|
+
LazyGraph.logger.error(e.backtrace)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module LazyGraph
|
2
|
+
class BuilderGroup
|
3
|
+
# A builder group is simply a named colleciton of builders (each a subclass of LazyGraph::Builder)
|
4
|
+
# That can be reloaded in bulk, and exposed via a simple HTTP server interface.
|
5
|
+
def self.bootstrap!(reload_paths: [], listen: ENV['RACK_ENV'] == 'development')
|
6
|
+
reload_paths = Array(reload_paths)
|
7
|
+
Module.new do
|
8
|
+
define_singleton_method(:included) do |base|
|
9
|
+
def base.each_builder(const = self, &blk)
|
10
|
+
return to_enum(__method__, const) unless blk
|
11
|
+
|
12
|
+
if const.is_a?(Class) && const < LazyGraph::Builder
|
13
|
+
blk[const]
|
14
|
+
elsif const.is_a?(Module)
|
15
|
+
const.constants.each do |c|
|
16
|
+
each_builder(const.const_get(c), &blk)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def base.server
|
22
|
+
LazyGraph::Server.new(
|
23
|
+
routes: each_builder.map do |builder|
|
24
|
+
[
|
25
|
+
builder.to_s.downcase.gsub('::', '/').gsub(/^#{name.downcase}/, ''),
|
26
|
+
builder
|
27
|
+
]
|
28
|
+
end.to_h
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
base.define_singleton_method(:reload_lazy_graphs!) do
|
33
|
+
each_builder do |builder|
|
34
|
+
builder.clear_rules_modules!
|
35
|
+
builder.clear_helper_modules!
|
36
|
+
end
|
37
|
+
|
38
|
+
reload_paths.flat_map { |p| Dir[p] }.each do |file|
|
39
|
+
load file
|
40
|
+
rescue StandardError => e
|
41
|
+
LazyGraph.logger.error("Failed to load #{file}: #{e.message}")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
base.reload_lazy_graphs!
|
46
|
+
|
47
|
+
return unless listen
|
48
|
+
|
49
|
+
require 'listen'
|
50
|
+
Listen.to(*reload_paths.map { |p| p.gsub(%r{(?:/\*\*)*/\*\.rb}, '') }) do
|
51
|
+
base.reload_lazy_graphs!
|
52
|
+
end.start
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LazyGraph
|
4
|
+
# Context class is responsible for managing ruleset and input data,
|
5
|
+
# allowing querying and dynamic method calls to access input fields.
|
6
|
+
class Context
|
7
|
+
attr_accessor :ruleset, :input
|
8
|
+
|
9
|
+
def initialize(graph, input)
|
10
|
+
HashUtils.deep_symbolize!(input)
|
11
|
+
graph.validate!(input) if [true, 'input'].include?(graph.validate)
|
12
|
+
@graph = graph
|
13
|
+
@input = input
|
14
|
+
end
|
15
|
+
|
16
|
+
def query(paths)
|
17
|
+
paths.is_a?(Array) ? paths.map { |path| resolve(input, path) } : resolve(input, paths)
|
18
|
+
end
|
19
|
+
|
20
|
+
def resolve(input, path)
|
21
|
+
@input = @graph.root_node.fetch_item({ input: input }, :input, nil)
|
22
|
+
query = PathParser.parse(path, true)
|
23
|
+
stack = StackPointer.new(nil, @input, 0, :'$', nil)
|
24
|
+
stack.root = stack
|
25
|
+
|
26
|
+
result = @graph.root_node.resolve(query, stack)
|
27
|
+
@graph.root_node.clear_visits!
|
28
|
+
if @graph.debug?
|
29
|
+
debug_trace = stack.frame[:DEBUG]
|
30
|
+
stack.frame[:DEBUG] = nil
|
31
|
+
end
|
32
|
+
{
|
33
|
+
output: HashUtils.strip_invalid(result),
|
34
|
+
debug_trace: HashUtils.strip_invalid(debug_trace)
|
35
|
+
}
|
36
|
+
rescue LazyGraph::AbortError => e
|
37
|
+
{
|
38
|
+
output: { err: e.message, status: :abort }
|
39
|
+
}
|
40
|
+
rescue StandardError => e
|
41
|
+
LazyGraph.logger.error(e.message)
|
42
|
+
LazyGraph.logger.error(e.backtrace)
|
43
|
+
{
|
44
|
+
output: { err: e.message, backtrace: e.backtrace }
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def pretty_print(q)
|
49
|
+
# Start the custom pretty print
|
50
|
+
q.group(1, '<LazyGraph::Context ', '>') do
|
51
|
+
q.group do
|
52
|
+
q.text 'graph='
|
53
|
+
q.pp(@graph)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
alias [] query
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json-schema'
|
4
|
+
|
5
|
+
module LazyGraph
|
6
|
+
# Represents a lazy graph structure based on JSON schema
|
7
|
+
VALIDATION_CACHE = {}
|
8
|
+
METASCHEMA = JSON.load_file(File.join(__dir__, 'lazy-graph.json'))
|
9
|
+
|
10
|
+
class Graph
|
11
|
+
attr_reader :json_schema, :root_node, :validate
|
12
|
+
|
13
|
+
def context(input) = Context.new(self, input)
|
14
|
+
def debug? = @debug
|
15
|
+
|
16
|
+
def initialize(json_schema, debug: false, validate: true, helpers: nil)
|
17
|
+
@json_schema = HashUtils.deep_dup!(json_schema).merge(type: :object)
|
18
|
+
|
19
|
+
@debug = debug
|
20
|
+
@validate = validate
|
21
|
+
@helpers = helpers
|
22
|
+
|
23
|
+
signature = HashUtils.deep_symbolize!(@json_schema)
|
24
|
+
if [true, 'schema'].include?(validate)
|
25
|
+
VALIDATION_CACHE[signature] ||= validate!(@json_schema, METASCHEMA)
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
if @json_schema[:type].to_sym != :object || @json_schema[:properties].nil?
|
30
|
+
raise ArgumentError, 'Root schema must be a non-empty object'
|
31
|
+
end
|
32
|
+
|
33
|
+
@root_node = build_node(@json_schema)
|
34
|
+
end
|
35
|
+
|
36
|
+
def build_node(schema, path = :'$', name = :root, parent = nil)
|
37
|
+
schema[:type] = schema[:type].to_sym
|
38
|
+
case schema[:type]
|
39
|
+
when :object then ObjectNode
|
40
|
+
when :array then ArrayNode
|
41
|
+
else Node
|
42
|
+
end.new(name, path, schema, parent, debug: @debug, helpers: @helpers).tap do |node|
|
43
|
+
node.children = build_children(node, schema, path)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def build_children(node, schema, path)
|
48
|
+
case node.type
|
49
|
+
when :object then build_object_children(schema, path, node)
|
50
|
+
when :array then build_node(schema.fetch(:items, {}), :"#{path}[]", :items, node)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def build_object_children(schema, path, parent)
|
55
|
+
{
|
56
|
+
properties: schema.fetch(:properties, {}).map do |key, value|
|
57
|
+
[key, build_node(value, "#{path}.#{key}", key, parent)]
|
58
|
+
end.to_h,
|
59
|
+
pattern_properties: schema.fetch(:patternProperties, {}).map do |key, value|
|
60
|
+
[Regexp.new(key.to_s), build_node(value, :"#{path}.#{key}", :'<property>', parent)]
|
61
|
+
end.to_h
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
def validate!(input, schema = @json_schema)
|
66
|
+
JSON::Validator.validate!(schema, input)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
Hash.define_method(:to_lazy_graph, ->(**opts) { LazyGraph::Graph.new(self, **opts) })
|
71
|
+
Hash.define_method(:to_graph_ctx, ->(input, **opts) { to_lazy_graph(**opts).context(input) })
|
72
|
+
Hash.define_method(:eval_graph, ->(input, *query, **opts) { to_graph_ctx(input, **opts)[*query] })
|
73
|
+
end
|