thunderframe 0.2.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/README.md +3 -0
- data/lib/thunderframe/association.rb +51 -0
- data/lib/thunderframe/condition.rb +30 -0
- data/lib/thunderframe/field.rb +47 -0
- data/lib/thunderframe/frame.rb +123 -0
- data/lib/thunderframe/preloader.rb +130 -0
- data/lib/thunderframe/version.rb +5 -0
- data/lib/thunderframe.rb +19 -0
- metadata +72 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b9c6e4b83f0233fc6d8f91d1e5a5285f16136964d2837753b76dd8a0dc1be928
|
|
4
|
+
data.tar.gz: c28f8d42fa7db56fd8f7e78b771820ca8e4869d3d3d7e5e3c86acfcdcf03306f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 61cd9d6e6956b6d873425c6b8e7ede0d8f6338066772396da1a3e328c5369ed44fa9ea964a98cc199bc255712e033aa254f19e85538a46ec950dbdcde6744aae
|
|
7
|
+
data.tar.gz: cb16f8a545735240c5a947e98d930c3ea9d5726b41bd5007a8434568ca73a9126c08a26df790297aac6f4f3364510e314f333bbfb1c4419345e1b2da75d87546
|
data/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderFrame
|
|
4
|
+
class Association
|
|
5
|
+
attr_reader :name, :frame, :options
|
|
6
|
+
|
|
7
|
+
def initialize(name, frame, options)
|
|
8
|
+
@name = name
|
|
9
|
+
@frame = frame
|
|
10
|
+
@options = options
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def relation_name
|
|
14
|
+
final_name = options[:as] || name
|
|
15
|
+
final_name = options[:eager_loads] if options[:eager_loads].is_a?(String) || options[:eager_loads].is_a?(Symbol)
|
|
16
|
+
|
|
17
|
+
final_name.to_sym
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def render(entity, context)
|
|
21
|
+
frame.render_hash(value(entity, context), context:)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# A callable `from` is not an association name, so it cannot be preloaded by itself: in that case only an
|
|
25
|
+
# explicit `eager_loads` says what to preload, and without it nothing is preloaded.
|
|
26
|
+
def preload_key
|
|
27
|
+
return options[:from] || relation_name unless options[:from].respond_to?(:call)
|
|
28
|
+
|
|
29
|
+
relation_name if options[:eager_loads].is_a?(String) || options[:eager_loads].is_a?(Symbol)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def prepare_preload
|
|
33
|
+
@prepare_preload ||= begin
|
|
34
|
+
below_preloads = frame.prepare_preload
|
|
35
|
+
|
|
36
|
+
{}.tap do |hash|
|
|
37
|
+
hash[preload_key] = below_preloads if preload_key && (below_preloads.any? || options[:eager_loads])
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def value(entity, context)
|
|
45
|
+
from = options[:from] || name
|
|
46
|
+
return from.arity == 2 ? from.call(entity, context) : from.call(entity) if from.respond_to?(:call)
|
|
47
|
+
|
|
48
|
+
entity.is_a?(Hash) ? entity[from] || entity[from.to_s] : entity.public_send(from)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderFrame
|
|
4
|
+
class Condition
|
|
5
|
+
attr_reader :test, :frame
|
|
6
|
+
|
|
7
|
+
def initialize(test, frame)
|
|
8
|
+
@test = test
|
|
9
|
+
@frame = frame
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def applies?(entity)
|
|
13
|
+
case test
|
|
14
|
+
when Class then entity.is_a?(test)
|
|
15
|
+
when Symbol then entity.public_send(test)
|
|
16
|
+
else test.call(entity)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def prepare_preload
|
|
21
|
+
@prepare_preload ||= begin
|
|
22
|
+
below_preloads = frame.prepare_preload
|
|
23
|
+
|
|
24
|
+
{}.tap do |hash|
|
|
25
|
+
hash[test] = below_preloads if below_preloads.any? && test.is_a?(Class)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderFrame
|
|
4
|
+
class Field
|
|
5
|
+
attr_reader :name, :codec, :options, :block
|
|
6
|
+
|
|
7
|
+
def initialize(name, codec, options, block)
|
|
8
|
+
@name = name
|
|
9
|
+
@codec = codec
|
|
10
|
+
@options = options
|
|
11
|
+
@block = block
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def relation_name
|
|
15
|
+
final_name = options[:as] || name
|
|
16
|
+
final_name = options[:eager_loads] if options[:eager_loads].is_a?(String) || options[:eager_loads].is_a?(Symbol)
|
|
17
|
+
|
|
18
|
+
final_name.to_sym
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def prepare_preload
|
|
22
|
+
eager = options[:eager_loads]
|
|
23
|
+
return {} unless eager
|
|
24
|
+
return eager if eager.is_a?(Hash)
|
|
25
|
+
|
|
26
|
+
{ relation_name => {} }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def extract(entity, context, label)
|
|
30
|
+
value = read(entity, context)
|
|
31
|
+
codec.coerce(value)
|
|
32
|
+
rescue ArgumentError
|
|
33
|
+
raise Error, "#{label}: #{name} must be #{codec.name}, got #{value.class} (#{value.inspect})"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def read(entity, context)
|
|
39
|
+
return block.arity == 2 ? block.call(entity, context) : block.call(entity) if block
|
|
40
|
+
|
|
41
|
+
source = options[:from] || name
|
|
42
|
+
return entity.public_send(source) unless entity.is_a?(Hash)
|
|
43
|
+
|
|
44
|
+
entity.key?(source) ? entity[source] : entity[source.to_s]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ThunderFrame
|
|
4
|
+
class Frame
|
|
5
|
+
attr_reader :associations, :conditions
|
|
6
|
+
|
|
7
|
+
def initialize(const_name, owner:, parent: nil, &block)
|
|
8
|
+
@const_name = const_name
|
|
9
|
+
@owner = owner
|
|
10
|
+
@parent = parent
|
|
11
|
+
@fields = []
|
|
12
|
+
@associations = []
|
|
13
|
+
@conditions = []
|
|
14
|
+
instance_exec(&block) if block
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def fields(*names)
|
|
18
|
+
return @fields if names.empty?
|
|
19
|
+
|
|
20
|
+
names.each { |name| field(name) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def field(name, type = String, **options, &block)
|
|
24
|
+
check_name!(name)
|
|
25
|
+
@fields << Field.new(name.to_sym, ThunderFoundation::Types.fetch(type), options, block)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def association(name, **options, &block)
|
|
29
|
+
check_name!(name)
|
|
30
|
+
@associations << Association.new(name.to_sym, nested(name, &block), options)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def condition(test, &block)
|
|
34
|
+
@conditions << Condition.new(test, nested("Condition#{@conditions.size + 1}", &block))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def members
|
|
38
|
+
fields.map(&:name) + associations.map(&:name) + conditions.flat_map { |condition| condition.frame.members }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# A Hash responds to each, but it is an entity and not a collection of entities; a Struct too.
|
|
42
|
+
def render_hash(entity, context: nil, skip_preload: false)
|
|
43
|
+
return if entity.nil?
|
|
44
|
+
return render_many_hash(entity, context:, skip_preload:) if collection?(entity)
|
|
45
|
+
|
|
46
|
+
hash = {}
|
|
47
|
+
fields.each { |field| hash[field.name] = field.extract(entity, context, label) }
|
|
48
|
+
associations.each { |association| hash[association.name] = association.render(entity, context) }
|
|
49
|
+
conditions.each { |condition| hash.merge!(condition.frame.render_hash(entity, context:)) if condition.applies?(entity) }
|
|
50
|
+
hash
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# render_hash for a Hash that must have exactly the declared shape: a key the frame does not declare, or a declared
|
|
54
|
+
# field or association left out, raises Error. Associations are checked the same way, item by item when they hold
|
|
55
|
+
# a list. A field or association declared with `optional: true`, and the fields of a condition, may be left out.
|
|
56
|
+
def render_hash!(entity, context: nil)
|
|
57
|
+
return if entity.nil?
|
|
58
|
+
return entity.map { |item| render_hash!(item, context:) } if entity.is_a?(Array)
|
|
59
|
+
raise Error, "#{label} must be a Hash, got #{entity.class}" unless entity.is_a?(Hash)
|
|
60
|
+
|
|
61
|
+
keys = entity.keys.map(&:to_sym)
|
|
62
|
+
declared = fields + associations
|
|
63
|
+
required = declared.reject { |member| member.options[:optional] }.map(&:name)
|
|
64
|
+
allowed = declared.map(&:name) + conditions.flat_map { |condition| condition.frame.members }
|
|
65
|
+
unknown = keys - allowed
|
|
66
|
+
missing = required - keys
|
|
67
|
+
|
|
68
|
+
errors = []
|
|
69
|
+
errors << "unknown #{unknown.join(', ')}" if unknown.any?
|
|
70
|
+
errors << "missing #{missing.join(', ')}" if missing.any?
|
|
71
|
+
raise Error, "#{label}: #{errors.join('; ')}" if errors.any?
|
|
72
|
+
|
|
73
|
+
hash = {}
|
|
74
|
+
fields.each { |field| hash[field.name] = field.extract(entity, context, label) }
|
|
75
|
+
associations.each do |association|
|
|
76
|
+
value = entity.key?(association.name) ? entity[association.name] : entity[association.name.to_s]
|
|
77
|
+
hash[association.name] = association.frame.render_hash!(value, context:)
|
|
78
|
+
end
|
|
79
|
+
conditions.each { |condition| hash.merge!(condition.frame.render_hash(entity, context:)) if condition.applies?(entity) }
|
|
80
|
+
hash
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Only records of ActiveRecord are preloaded; a collection of anything else is rendered as it is.
|
|
84
|
+
def render_many_hash(enumerable, context: nil, skip_preload: false)
|
|
85
|
+
records = enumerable.to_a
|
|
86
|
+
Preloader.call(records, prepare_preload) if !skip_preload && defined?(::ActiveRecord::Base) && records.first.is_a?(::ActiveRecord::Base)
|
|
87
|
+
|
|
88
|
+
records.map { |entity| render_hash(entity, context:, skip_preload: true) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def render_json(entity, root: :data)
|
|
92
|
+
{ root => render_hash(entity) }.to_json
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
alias render render_json
|
|
96
|
+
|
|
97
|
+
def prepare_preload
|
|
98
|
+
@prepare_preload ||= begin
|
|
99
|
+
hash = associations.each_with_object({}) { |association, acc| acc.deep_merge!(association.prepare_preload) }
|
|
100
|
+
hash = fields.each_with_object(hash) { |field, acc| acc.deep_merge!(field.prepare_preload) }
|
|
101
|
+
conditions.each_with_object(hash) { |condition, acc| acc.deep_merge!(condition.prepare_preload) }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def label
|
|
106
|
+
@parent ? "#{@parent.label}.#{@const_name.to_s.underscore}" : "#{@owner.name}::#{@const_name}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def nested(name, &block)
|
|
112
|
+
Frame.new(name, owner: @owner, parent: self, &block)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def check_name!(name)
|
|
116
|
+
raise Error, "#{label}: #{name} declared twice" if members.include?(name.to_sym)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def collection?(entity)
|
|
120
|
+
entity.is_a?(Enumerable) && !entity.is_a?(Hash) && !entity.is_a?(Struct)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Ported from the Framer of tcard_backend (lib/framer/preloader.rb). Preloads the associations of ActiveRecord
|
|
4
|
+
# records, reusing records already loaded instead of querying them again.
|
|
5
|
+
module ThunderFrame
|
|
6
|
+
class Preloader
|
|
7
|
+
def self.call(records, association)
|
|
8
|
+
new.call(records, association)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@available_records = []
|
|
13
|
+
@cached_record_ids = Hash.new { |h, k| h[k] = Set.new }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(records, association)
|
|
17
|
+
records = records.to_a if records.respond_to?(:to_a) && !records.is_a?(Array)
|
|
18
|
+
return [] if records.empty?
|
|
19
|
+
|
|
20
|
+
case association
|
|
21
|
+
when Symbol, String
|
|
22
|
+
result = ActiveRecord::Associations::Preloader.new(
|
|
23
|
+
records:, associations: association, available_records: @available_records
|
|
24
|
+
).call
|
|
25
|
+
cache_preloaded_records(result)
|
|
26
|
+
result
|
|
27
|
+
when Hash
|
|
28
|
+
association.each do |key, value|
|
|
29
|
+
preloader_result = call(records, key)
|
|
30
|
+
|
|
31
|
+
handle_preloader_result(preloader_result, value)
|
|
32
|
+
end
|
|
33
|
+
when Array
|
|
34
|
+
association.each do |value|
|
|
35
|
+
call(records, value)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def all_records_from(result)
|
|
43
|
+
seen = Set.new
|
|
44
|
+
records = []
|
|
45
|
+
|
|
46
|
+
result.records_by_owner.each_value do |owner_records|
|
|
47
|
+
owner_records.each do |record|
|
|
48
|
+
next unless seen.add?(record.object_id)
|
|
49
|
+
|
|
50
|
+
records << record
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
return records if records.any?
|
|
55
|
+
|
|
56
|
+
result.send(:owners).each do |owner|
|
|
57
|
+
assoc = owner.association(result.send(:reflection).name)
|
|
58
|
+
next unless assoc.loaded?
|
|
59
|
+
|
|
60
|
+
target = assoc.target
|
|
61
|
+
targets = target.is_a?(Array) ? target : [target].compact
|
|
62
|
+
targets.each do |record|
|
|
63
|
+
next unless seen.add?(record.object_id)
|
|
64
|
+
|
|
65
|
+
records << record
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
records
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def cache_preloaded_records(preloader_result)
|
|
73
|
+
preloader_result.each do |result|
|
|
74
|
+
all_records_from(result).each do |record|
|
|
75
|
+
key = record.class.base_class.name
|
|
76
|
+
next if @cached_record_ids[key].include?(record.id)
|
|
77
|
+
|
|
78
|
+
@cached_record_ids[key] << record.id
|
|
79
|
+
@available_records << record
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def resolve_class_key(preloader_result, key)
|
|
85
|
+
indexed_result = preloader_result.index_by(&:klass)
|
|
86
|
+
|
|
87
|
+
return all_records_from(indexed_result[key]) if indexed_result[key]
|
|
88
|
+
|
|
89
|
+
parent_result = indexed_result.find { |klass, _| key < klass }&.last
|
|
90
|
+
return unless parent_result
|
|
91
|
+
|
|
92
|
+
all_records_from(parent_result).select { |r| r.is_a?(key) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def handle_preloader_result(preloader_result, association)
|
|
96
|
+
return unless preloader_result.any?
|
|
97
|
+
|
|
98
|
+
case association
|
|
99
|
+
when Hash
|
|
100
|
+
association.each do |key, value|
|
|
101
|
+
if key.is_a?(Class)
|
|
102
|
+
records = resolve_class_key(preloader_result, key)
|
|
103
|
+
call(records, value) if records&.any?
|
|
104
|
+
else
|
|
105
|
+
preloader_result.each do |result|
|
|
106
|
+
sub_result = call(all_records_from(result), key)
|
|
107
|
+
|
|
108
|
+
handle_preloader_result(sub_result, value) if sub_result&.any?
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
when Array
|
|
113
|
+
association.each do |value|
|
|
114
|
+
case value
|
|
115
|
+
when Hash
|
|
116
|
+
handle_preloader_result(preloader_result, value)
|
|
117
|
+
else
|
|
118
|
+
preloader_result.each do |result|
|
|
119
|
+
call(all_records_from(result), value)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
else
|
|
124
|
+
preloader_result.each do |result|
|
|
125
|
+
call(all_records_from(result), association)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
data/lib/thunderframe.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support'
|
|
4
|
+
require 'active_support/core_ext/string/inflections'
|
|
5
|
+
require 'active_support/core_ext/hash/deep_merge'
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'thunderfoundation'
|
|
8
|
+
|
|
9
|
+
require_relative 'thunderframe/version'
|
|
10
|
+
|
|
11
|
+
module ThunderFrame
|
|
12
|
+
class Error < StandardError; end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
require_relative 'thunderframe/field'
|
|
16
|
+
require_relative 'thunderframe/association'
|
|
17
|
+
require_relative 'thunderframe/condition'
|
|
18
|
+
require_relative 'thunderframe/preloader'
|
|
19
|
+
require_relative 'thunderframe/frame'
|
metadata
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: thunderframe
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Albert Alef
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '8.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '8.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: thunderfoundation
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - '='
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 0.2.0
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - '='
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 0.2.0
|
|
40
|
+
executables: []
|
|
41
|
+
extensions: []
|
|
42
|
+
extra_rdoc_files: []
|
|
43
|
+
files:
|
|
44
|
+
- README.md
|
|
45
|
+
- lib/thunderframe.rb
|
|
46
|
+
- lib/thunderframe/association.rb
|
|
47
|
+
- lib/thunderframe/condition.rb
|
|
48
|
+
- lib/thunderframe/field.rb
|
|
49
|
+
- lib/thunderframe/frame.rb
|
|
50
|
+
- lib/thunderframe/preloader.rb
|
|
51
|
+
- lib/thunderframe/version.rb
|
|
52
|
+
licenses: []
|
|
53
|
+
metadata:
|
|
54
|
+
rubygems_mfa_required: 'true'
|
|
55
|
+
rdoc_options: []
|
|
56
|
+
require_paths:
|
|
57
|
+
- lib
|
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '3.4'
|
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
requirements: []
|
|
69
|
+
rubygems_version: 3.6.9
|
|
70
|
+
specification_version: 4
|
|
71
|
+
summary: Declares the shape of a result and renders entities into immutable Data
|
|
72
|
+
test_files: []
|