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.
@@ -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