lazy_graph 0.1.2 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29043baea3639e83c226d8e157af818466d50d4e3c125853dac64ae411aa0c0a
4
- data.tar.gz: f137caa57317cd9046bc16384bf657ca7c43d10b12701f30be6bc7a31580c6db
3
+ metadata.gz: 2cb285016525832f149dd41b91340c71f6f6c68d7aebcc0de6376dbf81c71cd8
4
+ data.tar.gz: fc0bee81e2e2b45dfad51c822c421d0496bda877d8e77598d29875b66f261286
5
5
  SHA512:
6
- metadata.gz: b1c5107a005882da016caa1fd54afa5131f296637fd6a7b18810d3b2c1f18893ae11938e63fc42ceec05def4ceba76afbe016b152d1c107225af148b06f6db52
7
- data.tar.gz: b5a2126ac97b38e1e0d4ce23a1d46859a89979b0b09a2d3ad3e3676b763d6277745310ee08a0c49fbbc85bcd930d246362bd79793899726c2f763b0aa1f6fe44
6
+ metadata.gz: 0d156ef5344d260232771fa272d5a318342a46293a5991abb9c47948fd0b79c096f2f1f219cf9383a677435ec0e9e9f1bc95011b254d9828e2b19b86f991a5fe
7
+ data.tar.gz: e9c44f0ff222e41ad1992703b3f8adf1a4424f073bc80b89bb1d0f85f515fbe8413d023f9072141fbeaff5b242bf9e91fd415c43b8445cc10d2754a17af73b0a
@@ -13,9 +13,9 @@ class PerformanceBuilder < LazyGraph::Builder
13
13
  string :pay_schedule_id, invisible: true
14
14
  string :position_id, invisible: true
15
15
 
16
- object :position, rule: :"${$.positions[position_id]}"
17
- object :pay_schedule, rule: :'${pay_schedules[pay_schedule_id]}'
18
- number :pay_rate, rule: :"${position.pay_rate}"
16
+ object :position, rule: :"$.positions[position_id]"
17
+ object :pay_schedule, rule: :'pay_schedules[pay_schedule_id]'
18
+ number :pay_rate, rule: :"position.pay_rate"
19
19
  string :employee_id, rule: :id
20
20
  end
21
21
  end
@@ -67,7 +67,7 @@ def profile_n(n, debug: false, validate: false)
67
67
  graph = PerformanceBuilder.performance.build!(debug: debug, validate: validate)
68
68
  Vernier.profile(out: './examples/time_profile.json') do
69
69
  start = Time.now
70
- graph.context(employees).query('')
70
+ graph.context(employees).get('')
71
71
  ends = Time.now
72
72
  puts "Time elapsed: #{ends - start}"
73
73
  end
@@ -77,7 +77,7 @@ def memory_profile_n(n, debug: false, validate: false)
77
77
  employees = gen_employees(n)
78
78
  graph = PerformanceBuilder.performance.build!(debug: debug, validate: validate)
79
79
  report = MemoryProfiler.report do
80
- graph.context(employees).query('')
80
+ graph.context(employees).get('')
81
81
  end
82
82
  report.pretty_print
83
83
  end
@@ -87,7 +87,7 @@ def benchmark_ips_n(n, debug: false, validate: false)
87
87
  employees = gen_employees(n)
88
88
  Benchmark.ips do |x|
89
89
  x.report('performance') do
90
- graph.context(employees).query('')
90
+ graph.context(employees).get('')
91
91
  end
92
92
  x.compare!
93
93
  end
@@ -97,7 +97,7 @@ def console_n(n, debug: false, validate: false)
97
97
  require 'debug'
98
98
  graph = PerformanceBuilder.performance.build!(debug: debug, validate: validate)
99
99
  employees = gen_employees(n)
100
- result = graph.context(employees).query('')
100
+ result = graph.context(employees).get('')
101
101
  binding.b
102
102
  end
103
103
 
@@ -76,7 +76,6 @@ module LazyGraph
76
76
  }
77
77
  @prev_match_cases = @match_cases
78
78
  @match_cases = []
79
-
80
79
  yields(new_object, &blk)
81
80
 
82
81
  object_names = @match_cases.map do |match_case|
@@ -10,11 +10,11 @@ module LazyGraph
10
10
  # Cache up to a fixed number of graphs, context and queries
11
11
  BUILD_CACHE_CONFIG = {
12
12
  # Store up to 1000 graphs
13
- graph: {size: 1000, cache: {}},
13
+ graph: {size: ENV.fetch('LAZY_GRAPH_GRAPH_CACHE_MAX_ENTRIES', 1000).to_i, cache: {}},
14
14
  # Store up to 5000 configs
15
- context: {size: 5000, cache: {}},
15
+ context: {size: ENV.fetch('LAZY_GRAPH_CONTEXT_CACHE_MAX_ENTRIES', 5000).to_i, cache: {}},
16
16
  # Store up to 5000 queries
17
- query: {size: 5000, cache: {}}
17
+ query: {size: ENV.fetch('LAZY_GRAPH_QUERY_CACHE_MAX_ENTRIES', 5000).to_i, cache: {}}
18
18
  }.compare_by_identity.freeze
19
19
 
20
20
  include DSL
@@ -23,10 +23,16 @@ module LazyGraph
23
23
  attr_accessor :schema
24
24
 
25
25
  def initialize(schema: { type: 'object', properties: {} }) = @schema = schema
26
- def build!(debug: false, validate: false) = @schema.to_lazy_graph(debug: debug, validate: validate, helpers: self.class.helper_modules)
27
26
  def context(value, debug: false, validate: false) = build!(debug: debug, validate: validate).context(value)
28
27
  def eval!(context, *value, debug: false, validate: false) = context(context, validate: validate, debug: debug).query(*value)
29
28
 
29
+ def build!(debug: false, validate: false) = @schema.to_lazy_graph(
30
+ debug: debug,
31
+ validate: validate,
32
+ helpers: self.class.helper_modules,
33
+ namespace: self.class
34
+ )
35
+
30
36
  def self.rules_module(name, schema = { type: 'object', properties: {} }, &blk)
31
37
  rules_modules[:properties][name.to_sym] = { type: :object, properties: schema }
32
38
  module_body_func_name = :"_#{name}"
@@ -106,14 +112,15 @@ module LazyGraph
106
112
  evaluate_context(builder, context, debug: debug, validate: validate)
