activefacts-compositions 1.9.18 → 1.9.19
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/activefacts-compositions.gemspec +1 -1
- data/lib/activefacts/compositions/datavault.rb +2 -7
- data/lib/activefacts/compositions/staging.rb +10 -0
- data/lib/activefacts/compositions/version.rb +1 -1
- data/lib/activefacts/generator/doc/glossary.rb +0 -1
- data/lib/activefacts/generator/etl/unidex.rb +45 -22
- data/lib/activefacts/generator/rails/models.rb +27 -9
- data/lib/activefacts/generator/traits/expr.rb +1 -0
- data/lib/activefacts/generator/traits/sql/postgres.rb +71 -11
- metadata +7 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2dd043b17bcbdb5bf4f525fe24022ad95a32946e
|
4
|
+
data.tar.gz: 95fb5b1b17e005cc0bd90f6f9c527e6455557a87
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 528cbe042208d8dd290ef5cd5e11ae390261f25c04c4e5558ef111f7fc18aab8a82ea1421ee2147c71d3659c077c93af632ba7372fc337b2797f2a6c69846b30
|
7
|
+
data.tar.gz: fdf3d9610a0751f91d8b2b8fb022918aa81c31b99a73a359100622cef6f87d04338428253172921bde29e48906b25622915ea89d20fa416e0d156e8fd1695fff
|
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.require_paths = ["lib"]
|
21
21
|
|
22
22
|
spec.add_development_dependency "bundler", ">= 1.11"
|
23
|
-
spec.add_development_dependency "rake", "
|
23
|
+
spec.add_development_dependency "rake", "> 10"
|
24
24
|
spec.add_development_dependency "rspec", "~> 3.3"
|
25
25
|
|
26
26
|
spec.add_runtime_dependency "activesupport", ">= 4.2.7"
|
@@ -630,13 +630,8 @@ module ActiveFacts
|
|
630
630
|
if absorption.foreign_key
|
631
631
|
trace :datavault, "Setting new source composite for #{absorption.foreign_key.inspect}"
|
632
632
|
absorption.foreign_key.source_composite = link
|
633
|
-
|
634
|
-
|
635
|
-
else
|
636
|
-
p absorption.foreign_key
|
637
|
-
debugger
|
638
|
-
absorption.foreign_key.retract
|
639
|
-
end
|
633
|
+
debugger unless absorption.foreign_key.all_foreign_key_field.single
|
634
|
+
fk2_component = absorption.foreign_key.all_foreign_key_field.single.component
|
640
635
|
end
|
641
636
|
end
|
642
637
|
|
@@ -51,6 +51,16 @@ module ActiveFacts
|
|
51
51
|
@option_surrogates && composite.mapping.object_type != @loadbatch_entity_type
|
52
52
|
end
|
53
53
|
|
54
|
+
def inject_surrogates
|
55
|
+
assign_groups
|
56
|
+
super
|
57
|
+
end
|
58
|
+
|
59
|
+
def assign_groups
|
60
|
+
@composites.values.each{|composite| composite.composite_group = 'base' }
|
61
|
+
loadbatch_composite.composite_group = 'batch' if @option_audit == 'batch'
|
62
|
+
end
|
63
|
+
|
54
64
|
def generate
|
55
65
|
create_loadbatch if @option_audit == 'batch'
|
56
66
|
super
|
@@ -50,7 +50,7 @@ module ActiveFacts
|
|
50
50
|
|
51
51
|
def process_options options
|
52
52
|
@value_width = (options.delete('value_width') || 32).to_i
|
53
|
-
@phonetic_confidence = (options.delete('phonetic_confidence') ||
|
53
|
+
@phonetic_confidence = (options.delete('phonetic_confidence') || 40).to_i
|
54
54
|
|
55
55
|
super
|
56
56
|
end
|
@@ -236,32 +236,55 @@ module ActiveFacts
|
|
236
236
|
case sm
|
237
237
|
when 'none' # Do not index this value
|
238
238
|
nil
|
239
|
+
|
239
240
|
when 'simple' # Disregard white-space only
|
240
241
|
select(composite, truncate(col_expr, @value_width), 'simple', source_field, 1.0)
|
241
242
|
|
242
|
-
when 'alpha' # Strip white space and punctuation, just use alphabetic characters
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
when 'typo' # Use trigram similarity to detect typographic errors
|
250
|
-
# REVISIT: Implement this type properly
|
251
|
-
select(composite, trigram(as_alpha(col_expr)), 'typo', source_field, 0.9)
|
243
|
+
when 'alpha', # Strip white space and punctuation, just use alphabetic characters
|
244
|
+
'typo' # Use trigram similarity to detect typographic errors, over the same values
|
245
|
+
truncated = truncate(as_alpha(col_expr), @value_width)
|
246
|
+
select(
|
247
|
+
composite, truncated, sm, source_field,
|
248
|
+
"CASE WHEN #{truncated} = #{col_expr} THEN 1.0 ELSE 0.95 END" # Maybe exact match.
|
249
|
+
)
|
252
250
|
|
253
251
|
when 'phonetic' # Use phonetic matching as well as trigrams
|
254
|
-
|
255
|
-
|
256
|
-
|
252
|
+
search_expr(composite, intrinsic_type, col_expr, ['typo'], source_field) <<
|
253
|
+
select(composite, phonetics(col_expr), 'phonetic', source_field, @phonetic_confidence/100.0, true)
|
254
|
+
|
255
|
+
when 'words' # Break the text into words and match each word like alpha
|
256
|
+
truncated = truncate(unnest(as_words(col_expr)), @value_width)
|
257
|
+
select(composite, truncated, sm, source_field, 0.90, true)
|
257
258
|
|
258
259
|
when 'names' # Break the text into words and match each word like phonetic
|
259
|
-
|
260
|
+
truncated = truncate(unnest(as_words(col_expr, "''-")), @value_width) # N.B. ' is doubled for SQL
|
261
|
+
search_expr(composite, intrinsic_type, col_expr, ['words'], source_field) <<
|
262
|
+
phonetics(truncated).map do |phonetic|
|
263
|
+
select(composite, phonetic, 'names', source_field, @phonetic_confidence/100.0, true)
|
264
|
+
end
|
260
265
|
|
261
266
|
when 'text' # Index a large text field using significant words and phrases
|
262
267
|
nil # REVISIT: Implement this type
|
268
|
+
|
269
|
+
when 'number' # Cast to number and back to text to canonicalise the value;
|
270
|
+
# If the number doesn't match this regexp, we don't index it.
|
271
|
+
# This doesn't handle all valid Postgres numeric literals (e.g. 2.3e-4)
|
272
|
+
select(composite, col_expr, 'number', source_field, number_or_null(col_expr))
|
273
|
+
|
274
|
+
when 'phone' # Phone numbers; split, strip each to digits, take the last 8 of each
|
275
|
+
select(composite, phone_numbers(col_expr), 'phone', source_field, 1)
|
276
|
+
|
277
|
+
when 'email' # Use a regexp to find email addresses in this field
|
278
|
+
select(composite, truncate(email_addresses(col_expr), @value_width), 'email', source_field, 1)
|
279
|
+
|
280
|
+
when 'date' # Convert string to standard date format if it looks like a date, NULL otherwise
|
281
|
+
select(
|
282
|
+
composite, col_expr, 'date', source_field, 1,
|
283
|
+
%Q{CASE WHEN #{col_expr} ~ '^ *[0-9]+[.]?[0-9]*|[.][0-9]+) *$' THEN (#{col_expr}::numeric):text ELSE NULL END}
|
284
|
+
)
|
285
|
+
|
263
286
|
else
|
264
|
-
|
287
|
+
$stderrs.puts "Unknown search method #{sm}"
|
265
288
|
end
|
266
289
|
end
|
267
290
|
|
@@ -273,6 +296,7 @@ module ActiveFacts
|
|
273
296
|
MM::DataType::TYPE_Decimal,
|
274
297
|
MM::DataType::TYPE_Money
|
275
298
|
# Produce a right-justified value
|
299
|
+
# REVISIT: This is a dumb thing to do.
|
276
300
|
select(composite, lexical_decimal(col_expr, @value_width, value_type.scale), 'simple', source_field, 1)
|
277
301
|
|
278
302
|
when MM::DataType::TYPE_Date
|
@@ -301,7 +325,7 @@ module ActiveFacts
|
|
301
325
|
name.words.send(@column_case)*@column_joiner
|
302
326
|
end
|
303
327
|
|
304
|
-
def select composite, expression, processing, source_field, confidence = 1, where = []
|
328
|
+
def select composite, expression, processing, source_field, confidence = 1, distinct = false, where = []
|
305
329
|
# These fields are in order of index precedence, to co-locate
|
306
330
|
# comparable values regardless of source record type or column
|
307
331
|
where << 'Value IS NOT NULL' if expression.to_s =~ /\bNULL\b/
|
@@ -313,17 +337,16 @@ module ActiveFacts
|
|
313
337
|
source_table_name = stylise_column_name("SourceTable")
|
314
338
|
source_field_name = stylise_column_name("SourceField")
|
315
339
|
expression_text = expression.to_s
|
316
|
-
expression_text = "ARRAY[#{expression_text}]" unless expression.is_array
|
317
340
|
select = %Q{
|
318
|
-
SELECT
|
341
|
+
SELECT#{distinct ? ' DISTINCT' : ''}
|
342
|
+
'#{processing}' AS #{processing_name},
|
319
343
|
#{expression_text} AS #{value_name},
|
320
344
|
#{load_batch_id_name},
|
321
|
-
#{
|
345
|
+
#{confidence} AS #{confidence_name},
|
322
346
|
#{record_guid_name},
|
323
347
|
'#{safe_table_name(composite)}' AS #{source_table_name},
|
324
348
|
'#{source_field}' AS #{source_field_name}
|
325
|
-
FROM #{safe_table_name(composite)}
|
326
|
-
WHERE COALESCE(#{expression},'') <> ''}.
|
349
|
+
FROM #{safe_table_name(composite)}}.
|
327
350
|
unindent
|
328
351
|
|
329
352
|
if where.empty?
|
@@ -16,6 +16,7 @@ module ActiveFacts
|
|
16
16
|
HEADER = "# Auto-generated from CQL, edits will be lost"
|
17
17
|
def self.options
|
18
18
|
({
|
19
|
+
keep: ['Boolean', "Keep stale model files"],
|
19
20
|
output: [String, "Overwrite model files into this output directory"],
|
20
21
|
concern: [String, "Namespace for the concerns"],
|
21
22
|
validation: ['Boolean', "Disable generation of validations"],
|
@@ -25,6 +26,7 @@ module ActiveFacts
|
|
25
26
|
def initialize composition, options = {}
|
26
27
|
@composition = composition
|
27
28
|
@options = options
|
29
|
+
@option_keep = options.delete("keep")
|
28
30
|
@option_output = options.delete("output")
|
29
31
|
@option_concern = options.delete("concern")
|
30
32
|
@option_validations = options.include?('validations') ? options.delete("validations") : true
|
@@ -35,7 +37,7 @@ module ActiveFacts
|
|
35
37
|
end
|
36
38
|
|
37
39
|
def generate
|
38
|
-
list_extant_files if @option_output
|
40
|
+
list_extant_files if @option_output && !@option_keep
|
39
41
|
|
40
42
|
@ok = true
|
41
43
|
models =
|
@@ -46,7 +48,7 @@ module ActiveFacts
|
|
46
48
|
compact*"\n"
|
47
49
|
|
48
50
|
warn "\# #{@composition.name} generated with errors" unless @ok
|
49
|
-
delete_old_generated_files if @option_output
|
51
|
+
delete_old_generated_files if @option_output && !@option_keep
|
50
52
|
|
51
53
|
models
|
52
54
|
end
|
@@ -158,6 +160,7 @@ module ActiveFacts
|
|
158
160
|
composite.all_foreign_key_as_source_composite.
|
159
161
|
sort_by{ |fk| fk.all_foreign_key_field.map(&:component).flat_map(&:path).map(&:rank_key) }.
|
160
162
|
flat_map do |fk|
|
163
|
+
next nil if fk.all_foreign_key_field.size > 1
|
161
164
|
association_name = fk.rails.from_association_name
|
162
165
|
|
163
166
|
if association_name != fk.composite.rails.singular_name
|
@@ -171,9 +174,14 @@ module ActiveFacts
|
|
171
174
|
foreign_key = ''
|
172
175
|
end
|
173
176
|
|
177
|
+
single_fk_field = fk.all_foreign_key_field.single.component
|
178
|
+
if !single_fk_field.path_mandatory
|
179
|
+
optional = ", :optional => true"
|
180
|
+
end
|
181
|
+
|
174
182
|
[
|
175
183
|
fk.mapping ? " \# #{fk.mapping.comment}" : nil,
|
176
|
-
" belongs_to :#{association_name}#{class_name}#{foreign_key}",
|
184
|
+
" belongs_to :#{association_name}#{class_name}#{foreign_key}#{optional}",
|
177
185
|
fk.mapping ? '' : nil,
|
178
186
|
]
|
179
187
|
end.compact
|
@@ -184,9 +192,10 @@ module ActiveFacts
|
|
184
192
|
composite.all_foreign_key_as_target_composite.
|
185
193
|
sort_by{ |fk| fk.all_foreign_key_field.map(&:component).flat_map(&:path).map(&:rank_key) }.
|
186
194
|
flat_map do |fk|
|
195
|
+
next nil if fk.all_foreign_key_field.size > 1
|
187
196
|
|
188
197
|
if fk.all_foreign_key_field.size > 1
|
189
|
-
raise "Can't emit Rails associations for multi-part foreign key with #{fk.
|
198
|
+
raise "Can't emit Rails associations for multi-part foreign key with #{fk.all_foreign_key_field.inspect}. Did you mean to use --surrogate?"
|
190
199
|
end
|
191
200
|
|
192
201
|
association_type, association_name = *fk.rails.to_association
|
@@ -205,15 +214,22 @@ module ActiveFacts
|
|
205
214
|
fk.source_composite.primary_index.all_index_field.map(&:component).flat_map do |ic|
|
206
215
|
next nil if ic.is_a?(MM::Indicator) # or use rails.plural_name(ic.references[0].to_names) ?
|
207
216
|
onward_fks = ic.all_foreign_key_field.map(&:foreign_key)
|
208
|
-
next nil if onward_fks.size == 0 or onward_fks.detect{|
|
209
|
-
#
|
210
|
-
|
217
|
+
next nil if onward_fks.size == 0 or onward_fks.detect{|ofk| ofk.composite == composite} # Skip the back-reference
|
218
|
+
# This far association name needs to be augmented for its role name
|
219
|
+
# so the reverse associations still work for customised association names
|
220
|
+
source =
|
221
|
+
if composite.rails.singular_name != fk.rails.from_association_name
|
222
|
+
", :source => :#{fk.rails.from_association_name}"
|
223
|
+
else
|
224
|
+
''
|
225
|
+
end
|
226
|
+
" has_many :#{onward_fks[0].composite.rails.plural_name}, :through => :#{association_name}#{source}"
|
211
227
|
end.compact
|
212
228
|
else
|
213
229
|
[]
|
214
230
|
end +
|
215
231
|
[fk.mapping ? '' : nil]
|
216
|
-
end
|
232
|
+
end.compact
|
217
233
|
end
|
218
234
|
|
219
235
|
def column_constraints composite
|
@@ -223,7 +239,9 @@ module ActiveFacts
|
|
223
239
|
next unless component.path_mandatory && !component.is_a?(Metamodel::Indicator)
|
224
240
|
next if composite.primary_index != composite.natural_index && composite.primary_index.all_index_field.detect{|ixf| ixf.component == component}
|
225
241
|
next if component.is_a?(Metamodel::Mapping) && component.object_type.is_a?(Metamodel::ValueType) && component.is_auto_assigned
|
226
|
-
|
242
|
+
if component.all_foreign_key_field.size == 0
|
243
|
+
[ " validates :#{component.column_name.snakecase}, :presence => true" ]
|
244
|
+
end
|
227
245
|
end.compact
|
228
246
|
ccs.unshift("") unless ccs.empty?
|
229
247
|
ccs
|
@@ -19,6 +19,7 @@ module ActiveFacts
|
|
19
19
|
attr_reader :type_num # ActiveFacts::Metamodel::DataType number
|
20
20
|
attr_reader :value # String representation of the expression
|
21
21
|
attr_reader :is_mandatory # false if nullable
|
22
|
+
# This doesn't handle Postgres expressions, which can include a sub-table (e.g. via unnest)
|
22
23
|
attr_reader :is_array # the expression returns an array of the specified type
|
23
24
|
|
24
25
|
# Construct an expression that addresses a field from a Metamodel::Component
|
@@ -146,22 +146,24 @@ module ActiveFacts
|
|
146
146
|
"'|'::text || " +
|
147
147
|
expressions.map(&:to_s) * " || '|'::text || " +
|
148
148
|
" || '|'::text",
|
149
|
-
MM::DataType::TYPE_String
|
150
|
-
true
|
149
|
+
MM::DataType::TYPE_String
|
151
150
|
)
|
152
151
|
end
|
153
152
|
|
154
153
|
# Return an expression that yields a hash of the given expression
|
155
154
|
def hash expr, algo = 'sha1'
|
156
|
-
Expression.new("digest(#{expr}, '#{algo}')", MM::DataType::TYPE_Binary, expr.is_mandatory)
|
155
|
+
Expression.new("digest(#{expr}, '#{algo}')", MM::DataType::TYPE_Binary, expr.is_mandatory, expr.is_array)
|
157
156
|
end
|
158
157
|
|
159
158
|
def truncate expr, length
|
160
|
-
Expression.new("
|
159
|
+
Expression.new("left(#{expr}, #{length})", MM::DataType::TYPE_String, expr.is_mandatory, expr.is_array)
|
161
160
|
end
|
162
161
|
|
163
162
|
def trigram expr
|
164
|
-
|
163
|
+
# This is not a useful way to handle trigrams. Instead, create a trigram index
|
164
|
+
# over an ordinary text index value, and use a similarity search over that.
|
165
|
+
# Expression.new("show_trgm(#{expr})", MM::DataType::TYPE_String, expr.is_mandatory, true)
|
166
|
+
expr
|
165
167
|
end
|
166
168
|
|
167
169
|
# Produce a lexically-sortable decimal representation of the given numeric expression, to the overall specified length and scale
|
@@ -174,6 +176,39 @@ module ActiveFacts
|
|
174
176
|
)
|
175
177
|
end
|
176
178
|
|
179
|
+
def number_or_null expr
|
180
|
+
Expression.new(
|
181
|
+
%Q{CASE WHEN #{expr} ~ '^ *[-+]?([0-9]+[.]?[0-9]*|[.][0-9]+) *$' THEN #{expr}::numeric ELSE NULL END},
|
182
|
+
MM::DataType::TYPE_Real,
|
183
|
+
false
|
184
|
+
)
|
185
|
+
end
|
186
|
+
|
187
|
+
def split_on_separators expr, seps = ',\\\\|'
|
188
|
+
Expression.new(
|
189
|
+
%Q{regexp_split_to_table(#{expr}, E'#{seps}')},
|
190
|
+
MM::DataType::TYPE_String, true, true
|
191
|
+
)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Extract separated numbers, remove non-digits, take the last 8 (removing area codes etc)
|
195
|
+
def phone_numbers expr
|
196
|
+
Expression.new(
|
197
|
+
%Q{right(#{split_on_separators(%Q{regexp_replace(#{expr}, '[^0-9]+', '', 'g')})}, 8)},
|
198
|
+
MM::DataType::TYPE_String,
|
199
|
+
true
|
200
|
+
)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Extract separated numbers, remove non-digits, take the last 8 (removing area codes etc)
|
204
|
+
def email_addresses expr
|
205
|
+
Expression.new(
|
206
|
+
%Q{unnest(regexp_matches(#{expr}, E'[-_.[:alnum:]]+@[-_.[:alnum:]]+'))},
|
207
|
+
MM::DataType::TYPE_String,
|
208
|
+
true
|
209
|
+
)
|
210
|
+
end
|
211
|
+
|
177
212
|
def lexical_date expr
|
178
213
|
Expression.new("to_char(#{expr}, 'YYYY-MM-DD')", MM::DataType::TYPE_String, expr.is_mandatory)
|
179
214
|
end
|
@@ -190,13 +225,38 @@ module ActiveFacts
|
|
190
225
|
Expression.new("btrim(lower(regexp_replace(#{expr}, '[^[:alnum:]]+', ' ', 'g')))", MM::DataType::TYPE_String, expr.is_mandatory)
|
191
226
|
end
|
192
227
|
|
228
|
+
def as_words expr, extra_word_chars = ''
|
229
|
+
Expression.new(
|
230
|
+
"regexp_split_to_array(lower(#{expr}), E'[^[:alnum:]#{extra_word_chars}]+')",
|
231
|
+
MM::DataType::TYPE_String, expr.is_mandatory, true
|
232
|
+
)
|
233
|
+
end
|
234
|
+
|
235
|
+
def unnest expr
|
236
|
+
Expression.new("unnest(#{expr})", MM::DataType::TYPE_String, expr.is_mandatory, true)
|
237
|
+
end
|
238
|
+
|
193
239
|
def phonetics expr
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
240
|
+
if expr.is_array
|
241
|
+
[
|
242
|
+
Expression.new(
|
243
|
+
%Q{dmetaphone(#{expr})},
|
244
|
+
MM::DataType::TYPE_String,
|
245
|
+
expr.is_mandatory
|
246
|
+
),
|
247
|
+
Expression.new(
|
248
|
+
%Q{dmetaphone_alt(#{expr})},
|
249
|
+
MM::DataType::TYPE_String,
|
250
|
+
expr.is_mandatory
|
251
|
+
)
|
252
|
+
]
|
253
|
+
else
|
254
|
+
Expression.new(
|
255
|
+
%Q{unnest(ARRAY[dmetaphone(#{expr}), dmetaphone_alt(#{expr})])},
|
256
|
+
MM::DataType::TYPE_String,
|
257
|
+
expr.is_mandatory
|
258
|
+
)
|
259
|
+
end
|
200
260
|
end
|
201
261
|
|
202
262
|
# Reserved words cannot be used anywhere without quoting.
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activefacts-compositions
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.9.
|
4
|
+
version: 1.9.19
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Clifford Heath
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-02-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -28,16 +28,16 @@ dependencies:
|
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - ">"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '10
|
33
|
+
version: '10'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - ">"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '10
|
40
|
+
version: '10'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -257,7 +257,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
257
257
|
version: '0'
|
258
258
|
requirements: []
|
259
259
|
rubyforge_project:
|
260
|
-
rubygems_version: 2.
|
260
|
+
rubygems_version: 2.6.13
|
261
261
|
signing_key:
|
262
262
|
specification_version: 4
|
263
263
|
summary: Create and represent composite schemas, schema transforms and data transforms
|