lazy_graph 0.1.3 → 0.1.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 161365560b253072239da129afecb0d43a9f1aa0072a8a81668a25d4013a33ce
4
- data.tar.gz: 3d8835baa0e9ae05e3ed0a8438c7b18a0e2c7f5b55e3ab4569bc0f17c30bce0c
3
+ metadata.gz: 2cb285016525832f149dd41b91340c71f6f6c68d7aebcc0de6376dbf81c71cd8
4
+ data.tar.gz: fc0bee81e2e2b45dfad51c822c421d0496bda877d8e77598d29875b66f261286
5
5
  SHA512:
6
- metadata.gz: 12e62d31f1aa8d45d61b1de31e5d890ecb56be3ddfac5977431592723079ebe46ae120d489ddacb4eea1b458ab5efb8f68ff0b4e3ac38085a69caa78db6f29b3
7
- data.tar.gz: af1ad4be372659110fae0299f84247f48f76b37ce4a1344d2543ebd093ce93525fe50e76bc0f4866dc4357184cceb1c9bac650cccbe8e2dd4443ce1840552cfc
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,6 +28,17 @@ 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
@@ -42,17 +53,18 @@ module LazyGraph
42
53
  end
43
54
 
44
55
  def pretty_print(q)
45
- q.group(1, '<Props ', '>') do
56
+ q.group(1, '<', '>') do
57
+ q.text "#{self.class.name} "
46
58
  q.seplist(members.zip(values).reject do |m, v|
47
- m == :DEBUG && (v.nil? || v.is_a?(MissingValue))
48
- end) do |member, value|
49
- q.group do
50
- q.text "#{member}="
51
- value.respond_to?(:pretty_print) ? q.pp(value) : q.text(value.inspect)
52
- 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
53
63
  end
54
64
  end
55
65
  end
66
+
67
+ alias_method :keys, :members
56
68
  end
57
69
  end
58
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.3'
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.3
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