107
113
  end
108
114
 
109
- return context_result if context_result.is_a?(Hash) && context_result[:type] == :error
115
+ return context_result if context_result.is_a?(Hash) && context_result[:type].equal?(:error)
110
116
 
111
- query_result = cache_as(:query, [context_result, query]){ context_result.query(*(query || '')) }
117
+ cache_as(:query, [context_result, query]) do
118
+ HashUtils.strip_missing({
119
+ type: :success,
120
+ result: context_result.resolve(*(query || ''))
121
+ })
122
+ end
112
123
 
113
- {
114
- type: :success,
115
- result: query_result
116
- }
117
124
  rescue SystemStackError => e
118
125
  LazyGraph.logger.error(e.message)
119
126
  LazyGraph.logger.error(e.backtrace.join("\n"))
@@ -157,14 +164,14 @@ module LazyGraph
157
164
  end
158
165
  rescue ArgumentError => e
159
166
  format_error_response('Invalid Module Argument', e.message)
160
- LazyGraph.logger.error(e.backtrace)
167
+ LazyGraph.logger.error(e.backtrace.join("\n"))
161
168
  end
162
169
 
163
170
  private_class_method def self.evaluate_context(builder, context, debug: false, validate: false)
164
171
  builder.context(context, debug: debug, validate: validate)
165
172
  rescue ArgumentError => e
166
173
  format_error_response('Invalid Context Input', e.message)
167
- LazyGraph.logger.error(e.backtrace)
174
+ LazyGraph.logger.error(e.backtrace.join("\n"))
168
175
  end
169
176
  end
170
177
  end
@@ -29,10 +29,12 @@ module LazyGraph
29
29
  )
30
30
  end
31
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!
32
+ base.define_singleton_method(:reload_lazy_graphs!) do |clear_previous: false|
33
+ if clear_previous
34
+ each_builder do |builder|
35
+ builder.clear_rules_modules!
36
+ builder.clear_helper_modules!
37
+ end
36
38
  end
37
39
 
38
40
  reload_paths.flat_map { |p| Dir[p] }.each do |file|
@@ -42,13 +44,13 @@ module LazyGraph
42
44
  end
43
45
  end
44
46
 
45
- base.reload_lazy_graphs!
47
+ base.reload_lazy_graphs! if reload_paths.any?
46
48
 
47
49
  return unless listen
48
50
 
49
51
  require 'listen'
50
52
  Listen.to(*reload_paths.map { |p| p.gsub(%r{(?:/\*\*)*/\*\.rb}, '') }) do
51
- base.reload_lazy_graphs!
53
+ base.reload_lazy_graphs!(clear_previous: true)
52
54
  end.start
53
55
  end
54
56
  end
@@ -11,17 +11,34 @@ module LazyGraph
11
11
  graph.validate!(input) if [true, 'input'].include?(graph.validate)
12
12
  @graph = graph
13
13
  @input = input
14
+ @graph.root_node.properties.each_key do |key|
15
+ define_singleton_method(key) { get(key) }
16
+ end
14
17
  end
15
18
 
16
- def query(paths)
17
- paths.is_a?(Array) ? paths.map { |path| resolve(path) } : resolve(paths)
19
+ def get_json(path)
20
+ HashUtils.strip_missing(get(path))
21
+ end
22
+
23
+ def get(path)
24
+ result = resolve(path)
25
+ raise AbortError, result[:err], cause: result[:error] if result[:err]
26
+
27
+ result[:output]
28
+ end
29
+
30
+ def debug(path)
31
+ result = resolve(path)
32
+ raise AbortError, result[:err], cause: result[:error] if result[:err]
33
+
34
+ result[:debug_trace]
18
35
  end
19
36
 
20
37
  def resolve(path)
21
38
  @input = @graph.root_node.fetch_item({ input: @input }, :input, nil)
22
39
 
23
- query = PathParser.parse(path, true)
24
- stack = StackPointer.new(nil, @input, 0, :'$', nil)
40
+ query = PathParser.parse(path, strip_root: true)
41
+ stack = StackPointer.new(nil, @input, 0, 0, :'$', nil)
25
42
  stack.root = stack
26
43
 
27
44
  result = @graph.root_node.resolve(query, stack)
@@ -32,18 +49,18 @@ module LazyGraph
32
49
  stack.frame[:DEBUG] = nil
33
50
  end
34
51
  {
35
- output: HashUtils.strip_invalid(result),
36
- debug_trace: HashUtils.strip_invalid(debug_trace)
52
+ output: result,
53
+ debug_trace: debug_trace
37
54
  }
38
55
  rescue LazyGraph::AbortError => e
39
56
  {
40
- output: { err: e.message, status: :abort }
57
+ output: nil, err: e.message, status: :abort, error: e
41
58
  }
42
59
  rescue StandardError => e
43
60
  LazyGraph.logger.error(e.message)
44
- LazyGraph.logger.error(e.backtrace)
61
+ LazyGraph.logger.error(e.backtrace.join("\n"))
45
62
  {
46
- output: { err: e.message, backtrace: e.backtrace }
63
+ output: nil, err: e.message, backtrace: e.backtrace, error: e
47
64
  }
48
65
  end
49
66
 
@@ -54,9 +71,13 @@ module LazyGraph
54
71
  q.text 'graph='
55
72
  q.pp(@graph)
56
73
  end
74
+ q.group do
75
+ q.text 'input='
76
+ q.pp(@input)
77
+ end
57
78
  end
58
79
  end
59
80
 
60
- alias [] query
81
+ alias [] get
61
82
  end
62
83
  end
@@ -13,7 +13,7 @@ module LazyGraph
13
13
  def context(input) = Context.new(self, input)
14
14
  def debug? = @debug
15
15
 
16
- def initialize(input_schema, debug: false, validate: true, helpers: nil)
16
+ def initialize(input_schema, debug: false, validate: true, helpers: nil, namespace: nil)
17
17
  @json_schema = HashUtils.deep_dup(input_schema, symbolize: true, signature: signature = [0]).merge(type: :object)
18
18
  @debug = debug
19
19
  @validate = validate
@@ -25,18 +25,20 @@ module LazyGraph
25
25
  raise ArgumentError, 'Root schema must be a non-empty object'
26
26
  end
27
27
 
