activefacts-compositions 1.9.6 → 1.9.8
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/.gitignore +1 -0
- data/Rakefile +33 -0
- data/activefacts-compositions.gemspec +3 -3
- data/bin/schema_compositor +142 -85
- data/lib/activefacts/compositions/binary.rb +19 -15
- data/lib/activefacts/compositions/compositor.rb +126 -125
- data/lib/activefacts/compositions/constraints.rb +74 -54
- data/lib/activefacts/compositions/datavault.rb +545 -0
- data/lib/activefacts/compositions/names.rb +58 -58
- data/lib/activefacts/compositions/relational.rb +801 -692
- data/lib/activefacts/compositions/traits/rails.rb +180 -0
- data/lib/activefacts/compositions/version.rb +1 -1
- data/lib/activefacts/generator/doc/css/ldm.css +45 -0
- data/lib/activefacts/generator/doc/cwm.rb +764 -0
- data/lib/activefacts/generator/doc/glossary.rb +473 -0
- data/lib/activefacts/generator/doc/graphviz.rb +134 -0
- data/lib/activefacts/generator/doc/ldm.rb +698 -0
- data/lib/activefacts/generator/oo.rb +130 -124
- data/lib/activefacts/generator/rails/models.rb +237 -0
- data/lib/activefacts/generator/rails/schema.rb +273 -0
- data/lib/activefacts/generator/ruby.rb +75 -67
- data/lib/activefacts/generator/sql.rb +333 -351
- data/lib/activefacts/generator/sql/server.rb +100 -39
- data/lib/activefacts/generator/summary.rb +67 -59
- data/lib/activefacts/generator/validate.rb +19 -134
- metadata +18 -15
@@ -13,178 +13,184 @@ module ActiveFacts
|
|
13
13
|
module Generators
|
14
14
|
# Options are comma or space separated:
|
15
15
|
class ObjectOriented
|
16
|
+
def self.options
|
17
|
+
{
|
18
|
+
comments: ['Boolean', "Preceed each role definition with a comment that describes it"]
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
16
22
|
def initialize composition, options = {}
|
17
|
-
|
18
|
-
|
19
|
-
|
23
|
+
@composition = composition
|
24
|
+
@options = options
|
25
|
+
@comments = @options.delete("comments")
|
20
26
|
end
|
21
27
|
|
22
28
|
def generate
|
23
|
-
|
29
|
+
@composites_emitted = {}
|
24
30
|
|
25
|
-
|
31
|
+
retract_intrinsic_types
|
26
32
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
33
|
+
composites =
|
34
|
+
@composition.
|
35
|
+
all_composite.
|
36
|
+
sort_by{|composite| composite.mapping.name}
|
31
37
|
|
32
|
-
|
33
|
-
|
34
|
-
|
38
|
+
(prelude(@composition) +
|
39
|
+
generate_classes(composites) +
|
40
|
+
finale).gsub(/[ ][ ]*$/, '')
|
35
41
|
end
|
36
42
|
|
37
43
|
def generate_classes composites
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
+
composites.
|
45
|
+
map do |composite|
|
46
|
+
generate_class(composite)
|
47
|
+
end.
|
48
|
+
compact.
|
49
|
+
join("\n")
|
44
50
|
end
|
45
51
|
|
46
52
|
def composite_for object_type
|
47
|
-
|
53
|
+
@composition.all_composite.detect{|c| c.mapping.object_type == object_type }
|
48
54
|
end
|
49
55
|
|
50
56
|
# We don't need Composites for object types that are built-in to the Ruby API.
|
51
57
|
def is_intrinsic_type composite
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
58
|
+
o = composite.mapping.object_type
|
59
|
+
return true if o.name == "_ImplicitBooleanValueType"
|
60
|
+
return false if o.supertype
|
61
|
+
# A value type with no supertype must be emitted if it is the child in any absorption:
|
62
|
+
return !composite.mapping.all_member.detect{|m| m.forward_absorption}
|
57
63
|
end
|
58
64
|
|
59
65
|
def retract_intrinsic_types
|
60
|
-
|
66
|
+
@composition.
|
61
67
|
all_composite.
|
62
68
|
sort_by{|composite| composite.mapping.name}.
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
69
|
+
each do |composite|
|
70
|
+
o = composite.mapping.object_type
|
71
|
+
next unless o.is_a?(MM::ValueType)
|
72
|
+
composite.retract and next if is_intrinsic_type(composite)
|
73
|
+
end
|
68
74
|
end
|
69
75
|
|
70
76
|
def inherited_identification
|
71
|
-
|
77
|
+
''
|
72
78
|
end
|
73
79
|
|
74
80
|
def identified_by_roles identifying_roles
|
75
|
-
|
81
|
+
"REVISIT: override identified_by_roles\n"
|
76
82
|
end
|
77
83
|
|
78
84
|
def value_type_declaration object_type
|
79
|
-
|
85
|
+
"REVISIT: override value_type_declaration\n"
|
80
86
|
end
|
81
87
|
|
82
88
|
def class_prelude(object_type, supertype)
|
83
|
-
|
89
|
+
"REVISIT: override class_prelude\n"
|
84
90
|
end
|
85
91
|
|
86
92
|
def class_finale(object_type)
|
87
|
-
|
93
|
+
"REVISIT: override class_finale\n"
|
88
94
|
end
|
89
95
|
|
90
96
|
def generate_class composite, predefine_role_players = true
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
97
|
+
return nil if @composites_emitted[composite]
|
98
|
+
|
99
|
+
mapping = composite.mapping
|
100
|
+
object_type = mapping.object_type
|
101
|
+
is_entity_type = object_type.is_a?(MM::EntityType)
|
102
|
+
forward_declarations = []
|
103
|
+
|
104
|
+
# Emit supertypes before subtypes
|
105
|
+
supertype_composites =
|
106
|
+
object_type.all_supertype.map{|s| composite_for(s) }.compact
|
107
|
+
forward_declarations +=
|
108
|
+
supertype_composites.map{|c| generate_class(c, false)}.compact
|
109
|
+
|
110
|
+
@composites_emitted[composite] = true
|
111
|
+
|
112
|
+
# Select the members that will be declared as O-O roles:
|
113
|
+
mapping.re_rank
|
114
|
+
members = mapping.
|
115
|
+
all_member.
|
116
|
+
sort_by{|m| m.ordinal}.
|
117
|
+
reject do |m|
|
118
|
+
m.is_a?(MM::Absorption) and
|
119
|
+
m.forward_absorption || m.child_role.fact_type.is_a?(MM::TypeInheritance)
|
120
|
+
end
|
121
|
+
|
122
|
+
if predefine_role_players
|
123
|
+
# The idea was good, but we need to avoid triggering a forward reference problem.
|
124
|
+
# We only do it when we're not dumping a supertype dependency.
|
125
|
+
#
|
126
|
+
# For those roles that derive from Mappings, produce class definitions to avoid forward references:
|
127
|
+
forward_composites =
|
128
|
+
members.
|
129
|
+
select{ |m| m.is_a?(MM::Mapping) }.
|
130
|
+
map{ |m| composite_for m.object_type }.
|
131
|
+
compact.
|
132
|
+
sort_by{|c| c.mapping.name}
|
133
|
+
forward_declarations +=
|
134
|
+
forward_composites.map{|c| generate_class(c)}.compact
|
135
|
+
end
|
136
|
+
|
137
|
+
forward_declarations = forward_declarations.map{|f| "#{f}\n"}*''
|
138
|
+
|
139
|
+
primary_supertype =
|
140
|
+
if is_entity_type
|
141
|
+
object_type.identifying_supertype ||
|
142
|
+
object_type.supertypes[0] # Hopefully there's only one!
|
143
|
+
else
|
144
|
+
object_type.supertype || object_type
|
145
|
+
end
|
146
|
+
|
147
|
+
type_declaration =
|
148
|
+
if is_entity_type
|
149
|
+
if primary_supertype and object_type.identification_is_inherited
|
150
|
+
inherited_identification
|
151
|
+
else
|
152
|
+
identifying_roles =
|
153
|
+
if object_type.fact_type && object_type.fact_type.is_unary
|
154
|
+
# Objectified unary; find the absorption over the LinkFactType
|
155
|
+
members.
|
156
|
+
select{|m| m.is_a?(MM::Absorption) && m.child_role.base_role.fact_type.entity_type}.
|
157
|
+
map{|m| m.child_role}
|
158
|
+
else
|
159
|
+
object_type.preferred_identifier.role_sequence.all_role_ref.map(&:role).
|
160
|
+
map do |role|
|
161
|
+
members.detect{|m| m.all_role.include?(role)}
|
162
|
+
end
|
163
|
+
end
|
164
|
+
identified_by_roles identifying_roles
|
165
|
+
end
|
166
|
+
else
|
167
|
+
value_type_declaration object_type
|
168
|
+
end
|
169
|
+
|
170
|
+
forward_declarations +
|
171
|
+
class_prelude(object_type, primary_supertype) +
|
172
|
+
type_declaration +
|
173
|
+
members.
|
174
|
+
map do |component|
|
175
|
+
(@comments ? comment(component) + "\n" : '') +
|
176
|
+
role_definition(component)
|
177
|
+
end*'' +
|
178
|
+
class_finale(object_type)
|
173
179
|
end
|
174
180
|
|
175
181
|
def role_definition component
|
176
|
-
|
182
|
+
"REVISIT: override role_definition\n"
|
177
183
|
end
|
178
184
|
|
179
185
|
def comment component
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
186
|
+
if component.is_a?(MM::Absorption)
|
187
|
+
component.parent_role.fact_type.reading_preferably_starting_with_role(component.parent_role).expand([], false)
|
188
|
+
else
|
189
|
+
component.name
|
190
|
+
end
|
185
191
|
end
|
186
192
|
|
187
|
-
MM = ActiveFacts::Metamodel
|
193
|
+
MM = ActiveFacts::Metamodel unless const_defined?(:MM)
|
188
194
|
end
|
189
195
|
end
|
190
196
|
end
|
@@ -0,0 +1,237 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Rails Models Generator
|
3
|
+
#
|
4
|
+
# Copyright (c) 2009-2016 Clifford Heath. Read the LICENSE file.
|
5
|
+
#
|
6
|
+
require 'digest/sha1'
|
7
|
+
require 'activefacts/metamodel'
|
8
|
+
require 'activefacts/registry'
|
9
|
+
require 'activefacts/compositions'
|
10
|
+
require 'activefacts/generator'
|
11
|
+
require 'activefacts/compositions/traits/rails'
|
12
|
+
|
13
|
+
module ActiveFacts
|
14
|
+
module Generators
|
15
|
+
module Rails
|
16
|
+
class Models
|
17
|
+
HEADER = "# Auto-generated from CQL, edits will be lost"
|
18
|
+
def self.options
|
19
|
+
({
|
20
|
+
output: [String, "Overwrite model files into this output directory"],
|
21
|
+
concern: [String, "Namespace for the concerns"],
|
22
|
+
validation: ['Boolean', "Disable generation of validations"],
|
23
|
+
})
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize composition, options = {}
|
27
|
+
@composition = composition
|
28
|
+
@options = options
|
29
|
+
@option_output = options.delete("output")
|
30
|
+
@option_concern = options.delete("concern")
|
31
|
+
@option_validations = options.include?('validations') ? options.delete("validations") : true
|
32
|
+
end
|
33
|
+
|
34
|
+
def warn *a
|
35
|
+
$stderr.puts *a
|
36
|
+
end
|
37
|
+
|
38
|
+
def generate
|
39
|
+
list_extant_files if @option_output
|
40
|
+
|
41
|
+
@ok = true
|
42
|
+
models =
|
43
|
+
@composition.
|
44
|
+
all_composite.
|
45
|
+
sort_by{|composite| composite.mapping.name}.
|
46
|
+
map{|composite| generate_composite composite}.
|
47
|
+
compact*"\n"
|
48
|
+
|
49
|
+
warn "\# #{@composition.name} generated with errors" unless @ok
|
50
|
+
delete_old_generated_files if @option_output
|
51
|
+
|
52
|
+
models
|
53
|
+
end
|
54
|
+
|
55
|
+
def list_extant_files
|
56
|
+
@preexisting_files = Dir[@option_output+'/*.rb']
|
57
|
+
end
|
58
|
+
|
59
|
+
def delete_old_generated_files
|
60
|
+
remaining = []
|
61
|
+
cleaned = 0
|
62
|
+
@preexisting_files.each do |pathname|
|
63
|
+
if generated_file_exists(pathname) == true
|
64
|
+
File.unlink(pathname)
|
65
|
+
cleaned += 1
|
66
|
+
else
|
67
|
+
remaining << pathname
|
68
|
+
end
|
69
|
+
end
|
70
|
+
$stderr.puts "Cleaned up #{cleaned} old generated files" if @preexisting_files.size > 0
|
71
|
+
$stderr.puts "Remaining non-generated files:\n\t#{remaining*"\n\t"}" if remaining.size > 0
|
72
|
+
end
|
73
|
+
|
74
|
+
def generated_file_exists pathname
|
75
|
+
File.open(pathname, 'r') do |existing|
|
76
|
+
first_lines = existing.read(1024) # Make it possible to pass over a magic charset comment
|
77
|
+
if first_lines.length == 0 or first_lines =~ %r{^#{HEADER}}
|
78
|
+
return true
|
79
|
+
end
|
80
|
+
end
|
81
|
+
return false # File exists, but is not generated
|
82
|
+
rescue Errno::ENOENT
|
83
|
+
return nil # File does not exist
|
84
|
+
end
|
85
|
+
|
86
|
+
def create_if_ok filename
|
87
|
+
# Create a file in the output directory, being careful not to overwrite carelessly
|
88
|
+
out = $stdout
|
89
|
+
if @option_output
|
90
|
+
pathname = (@option_output+'/'+filename).gsub(%r{//+}, '/')
|
91
|
+
@preexisting_files.reject!{|f| f == pathname } # Don't clean up this file
|
92
|
+
if generated_file_exists(pathname) == false
|
93
|
+
warn "not overwriting non-generated file #{pathname}"
|
94
|
+
@ok = false
|
95
|
+
return nil
|
96
|
+
end
|
97
|
+
out = File.open(pathname, 'w')
|
98
|
+
end
|
99
|
+
out
|
100
|
+
end
|
101
|
+
|
102
|
+
def generate_composite composite
|
103
|
+
model =
|
104
|
+
(@option_concern ? "module #{@option_concern}\n" : '') +
|
105
|
+
model_body(composite).gsub(/^./, @option_concern ? ' \0' : '\0') +
|
106
|
+
(@option_concern ? "end\n" : '')
|
107
|
+
|
108
|
+
return model unless @option_output
|
109
|
+
|
110
|
+
filename = composite.rails.singular_name+'.rb'
|
111
|
+
out = create_if_ok(filename)
|
112
|
+
return nil unless out
|
113
|
+
out.puts "#{HEADER}\n\n"+model
|
114
|
+
ensure
|
115
|
+
out.close if out
|
116
|
+
nil
|
117
|
+
end
|
118
|
+
|
119
|
+
def model_header composite
|
120
|
+
[
|
121
|
+
"module #{composite.rails.class_name}",
|
122
|
+
" extend ActiveSupport::Concern",
|
123
|
+
" included do"
|
124
|
+
]
|
125
|
+
end
|
126
|
+
|
127
|
+
def model_key composite
|
128
|
+
identifier_columns = composite.primary_index.all_index_field
|
129
|
+
if identifier_columns.size == 1
|
130
|
+
[
|
131
|
+
" self.primary_key = '#{identifier_columns.single.component.column_name.snakecase}'",
|
132
|
+
'' # Leave a blank line
|
133
|
+
]
|
134
|
+
else
|
135
|
+
[]
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def model_body composite
|
140
|
+
(
|
141
|
+
model_header(composite) +
|
142
|
+
model_key(composite) +
|
143
|
+
to_associations(composite) +
|
144
|
+
from_associations(composite) +
|
145
|
+
column_constraints(composite) +
|
146
|
+
[
|
147
|
+
" end",
|
148
|
+
"end"
|
149
|
+
]
|
150
|
+
).
|
151
|
+
compact.
|
152
|
+
map{|l| l+"\n"}.
|
153
|
+
join('').
|
154
|
+
gsub(/\n\n\n+/,"\n\n") # At most double-spaced
|
155
|
+
end
|
156
|
+
|
157
|
+
def to_associations composite
|
158
|
+
# Each outbound foreign key generates a belongs_to association:
|
159
|
+
composite.all_foreign_key_as_source_composite.
|
160
|
+
sort_by{ |fk| fk.all_foreign_key_field.map(&:component).flat_map(&:path).map(&:rank_key) }.
|
161
|
+
flat_map do |fk|
|
162
|
+
association_name = fk.rails.from_association_name
|
163
|
+
|
164
|
+
if association_name != fk.composite.rails.singular_name
|
165
|
+
# A different class_name is implied, emit an explicit one:
|
166
|
+
class_name = ", :class_name => '#{fk.composite.rails.class_name}'"
|
167
|
+
end
|
168
|
+
|
169
|
+
foreign_key = ", :foreign_key => :#{fk.all_foreign_key_field.single.component.column_name.snakecase}"
|
170
|
+
if foreign_key == fk.composite.rails.singular_name+'_id'
|
171
|
+
# See lib/active_record/reflection.rb, method #derive_foreign_key
|
172
|
+
foreign_key = ''
|
173
|
+
end
|
174
|
+
|
175
|
+
[
|
176
|
+
fk.absorption ? " \# #{fk.absorption.comment}" : nil,
|
177
|
+
" belongs_to :#{association_name}#{class_name}#{foreign_key}",
|
178
|
+
fk.absorption ? '' : nil,
|
179
|
+
]
|
180
|
+
end.compact
|
181
|
+
end
|
182
|
+
|
183
|
+
def from_associations composite
|
184
|
+
# has_one/has_many Associations
|
185
|
+
composite.all_foreign_key_as_target_composite.
|
186
|
+
sort_by{ |fk| fk.all_foreign_key_field.map(&:component).flat_map(&:path).map(&:rank_key) }.
|
187
|
+
flat_map do |fk|
|
188
|
+
|
189
|
+
if fk.all_foreign_key_field.size > 1
|
190
|
+
raise "Can't emit Rails associations for multi-part foreign key with #{fk.references.inspect}. Did you mean to use --surrogate?"
|
191
|
+
end
|
192
|
+
|
193
|
+
association_type, association_name = *fk.rails.to_association
|
194
|
+
|
195
|
+
[
|
196
|
+
# REVISIT: We want the reverse-order comment here really
|
197
|
+
fk.absorption ? " \# #{fk.absorption.comment}" : nil,
|
198
|
+
%Q{ #{association_type} :#{association_name}} +
|
199
|
+
%Q{, :class_name => '#{fk.source_composite.rails.class_name}'} +
|
200
|
+
%Q{, :foreign_key => :#{fk.all_foreign_key_field.single.component.column_name.snakecase}} +
|
201
|
+
%Q{, :dependent => :destroy}
|
202
|
+
] +
|
203
|
+
# If fk.absorption.source_composite is a join table, we can emit a has_many :through for each other key
|
204
|
+
# REVISIT: We could alternately do this for all belongs_to's in the source composite
|
205
|
+
if fk.source_composite.primary_index.all_index_field.size > 1
|
206
|
+
fk.source_composite.primary_index.all_index_field.map(&:component).flat_map do |ic|
|
207
|
+
next nil if ic.is_a?(MM::Indicator) # or use rails.plural_name(ic.references[0].to_names) ?
|
208
|
+
onward_fks = ic.all_foreign_key_field.map(&:foreign_key)
|
209
|
+
next nil if onward_fks.size == 0 or onward_fks.detect{|fk| fk.composite == composite} # Skip the back-reference
|
210
|
+
# REVISIT: This far association name needs to be augmented for its role name
|
211
|
+
" has_many :#{onward_fks[0].composite.rails.plural_name}, :through => :#{association_name}"
|
212
|
+
end.compact
|
213
|
+
else
|
214
|
+
[]
|
215
|
+
end +
|
216
|
+
[fk.absorption ? '' : nil]
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def column_constraints composite
|
221
|
+
return [] unless @option_validations
|
222
|
+
ccs =
|
223
|
+
composite.mapping.all_leaf.flat_map do |component|
|
224
|
+
next unless component.path_mandatory
|
225
|
+
next if component.is_a?(Metamodel::Mapping) && component.object_type.is_a?(Metamodel::ValueType) && component.is_auto_assigned
|
226
|
+
[ " validates :#{component.column_name.snakecase}, :presence => true" ]
|
227
|
+
end.compact
|
228
|
+
ccs.unshift("") unless ccs.empty?
|
229
|
+
ccs
|
230
|
+
end
|
231
|
+
|
232
|
+
MM = ActiveFacts::Metamodel unless const_defined?(:MM)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
publish_generator Rails::Models
|
236
|
+
end
|
237
|
+
end
|