lazy_graph 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|