28
- @root_node = build_node(@json_schema)
28
+ @root_node = build_node(@json_schema, namespace: namespace)
29
+ @root_node.build_derived_inputs!
29
30
  end
30
31
 
31
- def build_node(schema, path = :'$', name = :root, parent = nil)
32
+ def build_node(schema, path = :'$', name = :root, parent = nil, namespace: nil)
32
33
  schema[:type] = schema[:type].to_sym
33
- node = case schema[:type]
34
- when :object then ObjectNode
35
- when :array then ArrayNode
36
- else Node
37
- end.new(name, path, schema, parent, debug: @debug, helpers: @helpers)
34
+ node = \
35
+ case schema[:type]
36
+ when :object then ObjectNode
37
+ when :array then ArrayNode
38
+ else Node
39
+ end.new(name, path, schema, parent, debug: @debug, helpers: @helpers, namespace: namespace)
38
40
 
39
- if node.type == :object
41
+ if node.type.equal?(:object)
40
42
  node.children = \
41
43
  {
42
44
  properties: schema.fetch(:properties, {}).map do |key, value|
@@ -46,7 +48,7 @@ module LazyGraph
46
48
  [Regexp.new(key.to_s), build_node(value, :"#{path}.#{key}", :'<property>', node)]
47
49
  end
48
50
  }
49
- elsif node.type == :array
51
+ elsif node.type.equal?(:array)
50
52
  node.children = build_node(schema.fetch(:items, {}), :"#{path}[]", :items, node)
51
53
  end
52
54
  node
@@ -54,6 +56,18 @@ module LazyGraph
54
56
 
55
57
  def validate!(input, schema = @json_schema)
56
58
  JSON::Validator.validate!(schema, input)
59
+ rescue JSON::Schema::ValidationError => e
60
+ raise AbortError, "Input validation failed: #{e.message}", cause: e
61
+ end
62
+
63
+ def pretty_print(q)
64
+ # Start the custom pretty print
65
+ q.group(1, '<LazyGraph::Graph ', '>') do
66
+ q.group do
67
+ q.text 'props='
68
+ q.text root_node.children[:properties].keys
69
+ end
70
+ end
57
71
  end
58
72
  end
59
73
 
@@ -20,6 +20,8 @@ module LazyGraph
20
20
  signature[0] ^= key.object_id if signature
21
21
  result[key] = deep_dup(value, symbolize: symbolize, signature: signature)
22
22
  end
23
+ when Struct
24
+ deep_dup(obj.to_h, symbolize: symbolize, signature: signature)
23
25
  when Array
24
26
  obj.map { |value| deep_dup(value, symbolize: symbolize, signature: signature) }
25
27
  when String, Numeric, TrueClass, FalseClass, NilClass
@@ -47,7 +49,7 @@ module LazyGraph
47
49
  end
48
50
  end
49
51
 
50
- def strip_invalid(obj, parent_list = {}.compare_by_identity)
52
+ def strip_missing(obj, parent_list = {}.compare_by_identity)
51
53
  return { '^ref': :circular } if (circular_dependency = parent_list[obj])
52
54
 
53
55
  parent_list[obj] = true
@@ -56,17 +58,17 @@ module LazyGraph
56
58
  obj.each_with_object({}) do |(key, value), obj|
57
59
  next if value.is_a?(MissingValue)
58
60
 
59
- obj[key] = strip_invalid(value, parent_list)
61
+ obj[key] = strip_missing(value, parent_list)
60
62
  end
61
63
  when Struct
62
64
  obj.members.each_with_object({}) do |key, res|
63
65
  next if obj[key].is_a?(MissingValue)
64
66
  next if obj.invisible.include?(key)
65
67
 
66
- res[key] = strip_invalid(obj[key], parent_list)
68
+ res[key] = strip_missing(obj[key], parent_list)
67
69
  end
68
70
  when Array
69
- obj.map { |value| strip_invalid(value, parent_list) }
71
+ obj.map { |value| strip_missing(value, parent_list) }
70
72
  when MissingValue
71
73
  nil
72
74
  else
@@ -8,12 +8,11 @@ module LazyGraph
8
8
 
9
9
  def initialize(details) = @details = details
10
10
  def to_s = "MISSING[#{@details}]"
11
-
12
11
  def inspect = to_s
13
12
  def coerce(other) = [self, other]
14
13
  def as_json = nil
14
+ def to_h = nil
15
15
  def +(other) = other
16
-
17
16
  def respond_to_missing?(_method_name, _include_private = false) = true
18
17
 
19
18
  def method_missing(method, *args, &block)
@@ -16,40 +16,35 @@ module LazyGraph
16
16
  )
17
17
  input = stack_memory.frame
18
18
  @visited[input.object_id >> 2 ^ path.shifted_id] ||= begin
19
+ path_next = path.next
19
20
  if (path_segment = path.segment).is_a?(PathParser::PathGroup)
20
21
  unless path_segment.index?
21
22
  return input.length.times.map do |index|
22
- item = children.fetch_item(input, index, stack_memory)
23
- children.resolve(path, stack_memory.push(item, index))
23
+ children.fetch_and_resolve(path, input, index, stack_memory)
24
24
  end
25
25
  end
26
26
 
27
- return resolve(path_segment.options.first.merge(path.next), stack_memory, nil) if path_segment.options.one?
27
+ return resolve(path_segment.options.first.merge(path_next), stack_memory, nil) if path_segment.options.one?
28
28
 
29
- return path_segment.options.map { |part| resolve(part.merge(path.next), stack_memory, nil) }
29
+ return path_segment.options.map { |part| resolve(part.merge(path_next), stack_memory, nil) }
30
30
  end
31
31
 
32
32
  segment = path_segment&.part
33
33
  case segment
34
34
  when nil
35
- input.length.times do |index|
36
- item = children.fetch_item(input, index, stack_memory)
37
- children.resolve(path, stack_memory.push(item, index))
35
+
36
+ unless @children.simple?
37
+ input.length.times do |index|
38
+ @children.fetch_and_resolve(path, input, index, stack_memory)
39
+ end
38
40
  end
39
41
  input
40
42
  when DIGIT_REGEXP
