activefacts-compositions 1.9.6 → 1.9.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|