forest_liana 9.3.13 → 9.3.15
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 +4 -4
- data/app/services/forest_liana/base_getter.rb +22 -29
- data/app/services/forest_liana/filters_parser.rb +7 -11
- data/app/services/forest_liana/resources_getter.rb +97 -116
- data/lib/forest_liana/version.rb +1 -1
- data/spec/services/forest_liana/filters_parser_spec.rb +1 -1
- data/spec/services/forest_liana/resources_getter_spec.rb +25 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 53102b046de3c8137e31de9c1cdd7581d860e497ef870f0fee0c08e4faa34c3f
|
|
4
|
+
data.tar.gz: 1eaf3d98e252b8683fb6b2398058dd86b269b45df5f6644c488d2aba5c110206
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6ccf9b6efd785f8ffcc39f0e0514ba73c408b98ae5a842695279b38127fb17cc8e5b555391596c5e2132933cb981a9916d641ff5cb7fb58481cbe5928b1a5d1c
|
|
7
|
+
data.tar.gz: 58c7b9630f16d8cbb00fa4c058a6002c635f3f07c0c3a0183989b9acbeda16667864a3e183fb1557faf6a38c1b6062056f720364722148fbfcd8facd6e4e4a3a
|
|
@@ -5,23 +5,14 @@ module ForestLiana
|
|
|
5
5
|
end
|
|
6
6
|
|
|
7
7
|
def get_resource
|
|
8
|
-
|
|
9
|
-
.include? :really_destroyed?
|
|
10
|
-
|
|
11
|
-
# NOTICE: Do not unscope with the paranoia gem to prevent the retrieval
|
|
12
|
-
# of deleted records.
|
|
13
|
-
use_act_as_paranoid ? @resource : @resource.unscoped
|
|
8
|
+
@resource.instance_methods.include?(:really_destroyed?) ? @resource : @resource.unscoped
|
|
14
9
|
end
|
|
15
10
|
|
|
16
11
|
def includes_for_serialization
|
|
17
|
-
includes_initial = @includes
|
|
18
12
|
includes_for_smart_belongs_to = @collection.fields_smart_belongs_to.map { |field| field[:field] }
|
|
13
|
+
includes_for_smart_belongs_to &= @field_names_requested if @field_names_requested
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
includes_for_smart_belongs_to = includes_for_smart_belongs_to & @field_names_requested
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
includes_initial.concat(includes_for_smart_belongs_to).map(&:to_s)
|
|
15
|
+
@includes.concat(includes_for_smart_belongs_to).map(&:to_s)
|
|
25
16
|
end
|
|
26
17
|
|
|
27
18
|
private
|
|
@@ -31,34 +22,36 @@ module ForestLiana
|
|
|
31
22
|
end
|
|
32
23
|
|
|
33
24
|
def optimize_record_loading(resource, records)
|
|
34
|
-
|
|
25
|
+
polymorphic, preload_loads = analyze_associations(resource)
|
|
26
|
+
result = records.eager_load(@includes.uniq - preload_loads - polymorphic)
|
|
35
27
|
|
|
28
|
+
result = result.preload(preload_loads) if Rails::VERSION::MAJOR >= 7
|
|
29
|
+
|
|
30
|
+
result
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def analyze_associations(resource)
|
|
36
34
|
polymorphic = []
|
|
37
|
-
preload_loads = @includes.select do |name|
|
|
35
|
+
preload_loads = @includes.uniq.select do |name|
|
|
38
36
|
association = resource.reflect_on_association(name)
|
|
39
37
|
if SchemaUtils.polymorphic?(association)
|
|
40
38
|
polymorphic << association.name
|
|
41
39
|
false
|
|
42
40
|
else
|
|
43
|
-
|
|
44
|
-
targetModelDatabase = targetModelConnection.current_database if targetModelConnection.respond_to? :current_database
|
|
45
|
-
resourceConnection = resource.connection
|
|
46
|
-
resourceDatabase = resourceConnection.current_database if resourceConnection.respond_to? :current_database
|
|
47
|
-
|
|
48
|
-
targetModelDatabase != resourceDatabase
|
|
41
|
+
separate_database?(resource, association)
|
|
49
42
|
end
|
|
50
|
-
end + instance_dependent_associations
|
|
43
|
+
end + instance_dependent_associations(resource)
|
|
51
44
|
|
|
52
|
-
|
|
45
|
+
[polymorphic, preload_loads]
|
|
46
|
+
end
|
|
53
47
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
end
|
|
48
|
+
def separate_database?(resource, association)
|
|
49
|
+
target_model_connection = association.klass.connection
|
|
50
|
+
target_model_database = target_model_connection.current_database if target_model_connection.respond_to? :current_database
|
|
51
|
+
resource_connection = resource.connection
|
|
52
|
+
resource_database = resource_connection.current_database if resource_connection.respond_to? :current_database
|
|
60
53
|
|
|
61
|
-
|
|
54
|
+
target_model_database != resource_database
|
|
62
55
|
end
|
|
63
56
|
|
|
64
57
|
def instance_dependent_associations(resource)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module ForestLiana
|
|
2
2
|
class FiltersParser
|
|
3
|
-
AGGREGATOR_OPERATOR = %w(and or)
|
|
3
|
+
AGGREGATOR_OPERATOR = %w(and or).freeze
|
|
4
4
|
|
|
5
5
|
def initialize(filters, resource, timezone, params = nil)
|
|
6
6
|
@filters = filters
|
|
@@ -13,18 +13,14 @@ module ForestLiana
|
|
|
13
13
|
def apply_filters
|
|
14
14
|
return @resource unless @filters
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
return @resource unless
|
|
16
|
+
where_clause = parse_aggregation(@filters)
|
|
17
|
+
return @resource unless where_clause
|
|
18
18
|
|
|
19
19
|
@joins.each do |join|
|
|
20
|
-
|
|
21
|
-
current_resource.include(ArelHelpers::Aliases)
|
|
22
|
-
current_resource.aliased_as(join.name) do |aliased_resource|
|
|
23
|
-
@resource = @resource.joins(ArelHelpers.join_association(@resource, join.name, Arel::Nodes::OuterJoin, aliases: [aliased_resource]))
|
|
24
|
-
end
|
|
20
|
+
@resource = @resource.eager_load(join.name)
|
|
25
21
|
end
|
|
26
22
|
|
|
27
|
-
@resource.where(
|
|
23
|
+
@resource.where(where_clause)
|
|
28
24
|
end
|
|
29
25
|
|
|
30
26
|
def parse_aggregation(node)
|
|
@@ -170,8 +166,8 @@ module ForestLiana
|
|
|
170
166
|
current_resource = @resource.reflect_on_association(field.split(':').first.to_sym)&.klass
|
|
171
167
|
raise ForestLiana::Errors::HTTP422Error.new("Field '#{field}' not found") unless current_resource
|
|
172
168
|
|
|
173
|
-
|
|
174
|
-
quoted_table_name =
|
|
169
|
+
get_association_name_for_condition(field)
|
|
170
|
+
quoted_table_name = current_resource.table_name
|
|
175
171
|
field_name = field.split(':')[1]
|
|
176
172
|
else
|
|
177
173
|
quoted_table_name = @resource.quoted_table_name
|
|
@@ -1,68 +1,32 @@
|
|
|
1
1
|
module ForestLiana
|
|
2
2
|
class ResourcesGetter < BaseGetter
|
|
3
|
-
attr_reader :search_query_builder
|
|
4
|
-
attr_reader :includes
|
|
5
|
-
attr_reader :records_count
|
|
3
|
+
attr_reader :search_query_builder, :includes, :records_count
|
|
6
4
|
|
|
7
5
|
def initialize(resource, params, forest_user)
|
|
8
6
|
@resource = resource
|
|
9
7
|
@params = params
|
|
8
|
+
@user = forest_user
|
|
10
9
|
@count_needs_includes = false
|
|
11
10
|
@collection_name = ForestLiana.name_for(@resource)
|
|
12
11
|
@collection = get_collection(@collection_name)
|
|
13
12
|
@fields_to_serialize = get_fields_to_serialize
|
|
14
13
|
@field_names_requested = field_names_requested
|
|
15
|
-
get_segment
|
|
14
|
+
@segment = get_segment
|
|
16
15
|
compute_includes
|
|
17
|
-
@
|
|
18
|
-
@search_query_builder = SearchQueryBuilder.new(@params, @includes, @collection, forest_user)
|
|
16
|
+
@search_query_builder = SearchQueryBuilder.new(@params, @includes, @collection, @user)
|
|
19
17
|
|
|
20
18
|
prepare_query
|
|
21
19
|
end
|
|
22
20
|
|
|
23
21
|
def self.get_ids_from_request(params, user)
|
|
24
22
|
attributes = params.dig('data', 'attributes')
|
|
25
|
-
|
|
26
|
-
is_select_all_records_query = has_body_attributes && attributes[:all_records] == true
|
|
27
|
-
|
|
28
|
-
# NOTICE: If it is not a "select all records" query and it receives a list of ID, return list of ID.
|
|
29
|
-
return attributes[:ids] if (!is_select_all_records_query && attributes[:ids])
|
|
30
|
-
|
|
31
|
-
# NOTICE: If it is a "select all records" we have to perform query to build ID list.
|
|
32
|
-
ids = Array.new
|
|
33
|
-
|
|
34
|
-
# NOTICE: Merging all_records_subset_query into attributes preserves filters in HasManyGetter and ResourcesGetter.
|
|
35
|
-
attributes = attributes.merge(attributes[:all_records_subset_query].dup.to_unsafe_h)
|
|
36
|
-
|
|
37
|
-
# NOTICE: Initialize actual resources getter (could either a HasManyGetter or a ResourcesGetter).
|
|
38
|
-
is_related_data = attributes[:parent_collection_id] &&
|
|
39
|
-
attributes[:parent_collection_name] &&
|
|
40
|
-
attributes[:parent_association_name]
|
|
41
|
-
if is_related_data
|
|
42
|
-
parent_collection_name = attributes[:parent_collection_name]
|
|
43
|
-
parent_model = ForestLiana::SchemaUtils.find_model_from_collection_name(parent_collection_name)
|
|
44
|
-
model = parent_model.reflect_on_association(attributes[:parent_association_name].try(:to_sym))
|
|
45
|
-
resources_getter = ForestLiana::HasManyGetter.new(parent_model, model, attributes.merge({
|
|
46
|
-
collection: parent_collection_name,
|
|
47
|
-
id: attributes[:parent_collection_id],
|
|
48
|
-
association_name: attributes[:parent_association_name],
|
|
49
|
-
}), user)
|
|
50
|
-
else
|
|
51
|
-
collection_name = attributes[:collection_name]
|
|
52
|
-
model = ForestLiana::SchemaUtils.find_model_from_collection_name(collection_name)
|
|
53
|
-
resources_getter = ForestLiana::ResourcesGetter.new(model, attributes, user)
|
|
54
|
-
end
|
|
23
|
+
return attributes[:ids] if attributes&.fetch(:all_records, false) == false && attributes[:ids]
|
|
55
24
|
|
|
56
|
-
|
|
57
|
-
resources_getter
|
|
58
|
-
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# NOTICE: remove excluded IDs.
|
|
62
|
-
ids_excluded = (attributes[:all_records_ids_excluded]).map { |id_excluded| id_excluded.to_s }
|
|
63
|
-
return ids.select { |id| !ids_excluded.include? id.to_s } if (ids_excluded && ids_excluded.any?)
|
|
25
|
+
attributes = merge_subset_query(attributes)
|
|
26
|
+
resources_getter = initialize_resources_getter(attributes, user)
|
|
27
|
+
ids = fetch_ids(resources_getter)
|
|
64
28
|
|
|
65
|
-
|
|
29
|
+
filter_excluded_ids(ids, attributes[:all_records_ids_excluded])
|
|
66
30
|
end
|
|
67
31
|
|
|
68
32
|
def perform
|
|
@@ -70,9 +34,7 @@ module ForestLiana
|
|
|
70
34
|
end
|
|
71
35
|
|
|
72
36
|
def count
|
|
73
|
-
|
|
74
|
-
# filters on associations.
|
|
75
|
-
@records_count = @count_needs_includes ? optimize_record_loading(@resource, @records).count : @records.count
|
|
37
|
+
@records_count = @count_needs_includes ? optimized_count : @records.count
|
|
76
38
|
end
|
|
77
39
|
|
|
78
40
|
def query_for_batch
|
|
@@ -91,12 +53,12 @@ module ForestLiana
|
|
|
91
53
|
|
|
92
54
|
if @collection && @collection.search_fields
|
|
93
55
|
includes_for_smart_search = @collection.search_fields
|
|
94
|
-
|
|
95
|
-
|
|
56
|
+
.select { |field| field.include? '.' }
|
|
57
|
+
.map { |field| field.split('.').first.to_sym }
|
|
96
58
|
|
|
97
59
|
includes_has_many = SchemaUtils.many_associations(@resource)
|
|
98
|
-
|
|
99
|
-
|
|
60
|
+
.select { |association| SchemaUtils.model_included?(association.klass) }
|
|
61
|
+
.map(&:name)
|
|
100
62
|
|
|
101
63
|
includes_for_smart_search = includes_for_smart_search & includes_has_many
|
|
102
64
|
end
|
|
@@ -115,107 +77,126 @@ module ForestLiana
|
|
|
115
77
|
private
|
|
116
78
|
|
|
117
79
|
def get_fields_to_serialize
|
|
118
|
-
|
|
119
|
-
@params[:fields][@collection_name].split(',').map { |name| name.to_sym }
|
|
120
|
-
else
|
|
121
|
-
[]
|
|
122
|
-
end
|
|
80
|
+
@params.dig(:fields, @collection_name)&.split(',')&.map(&:to_sym) || []
|
|
123
81
|
end
|
|
124
82
|
|
|
125
83
|
def get_segment
|
|
126
|
-
if @params[:segment]
|
|
127
|
-
@segment = @collection.segments.find do |segment|
|
|
128
|
-
segment.name == @params[:segment]
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
@segment ||= nil
|
|
84
|
+
@collection.segments.find { |segment| segment.name == @params[:segment] } if @params[:segment]
|
|
132
85
|
end
|
|
133
86
|
|
|
134
87
|
def field_names_requested
|
|
135
88
|
return nil unless @params[:fields] && @params[:fields][@collection_name]
|
|
136
89
|
|
|
137
|
-
associations_for_query =
|
|
90
|
+
associations_for_query = extract_associations_from_filter
|
|
91
|
+
associations_for_query << @params[:sort].split('.').first.to_sym if @params[:sort]&.include?('.')
|
|
92
|
+
@fields_to_serialize | associations_for_query
|
|
93
|
+
end
|
|
138
94
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
end
|
|
95
|
+
def extract_associations_from_filter
|
|
96
|
+
associations = []
|
|
97
|
+
@params[:filter]&.each do |field, _|
|
|
98
|
+
if field.include?(':')
|
|
99
|
+
associations << field.split(':').first.to_sym
|
|
100
|
+
@count_needs_includes = true
|
|
146
101
|
end
|
|
147
102
|
end
|
|
148
|
-
|
|
149
103
|
@count_needs_includes = true if @params[:search]
|
|
150
104
|
|
|
151
|
-
|
|
152
|
-
|
|
105
|
+
associations
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def prepare_query
|
|
109
|
+
@records = get_resource
|
|
110
|
+
|
|
111
|
+
if @segment
|
|
112
|
+
@records = apply_segment(@records)
|
|
153
113
|
end
|
|
154
114
|
|
|
155
|
-
|
|
115
|
+
apply_live_query_segment if @params[:segmentQuery]
|
|
116
|
+
@records = search_query
|
|
156
117
|
end
|
|
157
118
|
|
|
158
|
-
def
|
|
159
|
-
|
|
119
|
+
def optimized_count
|
|
120
|
+
optimize_record_loading(@resource, @records).count
|
|
160
121
|
end
|
|
161
122
|
|
|
162
|
-
def
|
|
163
|
-
@
|
|
123
|
+
def apply_segment(records)
|
|
124
|
+
return records.send(@segment.scope) if @segment.scope
|
|
125
|
+
return records.where(@segment.where.call) if @segment.where
|
|
126
|
+
|
|
127
|
+
records
|
|
128
|
+
end
|
|
164
129
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
130
|
+
def apply_live_query_segment
|
|
131
|
+
LiveQueryChecker.new(@params[:segmentQuery], 'Live Query Segment').validate
|
|
132
|
+
|
|
133
|
+
begin
|
|
134
|
+
segment_query = @params[:segmentQuery].gsub(/\;\s*$/, '')
|
|
135
|
+
@records = @records.where(
|
|
136
|
+
"#{@resource.table_name}.#{@resource.primary_key} IN (SELECT id FROM (#{segment_query}) as ids)"
|
|
137
|
+
)
|
|
138
|
+
rescue => error
|
|
139
|
+
handle_live_query_error(error)
|
|
169
140
|
end
|
|
141
|
+
end
|
|
170
142
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
143
|
+
def handle_live_query_error(error)
|
|
144
|
+
error_message = "Live Query Segment: #{error.message}"
|
|
145
|
+
FOREST_REPORTER.report error
|
|
146
|
+
FOREST_LOGGER.error(error_message)
|
|
147
|
+
raise ForestLiana::Errors::LiveQueryError.new(error_message)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def self.merge_subset_query(attributes)
|
|
151
|
+
attributes.merge(attributes[:all_records_subset_query].dup.to_unsafe_h)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def self.initialize_resources_getter(attributes, user)
|
|
155
|
+
if related_data?(attributes)
|
|
156
|
+
HasManyGetter.new(*related_data_params(attributes, user))
|
|
157
|
+
else
|
|
158
|
+
ResourcesGetter.new(SchemaUtils.find_model_from_collection_name(attributes[:collection_name]), attributes, user)
|
|
186
159
|
end
|
|
160
|
+
end
|
|
187
161
|
|
|
188
|
-
|
|
162
|
+
def self.related_data?(attributes)
|
|
163
|
+
attributes[:parent_collection_id] && attributes[:parent_collection_name] && attributes[:parent_association_name]
|
|
189
164
|
end
|
|
190
165
|
|
|
191
|
-
def
|
|
192
|
-
|
|
166
|
+
def self.related_data_params(attributes, user)
|
|
167
|
+
parent_model = SchemaUtils.find_model_from_collection_name(attributes[:parent_collection_name])
|
|
168
|
+
model = parent_model.reflect_on_association(attributes[:parent_association_name].to_sym)
|
|
169
|
+
[parent_model, model, attributes.merge(collection: attributes[:parent_collection_name]), user]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def self.fetch_ids(resources_getter)
|
|
173
|
+
ids = []
|
|
174
|
+
resources_getter.query_for_batch.find_in_batches { |records| ids += records.map(&:id) }
|
|
175
|
+
|
|
176
|
+
ids
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def self.filter_excluded_ids(ids, ids_excluded)
|
|
180
|
+
ids_excluded ? ids.reject { |id| ids_excluded.map(&:to_s).include?(id.to_s) } : ids
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def search_query
|
|
184
|
+
@search_query_builder.perform(@records)
|
|
193
185
|
end
|
|
194
186
|
|
|
195
187
|
def offset
|
|
196
188
|
return 0 unless pagination?
|
|
197
189
|
|
|
198
|
-
number = @params
|
|
199
|
-
|
|
200
|
-
(number.to_i - 1) * limit
|
|
201
|
-
else
|
|
202
|
-
0
|
|
203
|
-
end
|
|
190
|
+
number = @params.dig(:page, :number)
|
|
191
|
+
number.to_i.positive? ? (number.to_i - 1) * limit : 0
|
|
204
192
|
end
|
|
205
193
|
|
|
206
194
|
def limit
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if @params[:page][:size]
|
|
210
|
-
@params[:page][:size].to_i
|
|
211
|
-
else
|
|
212
|
-
10
|
|
213
|
-
end
|
|
195
|
+
@params.dig(:page, :size)&.to_i || 10
|
|
214
196
|
end
|
|
215
197
|
|
|
216
198
|
def pagination?
|
|
217
|
-
@params[:page]
|
|
199
|
+
@params[:page]&.dig(:number)
|
|
218
200
|
end
|
|
219
|
-
|
|
220
201
|
end
|
|
221
202
|
end
|
data/lib/forest_liana/version.rb
CHANGED
|
@@ -421,7 +421,7 @@ module ForestLiana
|
|
|
421
421
|
context 'on belongs to field' do
|
|
422
422
|
context 'existing field' do
|
|
423
423
|
let(:field_name) { 'trees:age' }
|
|
424
|
-
it { expect(result).to eq "
|
|
424
|
+
it { expect(result).to eq "trees.\"age\""}
|
|
425
425
|
end
|
|
426
426
|
context 'not existing field' do
|
|
427
427
|
let(:field_name) { 'hero:age' }
|
|
@@ -248,6 +248,31 @@ module ForestLiana
|
|
|
248
248
|
end
|
|
249
249
|
end
|
|
250
250
|
|
|
251
|
+
context 'when fields is given' do
|
|
252
|
+
let(:resource) { Island }
|
|
253
|
+
let(:filters) { {
|
|
254
|
+
field: 'location:id',
|
|
255
|
+
operator: 'equal',
|
|
256
|
+
value: 1,
|
|
257
|
+
}.to_json }
|
|
258
|
+
|
|
259
|
+
it 'should get only the expected records' do
|
|
260
|
+
getter.perform
|
|
261
|
+
records = getter.records
|
|
262
|
+
count = getter.count
|
|
263
|
+
|
|
264
|
+
expect(records.count).to eq 1
|
|
265
|
+
expect(count).to eq 1
|
|
266
|
+
expect(records.map(&:id)).to eq [1]
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it 'should include associated table only once' do
|
|
270
|
+
sql_query = getter.perform.to_sql
|
|
271
|
+
location_includes_count = sql_query.scan('LEFT OUTER JOIN "locations"').count
|
|
272
|
+
expect(location_includes_count).to eq(1)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
251
276
|
describe 'when getting instance dependent associations' do
|
|
252
277
|
let(:resource) { Island }
|
|
253
278
|
let(:fields) { { 'Island' => 'id,eponymous_tree', 'eponymous_tree' => 'id,name'} }
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: forest_liana
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 9.3.
|
|
4
|
+
version: 9.3.15
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sandro Munda
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2024-
|
|
11
|
+
date: 2024-09-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|