41
- item = @children.fetch_item(input, segment.to_s.to_i, stack_memory)
42
- children.resolve(path.next, stack_memory.push(item, segment))
43
- when :*
44
- input.length.times.map do |index|
45
- item = children.fetch_item(input, index, stack_memory)
46
- @children.resolve(path.next, stack_memory.push(item, index))
47
- end
43
+ @children.fetch_and_resolve(path_next, input, segment.to_s.to_i, stack_memory)
48
44
  else
49
45
  if @child_properties&.key?(segment) || input&.first&.key?(segment)
50
46
  input.length.times.map do |index|
51
- item = children.fetch_item(input, index, stack_memory)
52
- @children.resolve(path, stack_memory.push(item, index))
47
+ @children.fetch_and_resolve(path, input, index, stack_memory)
53
48
  end
54
49
  else
55
50
  MissingValue()
@@ -25,6 +25,7 @@ module LazyGraph
25
25
  def build_derived_inputs(derived, helpers)
26
26
  @resolvers = {}.compare_by_identity
27
27
  @path_cache = {}.compare_by_identity
28
+ @resolution_stack = {}.compare_by_identity
28
29
 
29
30
  derived = interpret_derived_proc(derived) if derived.is_a?(Proc)
30
31
  derived = { inputs: derived.to_s } if derived.is_a?(String) || derived.is_a?(Symbol)
@@ -124,7 +125,7 @@ module LazyGraph
124
125
  @derived_proc_cache[source_location]
125
126
  rescue StandardError => e
126
127
  LazyGraph.logger.error(e.message)
127
- LazyGraph.logger.error(e.backtrace)
128
+ LazyGraph.logger.error(e.backtrace.join("\n"))
128
129
  raise "Failed to extract expression from source location: #{source_location}. Ensure the file exists and the line number is correct. Extraction from a REPL is not supported"
129
130
  end
130
131
 
@@ -167,9 +168,8 @@ module LazyGraph
167
168
 
168
169
  def parse_rule_string(derived)
169
170
  calc_str = derived[:calc]
170
- src = @src
171
171
  instance_eval(
172
- "->{ begin; #{calc_str}; rescue StandardError => e; LazyGraph.logger.error(\"Exception in \#{src}. \#{e.message}\"); LazyGraph.logger.error(e.backtrace.join(\"\\n\")); raise; end }", __FILE__, __LINE__
172
+ "->{ begin; #{calc_str}; rescue StandardError => e; LazyGraph.logger.error(\"Exception in \#{calc_str}. \#{e.message}\"); LazyGraph.logger.error(e.backtrace.join(\"\\n\")); raise; end }", __FILE__, __LINE__
173
173
  )
174
174
  rescue SyntaxError
175
175
  missing_value = MissingValue { "Syntax error in #{derived[:src]}" }
@@ -194,12 +194,25 @@ module LazyGraph
194
194
  end.new
195
195
  end
196
196
 
197
+ def resolver_for(path)
198
+ segment = path.segment.part
199
+ return root.properties[path.next.segment.part] if segment == :'$'
200
+
201
+ (segment == name ? parent.parent : @parent).find_resolver_for(segment)
202
+ end
203
+
197
204
  def map_derived_inputs_to_paths(inputs)
198
205
  inputs.values.map.with_index do |path, idx|
199
- segment_indexes = path.parts.map.with_index do |segment, i|
200
- segment.is_a?(PathParser::PathGroup) && segment.options.length == 1 ? i : nil
206
+ segments = path.parts.map.with_index do |segment, i|
207
+ if segment.is_a?(PathParser::PathGroup) &&
208
+ segment.options.length == 1 &&
209
+ (resolver = resolver_for(segment.options.first))
210
+ [i, resolver]
211
+ else
212
+ nil
213
+ end
201
214
  end.compact
202
- [path, idx, segment_indexes.any? ? segment_indexes : nil]
215
+ [path, resolver_for(path), idx, segments.any? ? segments : nil]
203
216
  end
204
217
  end
205
218
  end
@@ -28,12 +28,21 @@ module LazyGraph
28
28
  to_h
29
29
  end
30
30
 
31
+ def to_h
32
+ HashUtils.strip_missing(self)
33
+ end
34
+
35
+ def ==(other)
36
+ return super if other.is_a?(self.class)
37
+ return to_h.eql?(other.to_h) if other.respond_to?(:to_h)
38
+
39
+ super
40
+ end
41
+
31
42
  define_method(:each_key, &members.method(:each))
32
43
 
33
44
  def dup
34
- duplicate = self.class.new
35
- members.each { duplicate[_1] = self[_1].dup }
36
- duplicate
45
+ self.class.new(members.each_with_object({}) { _2[_1] = _2[_1].dup })
37
46
  end
38
47
 
39
48
  def get_first_of(*props)
@@ -44,17 +53,18 @@ module LazyGraph
44
53
  end
45
54
 
46
55
  def pretty_print(q)
47
- q.group(1, '<Props ', '>') do
56
+ q.group(1, '<', '>') do
57
+ q.text "#{self.class.name} "
48
58
  q.seplist(members.zip(values).reject do |m, v|
49
- m == :DEBUG && (v.nil? || v.is_a?(MissingValue))
50
- end) do |member, value|
51
- q.group do
52
- q.text "#{member}="
53
- value.respond_to?(:pretty_print) ? q.pp(value) : q.text(value.inspect)
54
- end
59
+ m == :DEBUG || v.nil? || v.is_a?(MissingValue)
60
+ end.to_h) do |k, v|
61
+ q.text "#{k}: "
62
+ q.pp v
55
63
  end
56
64
  end
57
65
  end
66
+
67
+ alias_method :keys, :members
58
68
  end
59
69
  end
60
70
  end
@@ -2,6 +2,8 @@ module LazyGraph
2
2
  class ObjectNode < Node
3
3
  require_relative 'symbol_hash'
4
4
 
5
+ attr_reader :properties
6
+
5
7
  # An object supports the following types of path resolutions.
6
8
  # 1. Property name: obj.property => value
7
9
  # 2. Property name group: obj[property1, property2] => { property1: value1, property2: value2 }
@@ -14,58 +16,39 @@ module LazyGraph
14
16
  )
15
17
  input = stack_memory.frame
16
18
 
17
- @visited[input.object_id >> 2 ^ path.shifted_id] ||= begin
19
+ @visited[(input.object_id >> 2 ^ path.shifted_id) + preserve_keys.object_id] ||= begin
18
20
  return input if input.is_a?(MissingValue)
19
21
 
