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.
@@ -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
- @composition = composition
18
- @options = options
19
- @comments = @options.delete("comments")
23
+ @composition = composition
24
+ @options = options
25
+ @comments = @options.delete("comments")
20
26
  end
21
27
 
22
28
  def generate
23
- @composites_emitted = {}
29
+ @composites_emitted = {}
24
30
 
25
- retract_intrinsic_types
31
+ retract_intrinsic_types
26
32
 
27
- composites =
28
- @composition.
29
- all_composite.
30
- sort_by{|composite| composite.mapping.name}
33
+ composites =
34
+ @composition.
35
+ all_composite.
36
+ sort_by{|composite| composite.mapping.name}
31
37
 
32
- prelude(@composition) +
33
- generate_classes(composites) +
34
- finale
38
+ (prelude(@composition) +
39
+ generate_classes(composites) +
40
+ finale).gsub(/[ ][ ]*$/, '')
35
41
  end
36
42
 
37
43
  def generate_classes composites
38
- composites.
39
- map do |composite|
40
- generate_class(composite)
41
- end.
42
- compact.
43
- join("\n")
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
- @composition.all_composite.detect{|c| c.mapping.object_type == object_type }
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
- o = composite.mapping.object_type
53
- return true if o.name == "_ImplicitBooleanValueType"
54
- return false if o.supertype
55
- # A value type with no supertype must be emitted if it is the child in any absorption:
56
- return !composite.mapping.all_member.detect{|m| m.forward_absorption}
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
- @composition.
66
+ @composition.
61
67
  all_composite.
62
68
  sort_by{|composite| composite.mapping.name}.
63
- each do |composite|
64
- o = composite.mapping.object_type
65
- next unless o.is_a?(MM::ValueType)
66
- composite.retract and next if is_intrinsic_type(composite)
67
- end
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
- "REVISIT: override identified_by_roles\n"
81
+ "REVISIT: override identified_by_roles\n"
76
82
  end
77
83
 
78
84
  def value_type_declaration object_type
79
- "REVISIT: override value_type_declaration\n"
85
+ "REVISIT: override value_type_declaration\n"
80
86
  end
81
87
 
82
88
  def class_prelude(object_type, supertype)
83
- "REVISIT: override class_prelude\n"
89
+ "REVISIT: override class_prelude\n"
84
90
  end
85
91
 
86
92
  def class_finale(object_type)
87
- "REVISIT: override class_finale\n"
93
+ "REVISIT: override class_finale\n"
88
94
  end
89
95
 
90
96
  def generate_class composite, predefine_role_players = true
91
- return nil if @composites_emitted[composite]
92
-
93
- mapping = composite.mapping
94
- object_type = mapping.object_type
95
- is_entity_type = object_type.is_a?(MM::EntityType)
96
- forward_declarations = []
97
-
98
- # Emit supertypes before subtypes
99
- supertype_composites =
100
- object_type.all_supertype.map{|s| composite_for(s) }.compact
101
- forward_declarations +=
102
- supertype_composites.map{|c| generate_class(c, false)}.compact
103
-
104
- @composites_emitted[composite] = true
105
-
106
- # Select the members that will be declared as O-O roles:
107
- mapping.re_rank
108
- members = mapping.
109
- all_member.
110
- sort_by{|m| m.ordinal}.
111
- reject do |m|
112
- m.is_a?(MM::Absorption) and
113
- m.forward_absorption || m.child_role.fact_type.is_a?(MM::TypeInheritance)
114
- end
115
-
116
- if predefine_role_players
117
- # The idea was good, but we need to avoid triggering a forward reference problem.
118
- # We only do it when we're not dumping a supertype dependency.
119
- #
120
- # For those roles that derive from Mappings, produce class definitions to avoid forward references:
121
- forward_composites =
122
- members.
123
- select{ |m| m.is_a?(MM::Mapping) }.
124
- map{ |m| composite_for m.object_type }.
125
- compact.
126
- sort_by{|c| c.mapping.name}
127
- forward_declarations +=
128
- forward_composites.map{|c| generate_class(c)}.compact
129
- end
130
-
131
- forward_declarations = forward_declarations.map{|f| "#{f}\n"}*''
132
-
133
- primary_supertype =
134
- if is_entity_type
135
- object_type.identifying_supertype ||
136
- object_type.supertypes[0] # Hopefully there's only one!
137
- else
138
- object_type.supertype || object_type
139
- end
140
-
141
- type_declaration =
142
- if is_entity_type
143
- if primary_supertype and object_type.identification_is_inherited
144
- inherited_identification
145
- else
146
- identifying_roles =
147
- if object_type.fact_type && object_type.fact_type.is_unary
148
- # Objectified unary; find the absorption over the LinkFactType
149
- members.
150
- select{|m| m.is_a?(MM::Absorption) && m.child_role.base_role.fact_type.entity_type}.
151
- map{|m| m.child_role}
152
- else
153
- object_type.preferred_identifier.role_sequence.all_role_ref.map(&:role).
154
- map do |role|
155
- members.detect{|m| m.all_role.include?(role)}
156
- end
157
- end
158
- identified_by_roles identifying_roles
159
- end
160
- else
161
- value_type_declaration object_type
162
- end
163
-
164
- forward_declarations +
165
- class_prelude(object_type, primary_supertype) +
166
- type_declaration +
167
- members.
168
- map do |component|
169
- (@comments ? comment(component) + "\n" : '') +
170
- role_definition(component)
171
- end*'' +
172
- class_finale(object_type)
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
- "REVISIT: override role_definition\n"
182
+ "REVISIT: override role_definition\n"
177
183
  end
178
184
 
179
185
  def comment component
180
- if component.is_a?(MM::Absorption)
181
- component.parent_role.fact_type.reading_preferably_starting_with_role(component.parent_role).expand([], false)
182
- else
183
- component.name
184
- end
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