activefacts-generators 1.7.1
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 +7 -0
- data/.gitignore +9 -0
- data/.rspec +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +30 -0
- data/Rakefile +6 -0
- data/activefacts-generators.gemspec +26 -0
- data/lib/activefacts/dependency_analyser.rb +182 -0
- data/lib/activefacts/generators/absorption.rb +71 -0
- data/lib/activefacts/generators/composition.rb +119 -0
- data/lib/activefacts/generators/cql.rb +715 -0
- data/lib/activefacts/generators/diagrams/json.rb +340 -0
- data/lib/activefacts/generators/help.rb +64 -0
- data/lib/activefacts/generators/helpers/inject.rb +16 -0
- data/lib/activefacts/generators/helpers/oo.rb +162 -0
- data/lib/activefacts/generators/helpers/ordered.rb +605 -0
- data/lib/activefacts/generators/helpers/rails.rb +57 -0
- data/lib/activefacts/generators/html/glossary.rb +462 -0
- data/lib/activefacts/generators/metadata/json.rb +204 -0
- data/lib/activefacts/generators/null.rb +32 -0
- data/lib/activefacts/generators/rails/models.rb +247 -0
- data/lib/activefacts/generators/rails/schema.rb +217 -0
- data/lib/activefacts/generators/ruby.rb +134 -0
- data/lib/activefacts/generators/sql/mysql.rb +281 -0
- data/lib/activefacts/generators/sql/server.rb +274 -0
- data/lib/activefacts/generators/stats.rb +70 -0
- data/lib/activefacts/generators/text.rb +29 -0
- data/lib/activefacts/generators/traits/datavault.rb +241 -0
- data/lib/activefacts/generators/traits/oo.rb +73 -0
- data/lib/activefacts/generators/traits/ordered.rb +33 -0
- data/lib/activefacts/generators/traits/ruby.rb +210 -0
- data/lib/activefacts/generators/transform/datavault.rb +303 -0
- data/lib/activefacts/generators/transform/surrogate.rb +215 -0
- data/lib/activefacts/registry.rb +11 -0
- metadata +176 -0
@@ -0,0 +1,204 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Generators.
|
3
|
+
#
|
4
|
+
# Generate metadata in JSON
|
5
|
+
#
|
6
|
+
# Copyright (c) 2013 Clifford Heath. Read the LICENSE file.
|
7
|
+
#
|
8
|
+
require 'activefacts/api'
|
9
|
+
require 'activefacts/rmap'
|
10
|
+
require 'json'
|
11
|
+
require 'activefacts/registry'
|
12
|
+
|
13
|
+
module ActiveFacts
|
14
|
+
module Generators #:nodoc:
|
15
|
+
class Metadata #:nodoc:
|
16
|
+
class JSON #:nodoc:
|
17
|
+
def initialize(vocabulary, *options)
|
18
|
+
@vocabulary = vocabulary
|
19
|
+
@vocabulary = @vocabulary.Vocabulary.values[0] if ActiveFacts::API::Constellation === @vocabulary
|
20
|
+
options.each{|option| set_option(option) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def set_option(option)
|
24
|
+
end
|
25
|
+
|
26
|
+
def generate(out = $>)
|
27
|
+
@metadata = {"types" => {}}
|
28
|
+
|
29
|
+
object_types_dump
|
30
|
+
|
31
|
+
out.puts ::JSON.pretty_generate(@metadata)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Store the metadata for all types into the types section of the @metadata hash
|
35
|
+
def object_types_dump
|
36
|
+
types = @metadata["types"]
|
37
|
+
|
38
|
+
# Compute the relational mapping if not already done:
|
39
|
+
@tables ||= @vocabulary.tables
|
40
|
+
|
41
|
+
@vocabulary.all_object_type.
|
42
|
+
sort_by{|c| c.name}.each do |o|
|
43
|
+
object_type = o.as_json_metadata
|
44
|
+
|
45
|
+
types[o.name] = object_type if object_type
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
module Metamodel
|
54
|
+
class ObjectType
|
55
|
+
def as_json_metadata
|
56
|
+
# Using proc avoids polluting the object's namespace with these little methods
|
57
|
+
verbalise_role = proc do |role, plural|
|
58
|
+
fc = Array.new(role.fact_type.all_role.size, plural ? 'some' : 'one')
|
59
|
+
reading = role.fact_type.reading_preferably_starting_with_role(role)
|
60
|
+
fc.reverse! unless reading.role_sequence.all_role_ref.to_a[0].role == role
|
61
|
+
fc[reading.role_sequence.all_role_ref_in_order.to_a.index{|rr| rr.role == role}] = 'this'
|
62
|
+
reading.expand(fc, false)
|
63
|
+
end
|
64
|
+
|
65
|
+
titlize_words = proc do |phrase|
|
66
|
+
phrase && phrase.split(/\s+/).map{|w| w.sub(/^[a-z]/) {|i| i.upcase}}*' '
|
67
|
+
end
|
68
|
+
|
69
|
+
role_name = proc do |role|
|
70
|
+
if role.role_name
|
71
|
+
role.role_name
|
72
|
+
else
|
73
|
+
ref = role.preferred_reference
|
74
|
+
[ titlize_words.call(ref.leading_adjective), role.object_type.name, titlize_words.call(ref.trailing_adjective)].compact*' '
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
return nil if name == '_ImplicitBooleanValueType'
|
79
|
+
|
80
|
+
object_type = {}
|
81
|
+
object_type["is_main"] = is_table
|
82
|
+
object_type["id"] = concept.guid.to_s
|
83
|
+
functions = object_type["functions"] = []
|
84
|
+
|
85
|
+
if is_a?(ActiveFacts::Metamodel::EntityType)
|
86
|
+
|
87
|
+
# Don't emit a binary objectified fact type that plays no roles (except in implicit fact types:
|
88
|
+
if fact_type and fact_type.all_role.size == 2 and all_role.size == 2
|
89
|
+
return nil
|
90
|
+
end
|
91
|
+
|
92
|
+
# Export the supertypes
|
93
|
+
(supertypes_transitive-[self]).sort_by{|t| t.name}.each do |supertype|
|
94
|
+
functions <<
|
95
|
+
{
|
96
|
+
"title" => "as #{supertype.name}",
|
97
|
+
"type" => "#{supertype.name}"
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
# Export the subtypes
|
102
|
+
(subtypes_transitive-[self]).sort_by{|t| t.name}.each do |subtype|
|
103
|
+
functions <<
|
104
|
+
{
|
105
|
+
"title" => "as #{subtype.name}",
|
106
|
+
"type" => "#{subtype.name}"
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
# If an objectified fact type, export the fact type's roles
|
111
|
+
if fact_type
|
112
|
+
fact_type.preferred_reading.role_sequence.all_role_ref_in_order.map(&:role).each do |role|
|
113
|
+
functions <<
|
114
|
+
{
|
115
|
+
"title" => "involving #{role_name.call(role)}",
|
116
|
+
"type" => "#{role.object_type.name}",
|
117
|
+
"where" => verbalise_role.call(role, true) # REVISIT: Need plural setting here!
|
118
|
+
}
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Now export the ordinary roles. Get a sorted list first:
|
124
|
+
roles = all_role.reject do |role|
|
125
|
+
# supertype and subtype roles get handled separately
|
126
|
+
role.fact_type.is_a?(ActiveFacts::Metamodel::TypeInheritance) ||
|
127
|
+
role.fact_type.is_a?(ActiveFacts::Metamodel::LinkFactType)
|
128
|
+
end.sort_by do |role|
|
129
|
+
# Where this object type plays two roles in the same fact type,
|
130
|
+
# we order them by the position of that role in the preferred reading:
|
131
|
+
[role.fact_type.default_reading, role.fact_type.preferred_reading.role_sequence.all_role_ref_in_order.map(&:role).index(role)]
|
132
|
+
end
|
133
|
+
|
134
|
+
# For binary fact types, collect the count of the times the unadorned counterpart role name occurs, so we can adorn it
|
135
|
+
plural_counterpart_counts = roles.inject(Hash.new{0}) do |h, role|
|
136
|
+
next h unless role.fact_type.all_role.size == 2
|
137
|
+
uc = role.all_role_ref.detect do |rr|
|
138
|
+
rs = rr.role_sequence
|
139
|
+
next false if rs.all_role_ref.size != 1 # Looking for a UC over just this one role
|
140
|
+
rs.all_presence_constraint.detect do |pc|
|
141
|
+
next false unless pc.max_frequency == 1 # It's a uniqueness constraint
|
142
|
+
true
|
143
|
+
end
|
144
|
+
end
|
145
|
+
next h if uc # Not a plural role
|
146
|
+
|
147
|
+
counterpart_role = (role.fact_type.all_role.to_a - [role])[0]
|
148
|
+
h[role_name.call(counterpart_role)] += 1
|
149
|
+
h
|
150
|
+
end
|
151
|
+
|
152
|
+
roles.each do |role|
|
153
|
+
type_name = nil
|
154
|
+
counterpart_name = nil
|
155
|
+
|
156
|
+
if role.fact_type.entity_type and # Role is in an objectified fact type
|
157
|
+
# For binary objectified fact types, we traverse directly to the other role, not just to the objectification
|
158
|
+
!(role.fact_type.entity_type.all_role.size == 2 and role.fact_type.all_role.size == 2)
|
159
|
+
|
160
|
+
type_name = role.fact_type.entity_type.name
|
161
|
+
counterpart_name = type_name # If self plays more than one role in OFT, need to construct a role name
|
162
|
+
plural = true
|
163
|
+
elsif role.fact_type.all_role.size == 1
|
164
|
+
# Handle unary roles
|
165
|
+
type_name = 'boolean'
|
166
|
+
counterpart_name = role.fact_type.default_reading
|
167
|
+
plural = false
|
168
|
+
else
|
169
|
+
# Handle binary roles
|
170
|
+
counterpart_role = (role.fact_type.all_role.to_a - [role])[0]
|
171
|
+
type_name = counterpart_role.object_type.name
|
172
|
+
counterpart_name = role_name.call(counterpart_role)
|
173
|
+
# Figure out whether the counterpart is plural (say "all ..." if so)
|
174
|
+
uc = role.all_role_ref.detect do |rr|
|
175
|
+
rs = rr.role_sequence
|
176
|
+
next false if rs.all_role_ref.size != 1 # Looking for a UC over just this one role
|
177
|
+
rs.all_presence_constraint.detect do |pc|
|
178
|
+
next false unless pc.max_frequency == 1 # It's a uniqueness constraint
|
179
|
+
true
|
180
|
+
end
|
181
|
+
end
|
182
|
+
plural = !uc
|
183
|
+
if plural_counterpart_counts[counterpart_name] > 1
|
184
|
+
counterpart_name += " as " + role_name.call(role)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
node = {
|
189
|
+
"title" => "#{plural ? 'all ' : ''}#{counterpart_name}",
|
190
|
+
"type" => "#{type_name}",
|
191
|
+
"where" => verbalise_role.call(role, plural),
|
192
|
+
"role_id" => role.concept.guid.to_s
|
193
|
+
}
|
194
|
+
node["is_list"] = true if plural
|
195
|
+
functions << node
|
196
|
+
|
197
|
+
end
|
198
|
+
functions.size > 0 ? object_type : nil
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
ActiveFacts::Registry.generator('metadata/json', ActiveFacts::Generators::Metadata::JSON)
|
@@ -0,0 +1,32 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Generators.
|
3
|
+
# Generate *no* output for ActiveFacts vocabularies; i.e. just a stub
|
4
|
+
#
|
5
|
+
# Copyright (c) 2009 Clifford Heath. Read the LICENSE file.
|
6
|
+
#
|
7
|
+
require 'activefacts/rmap'
|
8
|
+
|
9
|
+
module ActiveFacts
|
10
|
+
module Generators
|
11
|
+
# Generate nothing from an ActiveFacts vocabulary. This is useful to check the file can be read ok.
|
12
|
+
# Invoke as
|
13
|
+
# afgen --null <file>.cql
|
14
|
+
class NULL
|
15
|
+
private
|
16
|
+
def initialize(vocabulary, *options)
|
17
|
+
@vocabulary = vocabulary
|
18
|
+
@vocabulary = @vocabulary.Vocabulary.values[0] if ActiveFacts::API::Constellation === @vocabulary
|
19
|
+
@tables = options.include? "tables"
|
20
|
+
@columns = options.include? "columns"
|
21
|
+
@indices = options.include? "indices"
|
22
|
+
end
|
23
|
+
|
24
|
+
public
|
25
|
+
def generate(out = $>)
|
26
|
+
@vocabulary.tables if @tables || @columns || @indices
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
ActiveFacts::Registry.generator('null', ActiveFacts::Generators::NULL)
|
@@ -0,0 +1,247 @@
|
|
1
|
+
#
|
2
|
+
# ActiveFacts Generators.
|
3
|
+
# Generate models for Rails from an ActiveFacts vocabulary.
|
4
|
+
#
|
5
|
+
# Models should normally be generated into "app/models/auto",
|
6
|
+
# then extend(ed) into your real models.
|
7
|
+
#
|
8
|
+
# Copyright (c) 2013 Clifford Heath. Read the LICENSE file.
|
9
|
+
#
|
10
|
+
require 'activefacts/metamodel'
|
11
|
+
require 'activefacts/rmap'
|
12
|
+
#require 'activefacts/generators/helpers/rails'
|
13
|
+
require 'activefacts/generators/traits/rails'
|
14
|
+
require 'active_support'
|
15
|
+
require 'activefacts/registry'
|
16
|
+
|
17
|
+
module ActiveFacts
|
18
|
+
module Generators
|
19
|
+
module Rails
|
20
|
+
# Generate Rails models for the vocabulary
|
21
|
+
# Invoke as
|
22
|
+
# afgen --rails/schema[=options] <file>.cql
|
23
|
+
class Models
|
24
|
+
|
25
|
+
HEADER = "# Auto-generated from CQL, edits will be lost"
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def initialize(vocabulary, *options)
|
30
|
+
@vocabulary = vocabulary
|
31
|
+
@vocabulary = @vocabulary.Vocabulary.values[0] if ActiveFacts::API::Constellation === @vocabulary
|
32
|
+
help if options.include? "help"
|
33
|
+
options.delete_if { |option| @output = $1 if option =~ /^output=(.*)/ }
|
34
|
+
@concern = nil
|
35
|
+
options.delete_if { |option| @concern = $1 if option =~ /^concern=(.*)/ }
|
36
|
+
@validations = true
|
37
|
+
options.delete_if { |option| @validations = eval($1) if option =~ /^validation=(.*)/ }
|
38
|
+
end
|
39
|
+
|
40
|
+
def help
|
41
|
+
@helping = true
|
42
|
+
warn %Q{Options for --rails/schema:
|
43
|
+
output=dir Overwrite model files into this output directory
|
44
|
+
concern=name Namespace for the concerns
|
45
|
+
validation=false Disable generation of validations
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def warn *a
|
50
|
+
$stderr.puts *a
|
51
|
+
end
|
52
|
+
|
53
|
+
def puts s
|
54
|
+
@out.puts s
|
55
|
+
end
|
56
|
+
|
57
|
+
public
|
58
|
+
def generate(out = $>) #:nodoc:
|
59
|
+
return if @helping
|
60
|
+
@out = out
|
61
|
+
list_extant_files if @output
|
62
|
+
|
63
|
+
# Populate all foreignkeys first:
|
64
|
+
@vocabulary.tables.each { |table| table.foreign_keys }
|
65
|
+
ok = true
|
66
|
+
@vocabulary.tables.each do |table|
|
67
|
+
ok &= generate_table(table)
|
68
|
+
end
|
69
|
+
$stderr.puts "\# #{@vocabulary.name} generated with errors" unless ok
|
70
|
+
delete_old_generated_files if @output
|
71
|
+
ok
|
72
|
+
end
|
73
|
+
|
74
|
+
def list_extant_files
|
75
|
+
@preexisting_files = Dir[@output+'/*.rb']
|
76
|
+
end
|
77
|
+
|
78
|
+
def delete_old_generated_files
|
79
|
+
remaining = []
|
80
|
+
cleaned = 0
|
81
|
+
@preexisting_files.each do |pathname|
|
82
|
+
if generated_file_exists(pathname) == true
|
83
|
+
File.unlink(pathname)
|
84
|
+
cleaned += 1
|
85
|
+
else
|
86
|
+
remaining << pathname
|
87
|
+
end
|
88
|
+
end
|
89
|
+
$stderr.puts "Cleaned up #{cleaned} old generated files" if @preexisting_files.size > 0
|
90
|
+
$stderr.puts "Remaining non-generated files:\n\t#{remaining*"\n\t"}" if remaining.size > 0
|
91
|
+
end
|
92
|
+
|
93
|
+
def generated_file_exists pathname
|
94
|
+
File.open(pathname, 'r') do |existing|
|
95
|
+
first_lines = existing.read(1024) # Make it possible to pass over a magic charset comment
|
96
|
+
if first_lines.length == 0 or first_lines =~ %r{^#{HEADER}}
|
97
|
+
return true
|
98
|
+
end
|
99
|
+
end
|
100
|
+
return false # File exists, but is not generated
|
101
|
+
rescue Errno::ENOENT
|
102
|
+
return nil # File does not exist
|
103
|
+
end
|
104
|
+
|
105
|
+
def create_if_ok filename
|
106
|
+
# Create a file in the output directory, being careful not to overwrite carelessly
|
107
|
+
if @output
|
108
|
+
pathname = (@output+'/'+filename).gsub(%r{//+}, '/')
|
109
|
+
@preexisting_files.reject!{|f| f == pathname } # Don't clean up this file
|
110
|
+
if generated_file_exists(pathname) == false
|
111
|
+
$stderr.puts "not overwriting non-generated file #{pathname}"
|
112
|
+
@individual_file = nil
|
113
|
+
return
|
114
|
+
end
|
115
|
+
@individual_file = @out = File.open(pathname, 'w')
|
116
|
+
puts "#{HEADER}"
|
117
|
+
end
|
118
|
+
true
|
119
|
+
end
|
120
|
+
|
121
|
+
def to_associations table
|
122
|
+
# belongs_to Associations
|
123
|
+
table.foreign_keys.map do |fk|
|
124
|
+
association_name = fk.rails_from_association_name
|
125
|
+
|
126
|
+
if association_name != fk.to.rails_singular_name
|
127
|
+
# A different class_name is implied, emit an explicit one:
|
128
|
+
class_name = ", :class_name => '#{fk.to.rails_class_name}'"
|
129
|
+
end
|
130
|
+
foreign_key = ", :foreign_key => :#{fk.from_columns[0].rails_name}"
|
131
|
+
if foreign_key == fk.to.rails_singular_name+'_id'
|
132
|
+
# See lib/active_record/reflection.rb, method #derive_foreign_key
|
133
|
+
foreign_key = ''
|
134
|
+
end
|
135
|
+
|
136
|
+
%Q{
|
137
|
+
\# #{fk.verbalised_path}
|
138
|
+
belongs_to :#{association_name}#{class_name}#{foreign_key}}
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def from_associations table
|
143
|
+
# has_one/has_many Associations
|
144
|
+
table.foreign_keys_to.sort_by{|fk| fk.describe}.map do |fk|
|
145
|
+
# Get the jump reference
|
146
|
+
|
147
|
+
if fk.from_columns.size > 1
|
148
|
+
raise "Can't emit Rails associations for multi-part foreign key with #{fk.references.inspect}. Did you mean to use --transform/surrogate"
|
149
|
+
end
|
150
|
+
|
151
|
+
association_type, association_name = *fk.rails_to_association
|
152
|
+
|
153
|
+
ref = fk.jump_reference
|
154
|
+
[
|
155
|
+
"\n \# #{fk.verbalised_path(true)}" +
|
156
|
+
"\n" +
|
157
|
+
%Q{ #{association_type} :#{association_name}} +
|
158
|
+
%Q{, :class_name => '#{fk.from.rails_class_name}'} +
|
159
|
+
%Q{, :foreign_key => :#{fk.from_columns[0].rails_name}} +
|
160
|
+
%Q{, :dependent => :destroy}
|
161
|
+
] +
|
162
|
+
# If ref.from is a join table, we can emit a has_many :through for each other key
|
163
|
+
# REVISIT Could alternately do this for all belongs_to's in ref.from
|
164
|
+
if ref.from.identifier_columns.length > 1
|
165
|
+
ref.from.identifier_columns.map do |ic|
|
166
|
+
next nil if ic.references[0] == ref or # Skip the back-reference
|
167
|
+
ic.references[0].is_unary # or use rails_plural_name(ic.references[0].to_names) ?
|
168
|
+
# This far association name needs to be augmented for its role name
|
169
|
+
far_association_name = ic.references[0].to.rails_name
|
170
|
+
%Q{ has_many :#{far_association_name}, :through => :#{association_name}} # \# via #{ic.name}}
|
171
|
+
end
|
172
|
+
else
|
173
|
+
[]
|
174
|
+
end
|
175
|
+
end.flatten.compact
|
176
|
+
end
|
177
|
+
|
178
|
+
def column_constraints table
|
179
|
+
return [] unless @validations
|
180
|
+
ccs =
|
181
|
+
table.columns.map do |column|
|
182
|
+
name = column.rails_name
|
183
|
+
column.is_mandatory &&
|
184
|
+
!column.is_auto_assigned && !column.is_auto_timestamp ? [
|
185
|
+
" validates :#{name}, :presence => true"
|
186
|
+
] : []
|
187
|
+
end.flatten
|
188
|
+
ccs.unshift("") unless ccs.empty?
|
189
|
+
ccs
|
190
|
+
end
|
191
|
+
|
192
|
+
def model_body table
|
193
|
+
%Q{module #{table.rails_class_name}
|
194
|
+
extend ActiveSupport::Concern
|
195
|
+
included do} +
|
196
|
+
(table.identifier_columns.length == 1 ? %Q{
|
197
|
+
self.primary_key = '#{table.identifier_columns[0].rails_name}'
|
198
|
+
} : ''
|
199
|
+
) +
|
200
|
+
|
201
|
+
(
|
202
|
+
to_associations(table) +
|
203
|
+
from_associations(table) +
|
204
|
+
column_constraints(table)
|
205
|
+
) * "\n" +
|
206
|
+
%Q{
|
207
|
+
end
|
208
|
+
end
|
209
|
+
}
|
210
|
+
end
|
211
|
+
|
212
|
+
def generate_table table
|
213
|
+
old_out = @out
|
214
|
+
filename = table.rails_singular_name+'.rb'
|
215
|
+
|
216
|
+
return unless create_if_ok filename
|
217
|
+
|
218
|
+
puts "\n"
|
219
|
+
puts "module #{@concern}" if @concern
|
220
|
+
puts model_body(table).gsub(/^./, @concern ? ' \0' : '\0')
|
221
|
+
puts 'end' if @concern
|
222
|
+
|
223
|
+
true # We succeeded
|
224
|
+
ensure
|
225
|
+
@out = old_out
|
226
|
+
@individual_file.close if @individual_file
|
227
|
+
end
|
228
|
+
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
module RMap
|
234
|
+
class Column
|
235
|
+
def is_auto_timestamp
|
236
|
+
case name('_')
|
237
|
+
when /\A(created|updated)_(at|on)\Z/i
|
238
|
+
true
|
239
|
+
else
|
240
|
+
false
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
ActiveFacts::Registry.generator('rails/models', ActiveFacts::Generators::Rails::Models)
|