22
+ path_next = path.next
23
+
20
24
  if (path_segment = path.segment).is_a?(PathParser::PathGroup)
21
25
  return path_segment.options.each_with_object({}.tap(&:compare_by_identity)) do |part, object|
22
- resolve(part.merge(path.next), stack_memory, nil, preserve_keys: object)
26
+ resolve(part.merge(path_next), stack_memory, nil, preserve_keys: object)
23
27
  end
24
28
  end
25
-
26
29
  if !segment = path_segment&.part
27
- @properties_a.each do |key, node|
28
- item = node.fetch_item(input, key, stack_memory)
29
- node.resolve(path.next, stack_memory.push(item, key))
30
+ @complex_properties_a.each do |key, node|
31
+ node.fetch_and_resolve(path_next, input, key, stack_memory)
30
32
  end
31
- if @pattern_properties.any? && input.keys.length > @properties_a.length
33
+ if @complex_pattern_properties_a.any?
32
34
  input.each_key do |key|
33
- node = !@properties[key] && @pattern_properties.find { |(pattern, _value)| pattern.match?(key) }&.last
34
- item = node.fetch_item(input, key, stack_memory)
35
- node.resolve(path.next, stack_memory.push(item, key))
35
+ node = !@properties[key] && @complex_pattern_properties_a.find do |(pattern, _value)|
36
+ pattern.match?(key)
37
+ end&.last
38
+ next unless node
39
+
40
+ node.fetch_and_resolve(path_next, input, key, stack_memory)
36
41
  end
37
42
  end
38
43
  input
39
44
  elsif (prop = @properties[segment])
40
- item = prop.fetch_item(input, segment, stack_memory)
41
- value = prop.resolve(
42
- path.next, stack_memory.push(item, segment)
43
- )
44
- preserve_keys ? preserve_keys[segment] = value : value
45
- elsif segment == :*
46
- # rubocop:disable
47
- (input.keys | @properties_a.map(&:first)).each do |key|
48
- next unless (node = @properties[key] || @pattern_properties.find do |(pattern, _value)|
49
- pattern.match?(key)
50
- end&.last)
51
-
52
- item = node.fetch_item(input, key, stack_memory)
53
- preserve_keys[key] = node.resolve(path.next, stack_memory.push(item, key))
54
- end
45
+ prop.fetch_and_resolve(path_next, input, segment, stack_memory, preserve_keys)
55
46
  elsif (_, prop = @pattern_properties.find { |(key, _val)| key.match?(segment) })
56
- item = prop.fetch_item(input, segment, stack_memory)
57
- value = prop.resolve(
58
- path.next, stack_memory.push(item, segment)
59
- )
60
- preserve_keys ? preserve_keys[segment] = value : value
47
+ prop.fetch_and_resolve(path_next, input, segment, stack_memory, preserve_keys)
61
48
  elsif input.key?(segment)
62
49
  prop = @properties[segment] = lazy_init_node!(input[segment], segment)
63
50
  @properties_a = @properties.to_a
64
- item = prop.fetch_item(input, segment, stack_memory)
65
- value = prop.resolve(
66
- path.next, stack_memory.push(item, segment)
67
- )
68
- preserve_keys ? preserve_keys[segment] = value : value
51
+ prop.fetch_and_resolve(path_next, input, segment, stack_memory, preserve_keys)
69
52
  else
70
53
  value = MissingValue()
71
54
  preserve_keys ? preserve_keys[segment] = value : value
@@ -76,10 +59,10 @@ module LazyGraph
76
59
  end
77
60
 
78
61
  def find_resolver_for(segment)
79
- if segment == :'$'
62
+ if segment.equal?(:'$')
80
63
  root
81
64
  elsif @properties.key?(segment)
82
- self
65
+ @properties[segment]
83
66
  else
84
67
  @parent&.find_resolver_for(segment)
85
68
  end
@@ -88,32 +71,30 @@ module LazyGraph
88
71
  def children=(value)
89
72
  @children = value
90
73
 
91
- @properties = @children.fetch(:properties, {})
92
- @properties.compare_by_identity
74
+ @properties = @children.fetch(:properties, {}).compare_by_identity
93
75
  @pattern_properties = @children.fetch(:pattern_properties, [])
94
76
 
95
- @properties_a = @properties.to_a
77
+ @complex_properties_a = @properties.to_a.reject { _2.simple? }
78
+ @complex_pattern_properties_a = @pattern_properties.reject { _2.simple? }
96
79
 
97
80
  @has_properties = @properties.any? || @pattern_properties.any?
98
81
 
99
- return unless @properties.any? || @pattern_properties.any?
82
+ return unless @has_properties
100
83
 
101
84
  if @pattern_properties.any?
102
85
  @property_class = SymbolHash
103
86
  else
104
87
  invisible = @properties.select { |_k, v| v.invisible }.map(&:first)
105
- @property_class = PROPERTY_CLASSES[{ members: @properties.keys + (@debug && !parent ? [:DEBUG] : []),
106
- invisible: invisible }]
107
- end
108
- define_singleton_method(:cast, build_caster)
109
- end
110
-
111
- private def build_caster
112
- if @property_class
113
- ->(value) { value.is_a?(@property_class) ? value : @property_class.new(value.to_h) }
114
- else
115
- ->(value) { value }
88
+ @property_class = LazyGraph.fetch_property_class(
89
+ path,
90
+ { members: @properties.keys + (@debug && !parent ? [:DEBUG] : []),
91
+ invisible: invisible },
92
+ namespace: root.namespace
93
+ )
116
94
  end
95
+ define_singleton_method(:cast, lambda { |val|
96
+ val.is_a?(@property_class) ? val : @property_class.new(val.to_h)
97
+ })
117
98
  end
118
99
  end
119
100
  end
@@ -1,4 +1,3 @@
1
- require 'debug'
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require 'bigdecimal/util'
@@ -12,8 +11,19 @@ module LazyGraph
12
11
 
13
12
  DIGIT_REGEXP = /^-?\d+$/
14
13
  SAFE_TOKEN_REGEXP = /^[A-Za-z][A-Za-z0-9]*$/
15
- PROPERTY_CLASSES = Hash.new do |h, members|
16
- h[members] = LazyGraph.const_set("NodeProperties#{h.hash.abs}", NodeProperties.build(**members))
14
+ PROPERTY_CLASSES = {}
15
+ UNIQUE_NAME_COUNTER = Hash.new(0)
16
+
17
+ def self.fetch_property_class(name, members, namespace: nil)
18
+ namespace ||= LazyGraph
19
+ PROPERTY_CLASSES[members] ||= begin
20
+ name = name.to_s.capitalize.gsub(/(\.|_)([a-zA-Z])/) do |m|
21
+ m[1].upcase
22
+ end.gsub(/[^a-zA-Z]/, '').then { |n| n.length > 0 ? n : 'NodeProps' }
23
+ index = UNIQUE_NAME_COUNTER[[name, namespace]] += 1
24
+ full_name = "#{name}#{index > 1 ? index : ''}"
25
+ namespace.const_set(full_name, NodeProperties.build(**members))
26
+ end
17
27
  end
18
28
 
19
29
  # Class: Node
@@ -39,27 +49,44 @@ module LazyGraph
39
49
  include DerivedRules
40
50
  attr_accessor :name, :path, :type, :derived, :depth, :parent, :root, :invisible
41
51
  attr_accessor :children
42
- attr_reader :is_object
52
+ attr_reader :is_object, :namespace
53
+
54
+ def simple? = @simple
43
55
 
44
- def initialize(name, path, node, parent, debug: false, helpers: nil)
56
+ def initialize(name, path, node, parent, debug: false, helpers: nil, namespace: nil)
45
57
  @name = name
46
58
  @path = path
47
59
  @parent = parent
48
60
  @debug = debug
49
61
  @depth = parent ? parent.depth + 1 : 0
50
62
  @root = parent ? parent.root : self
63
+ @rule = node[:rule]
51
64
  @type = node[:type]
65
+ @helpers = helpers
52
66
  @invisible = debug ? false : node[:invisible]
53
67
  @visited = {}.compare_by_identity
68
+ @namespace = namespace
69
+
54
70
  instance_variable_set("@is_#{@type}", true)
55
71
  define_singleton_method(:cast, build_caster)
56
72
  define_missing_value_proc!
57
73
 
58
74
  @has_default = node.key?(:default)
59
75
  @default = @has_default ? cast(node[:default]) : MissingValue { @name }
60
- @resolution_stack = []
61
76
 
62
- build_derived_inputs(node[:rule], helpers) if node[:rule]
77
+ # Simple nodes are not a container type, and do not have rule or default
78
+ @simple = !(%i[object array date time timestamp decimal].include?(@type) || node[:rule] || @has_default)
79
+ end
80
+
81
+ def build_derived_inputs!
82
+ build_derived_inputs(@rule, @helpers) if @rule
83
+ return unless @children
84
+ return @children.build_derived_inputs! if @children.is_a?(Node)
85
+
86
+ @children[:properties]&.each_value(&:build_derived_inputs!)
87
+ @children[:pattern_properties]&.each do |(_, node)|
88
+ node.build_derived_inputs!
89
+ end
63
90
  end
64
91
 
65
92
  def define_missing_value_proc!
@@ -69,7 +96,18 @@ module LazyGraph
69
96
  )
70
97
  end
71
98
 
72
- private def build_caster
99
+ def fetch_and_resolve(path, input, segment, stack_memory, preserve_keys = nil, recurse: false)
100
+ item = fetch_item(input, segment, stack_memory)
101
+ unless @simple || item.is_a?(MissingValue)
102
+ item = resolve(
103
+ path,
104
+ stack_memory.push(item, segment)
105
+ )
106
+ end
107
+ preserve_keys ? preserve_keys[segment] = item : item
108
+ end
109
+
110
+ def build_caster
73
111
  if @is_decimal
74
112
  ->(value) { value.is_a?(BigDecimal) ? value : value.to_d }
75
113
  elsif @is_date
@@ -100,9 +138,9 @@ module LazyGraph
100
138
 
101
139
  def clear_visits!
102
140
  @visited.clear
103
- @resolution_stack.clear
104
- @path_cache = {}.clear
105
- @resolvers = {}.clear
141
+ @resolution_stack&.clear
142
+ @path_cache&.clear
143
+ @resolvers&.clear
106
144
 
107
145
  return unless @children
108
146
  return @children.clear_visits! if @children.is_a?(Node)
@@ -113,10 +151,6 @@ module LazyGraph
113
151
  end
114
152
  end
115
153
 
116
- # When we assign children to a node, we preemptively extract the properties, and pattern properties
117
- # in both hash and array form. This micro-optimization pays off when we resolve values in the graph at
118
- # very high frequency.
119
-
120
154
  def resolve(
121
155
  path,
122
156
  stack_memory,
@@ -142,7 +176,7 @@ module LazyGraph
142
176
  when Array then :array
143
177
  end
144
178
  node.children = Node.new(:items, :"#{path}.#{key}[].items", { type: child_type }, node)
145
- node.children.children = { properties: {}, pattern_properties: [] } if child_type == :object
179
+ node.children.children = { properties: {}, pattern_properties: [] } if child_type.equal? :object
146
180
  node
147
181
  else
148
182
  Node.new(key, :"#{path}.#{key}", {}, self)
@@ -161,43 +195,24 @@ module LazyGraph
161
195
  end
162
196
  end
163
197
 
164
- def resolve_input(stack_memory, path, key)
165
- input_id = key.object_id >> 2 ^ stack_memory.shifted_id
166
- if @resolution_stack.include?(input_id)
167
- if @debug
168
- stack_memory.log_debug(
169
- property: "#{stack_memory}.#{key}",
170
- exception: 'Infinite Recursion Detected during dependency resolution'
171
- )
172
- end
173
- return MissingValue { "Infinite Recursion in #{stack_memory} => #{path.to_path_str}" }
174
- end
175
-
176
- @resolution_stack << (input_id)
177
- first_segment = path.segment.part
178
-
179
- resolver_node = @resolvers[first_segment] ||= (first_segment == key ? parent.parent : @parent).find_resolver_for(first_segment)
180
-
181
- if resolver_node
182
- input_frame_pointer = stack_memory.ptr_at(resolver_node.depth)
183
- resolver_node.resolve(
184
- first_segment == :'$' ? path.next : path,
185
- input_frame_pointer,
186
- nil
187
- )
188
- else
189
- MissingValue { path.to_path_str }
190
- end
191
- ensure
192
- @resolution_stack.pop
193
- end
194
-
195
198
  def ancestors
196
199
  @ancestors ||= [self, *(@parent ? @parent.ancestors : [])]
197
200
  end
198
201
 
199
202
  def find_resolver_for(segment)
200
- segment == :'$' ? root : @parent&.find_resolver_for(segment)
203
+ segment.equal?(:'$') ? root : @parent&.find_resolver_for(segment)
204
+ end
205
+
206
+ def resolve_relative_input(stack_memory, path)
207
+ input_frame_pointer = path.absolute? ? stack_memory.root : stack_memory.ptr_at(depth - 1)
208
+ input_frame_pointer.recursion_depth += 1
209
+ return input_frame_pointer.frame[path.first_path_segment.part] if @simple
210
+
211
+ fetch_and_resolve(
212
+ path.absolute? ? path.next.next : path.next, input_frame_pointer.frame, path.first_path_segment.part, input_frame_pointer
213
+ )
214
+ ensure
215
+ input_frame_pointer.recursion_depth -= 1
201
216
  end
202
217
 
203
218
  def fetch_item(input, key, stack)
@@ -217,20 +232,34 @@ module LazyGraph
217
232
 
218
233
  return input[key] = @default unless derived
219
234
 
220
- if @copy_input
221
- copy_item!(input, key, stack, @inputs.first)
222
- else
223
- derive_item!(input, key, stack)
235
+ if stack.recursion_depth >= 8
236
+ input_id = key.object_id >> 2 ^ input.object_id << 28
237
+ if @resolution_stack.key?(input_id)
238
+ if @debug
239
+ stack.log_debug(
240
+ output: :"#{stack}.#{key}",
241
+ exception: 'Infinite Recursion Detected during dependency resolution'
242
+ )
243
+ end
244
+ return MissingValue { "Infinite Recursion in #{stack} => #{key}" }
245
+ end
246
+ @resolution_stack[input_id] = true
224
247
  end
248
+
249
+ @copy_input ? copy_item!(input, key, stack, @inputs.first) : derive_item!(input, key, stack)
250
+ ensure
251
+ @resolution_stack.delete(input_id) if input_id
225
252
  end
226
253
 
227
- def copy_item!(input, key, stack, (path, _i, segment_indexes))
228
- if segment_indexes
229
- missing_value = nil
254
+ def copy_item!(input, key, stack, (path, resolver, _i, segments))
255
+ missing_value = resolver ? nil : MissingValue { key }
256
+ if resolver && segments
230
257
  parts = path.parts.dup
231
258
  parts_identity = path.identity
232
- segment_indexes.each do |index|
233
- part = resolve_input(stack, parts[index].options.first, key)
259
+ segments.each do |index, resolver|
260
+ break missing_value = MissingValue { key } unless resolver
261
+
262
+ part = resolver.resolve_relative_input(stack, parts[index].options.first)
234
263
  break missing_value = part if part.is_a?(MissingValue)
235
264
 
236
265
  part_sym = part.to_s.to_sym
@@ -240,12 +269,12 @@ module LazyGraph
240
269
  path = @path_cache[parts_identity] ||= PathParser::Path.new(parts: parts) unless missing_value
241
270
  end
242
271
 
243
- result = missing_value || cast(resolve_input(stack, path, key))
272
+ result = missing_value || cast(resolver.resolve_relative_input(stack, path))
244
273
 
245
274
  if @debug
246
275
  stack.log_debug(
247
276
  output: :"#{stack}.#{key}",
248
- result: result,
277
+ result: HashUtils.deep_dup(result),
249
278
  inputs: @node_context.to_h.except(:itself, :stack_ptr),
250
279
  calc: @src
251
280
  )
@@ -254,13 +283,15 @@ module LazyGraph
254
283
  end
255
284
 
256
285
  def derive_item!(input, key, stack)
257
- @inputs.each do |path, i, segment_indexes|
258
- if segment_indexes
286
+ @inputs.each do |path, resolver, i, segments|
287
+ if segments
259
288
  missing_value = nil
260
289
  parts = path.parts.dup
261
290
  parts_identity = path.identity
262
- segment_indexes.each do |index|
263
- part = resolve_input(stack, parts[index].options.first, key)
291
+ segments.each do |index, resolver|
292
+ break missing_value = MissingValue { key } unless resolver
293
+
294
+ part = resolver.resolve_relative_input(stack, parts[index].options.first)
264
295
  break missing_value = part if part.is_a?(MissingValue)
265
296
 
266
297
  part_sym = part.to_s.to_sym
@@ -269,7 +300,7 @@ module LazyGraph
269
300
  end
270
301
  path = @path_cache[parts_identity] ||= PathParser::Path.new(parts: parts) unless missing_value
271
302
  end
272
- result = missing_value || resolve_input(stack, path, key)
303
+ result = missing_value || resolver.resolve_relative_input(stack, path)
273
304
  @node_context[i] = result.is_a?(MissingValue) ? nil : result
274
305
  end
275
306
 
@@ -10,25 +10,33 @@ module LazyGraph
10
10
  Path = Struct.new(:parts, keyword_init: true) do
11
11
  def next = @next ||= parts.length <= 1 ? Path::BLANK : Path.new(parts: parts[1..])
12
12
  def empty? = @empty ||= parts.empty?
13
+ def length = @length ||= parts.length
13
14
  def segment = @segment ||= parts&.[](0)
14
- def index? = @index ||= !empty? && segment&.index?
15
+ def absolute? = instance_variable_defined?(:@absolute) ? @absolute : (@absolute = segment&.part.equal?(:'$'))
16
+ def index? = @index ||= !empty? && segment&.index?
15
17
  def identity = @identity ||= parts&.each_with_index&.reduce(0) { |acc, (p, i)| acc ^ (p.object_id) << (i * 8) }
16
18
  def map(&block) = empty? ? self : Path.new(parts: parts.map(&block))
17
19
  def shifted_id = @shifted_id ||= object_id << 28
20
+ def first_path_segment = @first_path_segment ||= absolute? ? self.next.segment : segment
18
21
 
19
22
  def merge(other)
20
- (@merged ||= {})[other] ||= \
21
- if other.empty?
22
- self
23
- else
24
- empty? ? other : Path.new(parts: parts + other.parts)
25
- end
23
+ if other.empty?
24
+ self
25
+ else
26
+ empty? ? other : Path.new(parts: parts + other.parts)
27
+ end
26
28
  end
27
29
 
28
30
  def to_path_str
29
31
  @to_path_str ||= create_path_str
30
32
  end
31
33
 
34
+ def ==(other)
35
+ return parts == other if other.is_a?(Array)
36
+
37
+ super
38
+ end
39
+
32
40
  private
33
41
 
34
42
  def create_path_str
@@ -7,6 +7,12 @@ module LazyGraph
7
7
  def index?
8
8
  @index ||= options.all?(&:index?)
9
9
  end
10
+
11
+ def ==(other)
12
+ return options == other if other.is_a?(Array)
13
+
14
+ super
15
+ end
10
16
  end
11
17
  end
12
18
  end
@@ -8,6 +8,14 @@ module LazyGraph
8
8
  def index?
9
9
  @index ||= part =~ INDEX_REGEXP
10
10
  end
11
+
12
+ def ==(other)
13
+ return part == other.to_sym if other.is_a?(String)
14
+ return part == other if other.is_a?(Symbol)
15
+ return part == other if other.is_a?(Array)
16
+
17
+ super
18
+ end
11
19
  end
12
20
  end
13
21
  end
@@ -19,7 +19,7 @@ module LazyGraph
19
19
  require_relative 'path_parser/path_part'
20
20
  # This module is responsible for parsing complex path strings into structured components.
21
21
  # Public class method to parse the path string
22
- def self.parse(path, strip_root = false)
22
+ def self.parse(path, strip_root: false)
23
23
  return Path::BLANK if path.nil? || path.empty?
24
24
 
25
25
  start = strip_root && path.to_s.start_with?('$.') ? 2 : 0
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'rack'
4
+
3
5
  module LazyGraph
4
6
  class Server
5
7
  ALLOWED_VALUES_VALIDATE = [true, false, nil, 'input', 'context'].to_set.freeze
6
8
  ALLOWED_VALUES_DEBUG = [true, false, nil].to_set.freeze
7
9
 
10
+ attr_reader :routes
11
+
8
12
  def initialize(routes: {})
9
13
  @routes = routes.transform_keys(&:to_sym).compare_by_identity
10
14
  end
@@ -59,6 +63,7 @@ module LazyGraph
59
63
  rescue StandardError => e
60
64
  LazyGraph.logger.error(e.message)
61
65
  LazyGraph.logger.error(e.backtrace.join("\n"))
66
+
62
67
  return error!(request, 500, 'Internal Server Error', e.message)
63
68
  end
64
69
  end
@@ -4,35 +4,38 @@ module LazyGraph
4
4
  # Module to provide lazy graph functionalities using stack pointers.
5
5
  POINTER_POOL = []
6
6
 
7
- StackPointer = Struct.new(:parent, :frame, :depth, :key, :root) do
7
+ StackPointer = Struct.new(:parent, :frame, :depth, :recursion_depth, :key, :root) do
8
8
  attr_accessor :pointer_cache
9
9
 
10
- def shifted_id = @shifted_id ||= object_id << 28
11
-
10
+ # Pushes a new frame onto the stack, creating or reusing a StackPointer.
11
+ # Frames represent activation contexts; keys are identifiers within those frames.
12
12
  def push(frame, key)
13
- if (ptr = POINTER_POOL.pop)
14
- ptr.parent = self
15
- ptr.parent = self
16
- ptr.frame = frame
17
- ptr.key = key
18
- ptr.depth = depth + 1
19
- ptr.pointer_cache&.clear
20
- ptr
21
- else
22
- StackPointer.new(parent: self, frame: frame, key: key, depth: depth + 1, root: root || self)
23
- end
13
+ ptr = POINTER_POOL.pop || StackPointer.new
14
+ ptr.parent = self
15
+ ptr.root = root || self
16
+ ptr.frame = frame
17
+ ptr.key = key
18
+ ptr.depth = depth + 1
19
+ ptr.recursion_depth = recursion_depth
20
+ ptr.pointer_cache&.clear
21
+ ptr
24
22
  end
25
23
 
24
+ # Recycles the current StackPointer by adding it back to the pointer pool.
25
+ # Once recycled, this instance should no longer be used unless reassigned by push.
26
26
  def recycle!
27
27
  POINTER_POOL.push(self)
28
28
  nil
29
29
  end
30
30
 
31
+ # Retrieves the StackPointer at a specific index in the upward chain of parents.
31
32
  def ptr_at(index)
32
33
  @pointer_cache ||= {}.compare_by_identity
33
34
  @pointer_cache[index] ||= depth == index ? self : parent&.ptr_at(index)
34
35
  end
35
36
 
37
+ # Handles method calls not explicitly defined in this class by delegating them
38
+ # first to the frame, then to the parent, recursively up the stack.
36
39
  def method_missing(name, *args, &block)
37
40
  if frame.respond_to?(name)
38
41
  frame.send(name, *args, &block)
@@ -43,20 +46,25 @@ module LazyGraph
43
46
  end
44
47
  end
45
48
 
49
+ # Returns the key associated with this stack pointer's frame.
46
50
  def index
47
51
  key
48
52
  end
49
53
 
54
+ # Logs debugging information related to this stack pointer in the root frame's DEBUG section.
50
55
  def log_debug(**log_item)
51
56
  root.frame[:DEBUG] = [] if !root.frame[:DEBUG] || root.frame[:DEBUG].is_a?(MissingValue)
52
57
  root.frame[:DEBUG] << { **log_item, location: to_s }
53
58
  nil
54
59
  end
55
60
 
61
+ # Determines if the stack pointer can respond to a missing method by mimicking the behavior
62
+ # of the frame or any parent stack pointers recursively.
56
63
  def respond_to_missing?(name, include_private = false)
57
64
  frame.respond_to?(name, include_private) || parent.respond_to?(name, include_private)
58
65
  end
59
66
 
67
+ # Returns a string representation of the stacking path of keys up to this pointer.
60
68
  def to_s
61
69
  if parent
62
70
  "#{parent}#{key.to_s =~ /\d+/ ? "[#{key}]" : ".#{key}"}"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LazyGraph
4
- VERSION = '0.1.2'
4
+ VERSION = '0.1.6'
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lazy_graph
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wouter Coppieters
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2024-12-25 00:00:00.000000000 Z
10
+ date: 2024-12-29 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: json-schema