whiteprint 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ac5d879aa5d240df5fb1689636e8c15462f9a192
4
+ data.tar.gz: 464a364c30c9edc45f9418d4f1f9ab8eaf57fb43
5
+ SHA512:
6
+ metadata.gz: 78358814f2b3e688cdb5720a4049c3bd3b0951d83f74627aaa754688ffa2b9792d5c8d44de038b824f44b785c55046b69299d84048a932f5157cfd223dde64f4
7
+ data.tar.gz: 2304c58e46b558e2833ebb1fe8d8aafed433e060a4f62afb814a1ea95389e5ab5793cdb82f04c06bee692bf8c9f753851b97325d299aaf25047eeef2d48bcc50
@@ -0,0 +1,251 @@
1
+ # Whiteprint
2
+ by [10KB](https://10kb.nl)
3
+
4
+
5
+ Whiteprint keeps track of the attributes of your models. It:
6
+ * Generates migrations for you if you update your model's whiteprint (only ActiveRecord at the moment)
7
+ * Provides you with helpers to use in your serializers or permitted attributes definition
8
+ * Can be extended with plugins
9
+ * Has support for inheritance and composition
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ gem 'whiteprint'
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install whiteprint
24
+
25
+ ## Usage
26
+
27
+ ### 1. Add Whiteprint to your model
28
+
29
+ ```ruby
30
+ class Car
31
+ include Whiteprint::Model
32
+ end
33
+ ```
34
+
35
+ Alternatively, in an ActiveRecord model you could also use `has_whiteprint`.
36
+
37
+ ```ruby
38
+ class Car < ActiveRecord::Base
39
+ has_whiteprint
40
+ end
41
+ ```
42
+
43
+ ### 2. Add some attributes
44
+
45
+ ```ruby
46
+ class Car < ActiveRecord::Base
47
+ include Whiteprint::Model
48
+
49
+ whiteprint do
50
+ string :brand, default: 'BMW'
51
+ string :name
52
+ text :description
53
+ decimal :price, precision: 5, scale: 10
54
+ end
55
+ end
56
+ ```
57
+
58
+ ### 3. Generate a migration
59
+ Let Whiteprint generate a migration to update your database schema for you (only ActiveRecord at the moment). Run:
60
+
61
+ ```
62
+ rake whiteprint:migrate
63
+ ```
64
+
65
+ Whiteprint will check all your models for changes and list them in your terminal. If multiple models have changes it will ask you if you want to apply these changes in one or separate migrations.
66
+
67
+ ```
68
+ Whiteprint has detected 1 changes to your models.
69
+ +----------------------------+------------------------+--------------------------------------------+
70
+ | 1. Create a new table cars |
71
+ +----------------------------+------------------------+--------------------------------------------+
72
+ | name | type | options |
73
+ +----------------------------+------------------------+--------------------------------------------+
74
+ | brand | string | {:default=>"BMW"} |
75
+ | name | string | {} |
76
+ | description | text | {} |
77
+ | price | decimal | {:precision=>10, :scale=>5} |
78
+ | timestamps | | |
79
+ +----------------------------+------------------------+--------------------------------------------+
80
+ Migrations:
81
+ 1. In one migration
82
+ 2. In separate migrations
83
+ How would you like to process these changes?
84
+ > 1
85
+ How would you like to name this migration?
86
+ > Create cars
87
+ ```
88
+ Your migration wil be **created** and **migrated**.
89
+
90
+ ```ruby
91
+ # db/migrate/*********_create_cars.rb
92
+
93
+ class CreateCars < ActiveRecord::Migration
94
+ def change
95
+ create_table :cars do |t|
96
+ t.string :brand, {:default=>"BMW"}
97
+ t.string :name, {}
98
+ t.text :description, {}
99
+ t.decimal :price, {:precision=>10, :scale=>5}
100
+ t.timestamps
101
+ end
102
+ end
103
+ end
104
+ ```
105
+
106
+ ```
107
+ == 20160905153022 CreateCars: migrating =======================================
108
+ -- create_table(:cars)
109
+ -> 0.0081s
110
+ == 20160905153022 CreateCars: migrated (0.0082s) ==============================
111
+ ```
112
+
113
+ ### 4. Make some changes to your model
114
+ If we make some changes to our `Car` model and run `whiteprint:migrate` again, Whiteprint will detect these changes and create a migration to update your table.
115
+
116
+ ```ruby
117
+ class Car < ActiveRecord::Base
118
+ include Whiteprint::Model
119
+
120
+ whiteprint do
121
+ string :brand, default: 'Ford'
122
+ string :name
123
+ decimal :price, precision: 10, scale: 5
124
+ references :color
125
+ end
126
+ end
127
+ ```
128
+
129
+ ```
130
+ > rake whiteprint:migrate
131
+ Whiteprint has detected 1 changes to your models.
132
+ +--------+-------------+------------+------------------+--------------------+----------------------+
133
+ | 1. Make changes to cars |
134
+ +--------+-------------+------------+------------------+--------------------+----------------------+
135
+ | action | name | type | type (currently) | options | options (currently) |
136
+ +--------+-------------+------------+------------------+--------------------+----------------------+
137
+ | added | color | references | | {} | |
138
+ | change | brand | string | string | {:default=>"Ford"} | {:default=>"BMW"} |
139
+ | remove | description | | | | |
140
+ +--------+-------------+------------+------------------+--------------------+----------------------+
141
+ Migrations:
142
+ 1. In one migration
143
+ 2. In separate migrations
144
+ How would you like to process these changes?
145
+ 1
146
+ How would you like to name this migration?
147
+ Add color change default brand and remove description for cars
148
+ == 20160905162923 AddColorChangeDefaultBrandAndRemoveDescriptionForCars: migrating
149
+ -- change_table(:cars)
150
+ -> 0.0032s
151
+ == 20160905162923 AddColorChangeDefaultBrandAndRemoveDescriptionForCars: migrated (0.0034s)
152
+ ```
153
+
154
+ ## Adapters
155
+ Whiteprint is made to be persistence layer agnostic, but at this moment only an ActiveRecord adapter is implemented. If you would like to implement an adapter for another persistence layer please contact us. We'd love to help you.
156
+
157
+ An example of a Whiteprint adapter:
158
+ ```ruby
159
+ module Whiteprint
160
+ module Adapters
161
+ class MyOwnAdapater < ::Whiteprint::Base
162
+ class << self
163
+ def applicable?(model)
164
+ # method used to automatically select an adapter for a model.
165
+ # for example:
166
+ model < MyOrm::Base
167
+ end
168
+
169
+ def generate_migration(name, trees)
170
+ # create a migration here given a set of trees with changes
171
+ # look at the activerecord adapter for further implementation details
172
+ end
173
+ end
174
+
175
+ def persisted_attributes
176
+ # this method has to return the current attributes of the persistance layer
177
+ # return an instance of Whiteprint::Attributes
178
+ end
179
+
180
+ # The whiteprint do ... end block in your model is executed in the context of your adapter instance
181
+ # you can add methods to add functionality to your adapter. For example:
182
+ def address(name)
183
+ @attributes.add name: "#{name}_street", type: :text
184
+ @attributes.add name: "#{name}_house_number", type: :integer
185
+ @attributes.add name: "#{name}_city", type: :text
186
+ end
187
+ # And then you could do:
188
+ # class Company < MyOrm::Base
189
+ # include Whiteprint::Model
190
+ #
191
+ # whiteprint do
192
+ # address :office
193
+ # end
194
+ # end
195
+ end
196
+ end
197
+ end
198
+ ```
199
+
200
+ ## ActiveRecord Adapter
201
+ The ActiveRecord adapter has some special properties which are explained in this section.
202
+
203
+ ### Default id and timestamps
204
+ By default the adapter will add id and timestamps columns. You can disable this behaviour by passing arguments to the whiteprint method.
205
+
206
+ Model without an id:
207
+ ```ruby
208
+ whiteprint(id: false) do
209
+ # ...
210
+ end
211
+ ```
212
+
213
+ Model without timestamps:
214
+ ```ruby
215
+ whiteprint(timestamps: false) do
216
+ # ...
217
+ end
218
+ ```
219
+
220
+ ### References
221
+ Adding an references columns will automatically set a `belongs_to` association on the model. Any options for the association can be passed in the whiteprint block.
222
+
223
+ ```ruby
224
+ whiteprint do
225
+ references :fileable, polymorphic: true
226
+ end
227
+ ```
228
+
229
+ You can disable this behaviour by passing `auto_belongs_to: false` to the whiteprint method.
230
+
231
+ ### Has and belongs to many
232
+ The activerecord adapter has support for a has_and_belongs_to_many attribute. This won't add a column to your model's table, but instead create a join table and set the association.
233
+
234
+ ```ruby
235
+ whiteprint do
236
+ has_and_belongs_to_many :categories
237
+ end
238
+ ```
239
+
240
+ `habtm` is added as an alias
241
+
242
+ ### Method as default value
243
+
244
+ ### Accessor
245
+
246
+ ## Attributes
247
+
248
+ ## Configuration
249
+
250
+ ## Origin
251
+ Whiteprint is extracted from an application framework we use internally. Right now, our framework is lacking tests and documentation, but we intend to open source more parts of our framework in the future.
@@ -0,0 +1,55 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+ require 'rubocop/rake_task'
4
+
5
+ test = File.expand_path('../test', __FILE__)
6
+ $LOAD_PATH.unshift(test) unless $LOAD_PATH.include?(test)
7
+
8
+ task default: :test
9
+
10
+ Rake::TestTask.new do |t|
11
+ t.libs << 'test'
12
+ t.pattern = 'test/**/*_test.rb'
13
+ t.verbose = true
14
+ t.warning = true
15
+ end
16
+
17
+ namespace :analysis do
18
+ RuboCop::RakeTask.new
19
+
20
+ desc 'Analyze code style'
21
+ task style: [:rubocop]
22
+
23
+ desc 'Analyze code duplication'
24
+ task :duplication do
25
+ output = `bundle exec flay lib/`
26
+ if output.include? 'Similar code found'
27
+ puts output
28
+ exit 1
29
+ end
30
+ end
31
+
32
+ desc 'Analyze code complexity'
33
+ task :comlexity do
34
+ output = `bundle exec flog -m lib/`
35
+ exit_code = 0
36
+ minimum_score = 30
37
+ output = output.lines
38
+
39
+ # Skip total and average complexity score
40
+ output.shift
41
+ output.shift
42
+
43
+ output.each do |line|
44
+ score, method = line.split(' ')
45
+ score = score.to_i
46
+
47
+ if score > minimum_score
48
+ exit_code = 1
49
+ puts "High complexity in #{method}. Score: #{score}"
50
+ end
51
+ end
52
+
53
+ exit exit_code
54
+ end
55
+ end
@@ -0,0 +1,5 @@
1
+ namespace :whiteprint do
2
+ task migrate: :environment do
3
+ Whiteprint::Migrator.interactive
4
+ end
5
+ end
@@ -0,0 +1,99 @@
1
+ require 'whiteprint/version'
2
+
3
+ module Whiteprint
4
+ require 'active_support/concern' unless defined?(ActiveSupport)
5
+ require 'active_support/inflections'
6
+
7
+ require 'parslet'
8
+ require 'terminal-table'
9
+ require 'highline'
10
+
11
+ require 'whiteprint/config'
12
+ require 'whiteprint/attributes'
13
+ require 'whiteprint/base'
14
+ require 'whiteprint/explanation'
15
+ require 'whiteprint/model'
16
+ require 'whiteprint/migrator'
17
+ require 'whiteprint/transform'
18
+
19
+ config do |c|
20
+ c.default_adapter = :base
21
+ c.eager_load = false
22
+ c.eager_load_paths = []
23
+ c.persisted_attribute_options = {
24
+ array: false,
25
+ limit: nil,
26
+ precision: nil,
27
+ scale: nil,
28
+ polymorphic: false,
29
+ null: true,
30
+ default: nil
31
+ }
32
+ c.meta_attribute_options = [:enum]
33
+ end
34
+
35
+ if defined?(ActiveRecord)
36
+ require 'whiteprint/has_whiteprint'
37
+ ActiveSupport.on_load :active_record do
38
+ ActiveRecord::Base.send :extend, Whiteprint::HasWhiteprint
39
+ end
40
+ end
41
+
42
+ require 'whiteprint/railtie' if defined?(Rails)
43
+
44
+ require 'whiteprint/adapters/active_record'
45
+ require 'whiteprint/adapters/test'
46
+
47
+ ADAPTERS = {
48
+ active_record: Adapters::ActiveRecord,
49
+ has_and_belongs_to_many: Adapters::ActiveRecord::HasAndBelongsToMany,
50
+ test: Adapters::Test,
51
+ base: Base
52
+ }.freeze
53
+
54
+ class << self
55
+ def new(model, adapter: nil, **options)
56
+ if adapter
57
+ ADAPTERS[adapter].new(model, **options)
58
+ else
59
+ adapter = ADAPTERS.find do |_, whiteprint|
60
+ whiteprint.applicable?(model)
61
+ end
62
+
63
+ adapter[-1].new(model, **options)
64
+ end
65
+ end
66
+
67
+ @@models = []
68
+ @@plugins = {}
69
+
70
+ def models=(models)
71
+ @@models = models
72
+ end
73
+
74
+ def models
75
+ @@models.select { |model| model.is_a?(Class) }
76
+ .reject { |model| model.respond_to?(:abstract_class) && model.abstract_class }
77
+ .sort_by { |model| model.respond_to?(:table_name) && model.table_name || model.name || model.object_id.to_s }
78
+ .sort { |a, b| -1 * (a <=> b).to_i }
79
+ .uniq { |model| model.respond_to?(:table_name) && model.table_name || model.name || model.object_id.to_s }
80
+ .sort_by { |model| model.name || model.object_id.to_s }
81
+ end
82
+
83
+ def whiteprints
84
+ models.map(&:whiteprint).compact
85
+ end
86
+
87
+ def changed_whiteprints
88
+ whiteprints.select(&:changes?)
89
+ end
90
+
91
+ def plugins
92
+ @@plugins
93
+ end
94
+
95
+ def register_plugin(name, constant)
96
+ @@plugins[name] = constant
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,193 @@
1
+ module Whiteprint
2
+ module Adapters
3
+ class ActiveRecord < ::Whiteprint::Base
4
+ require 'whiteprint/adapters/active_record/migration'
5
+ require 'whiteprint/adapters/active_record/has_and_belongs_to_many'
6
+
7
+ BELONGS_TO_OPTIONS = [:class_name, :anonymous_class, :foreign_key, :validate, :autosave,
8
+ :dependent, :primary_key, :inverse_of, :required, :foreign_type,
9
+ :polymorphic, :touch, :counter_cache, :cached]
10
+
11
+ class << self
12
+ def applicable?(model)
13
+ return false unless defined?(::ActiveRecord)
14
+ model < ::ActiveRecord::Base
15
+ end
16
+
17
+ def underscore(name)
18
+ name = name.tr(' ', '_')
19
+ name.gsub(/([a-z])([A-Z])/) { "#{Regexp.last_match[1]}_#{Regexp.last_match[2].downcase}" }.downcase
20
+ end
21
+
22
+ def camelize(name)
23
+ name = underscore(name)
24
+ name = name.gsub(/^([a-z])/) { Regexp.last_match[1].upcase }
25
+ name.gsub(/_([a-zA-Z])/) { Regexp.last_match[1].upcase }
26
+ end
27
+
28
+ def generate_migration(name, trees)
29
+ filename = "#{Time.now.strftime('%Y%m%d%H%M%S')}_#{underscore(name)}.rb"
30
+ File.open(File.join(Whiteprint.config.migration_path, filename), 'w') do |f|
31
+ f.write migration(name, trees)
32
+ end
33
+ end
34
+
35
+ def migration(name, trees)
36
+ "class #{camelize(name)} < ActiveRecord::Migration\n def change\n" + transform(trees) + " end\nend\n"
37
+ end
38
+
39
+ private
40
+
41
+ def transform(trees)
42
+ Migration.new.apply(trees).join("\n")
43
+ end
44
+ end
45
+
46
+ def initialize(model, id: true, timestamps: true, auto_belongs_to: true, **_options)
47
+ super(model, id: true, timestamps: true, **_options)
48
+
49
+ @has_id = id
50
+ @has_timestamps = timestamps
51
+ @auto_belongs_to = auto_belongs_to
52
+ @attributes.add(name: :id, type: :integer, null: false) if id
53
+ @attributes.add(name: :created_at, type: :datetime) if timestamps
54
+ @attributes.add(name: :updated_at, type: :datetime) if timestamps
55
+ end
56
+
57
+ def accessor(name, options)
58
+ @attributes.add(name: name, type: :accessor, virtual: true, **options)
59
+ model.send :attr_accessor, name
60
+ end
61
+
62
+ def changes_tree
63
+ changes_tree = super
64
+
65
+ unless changes_tree[:table_exists]
66
+ changes_tree[:has_id] = @has_id
67
+ changes_tree[:attributes].reject! { |attribute| attribute[:name] == :id }
68
+ end
69
+
70
+ added_created_at = changes_tree[:attributes].select { |attribute| attribute[:name] == :created_at && attribute[:kind] == :added }
71
+ added_updated_at = changes_tree[:attributes].select { |attribute| attribute[:name] == :updated_at && attribute[:kind] == :added }
72
+
73
+ if added_created_at.size == 1 && added_updated_at.size == 1
74
+ changes_tree[:attributes] -= [*added_created_at, *added_updated_at]
75
+ changes_tree[:attributes] += [{ type: :timestamps, kind: :added }]
76
+ end
77
+
78
+ changes_tree[:attributes].each do |attribute|
79
+ persisted_attribute = persisted_attributes[attribute[:name]]
80
+ if persisted_attribute && attribute[:options][:default].nil? && persisted_attribute[:options][:default]
81
+ changes_tree[:attributes] += [{ name: attribute[:name], kind: :removed_default }]
82
+ end
83
+ end
84
+
85
+ changes_tree
86
+ end
87
+
88
+ def has_and_belongs_to_many(name, **options)
89
+ super(name, **options.merge(virtual: true))
90
+ model.send(:has_and_belongs_to_many, name.to_sym, **options) unless model.reflect_on_association(name)
91
+
92
+ association = model.reflect_on_association(name)
93
+
94
+ Class.new do
95
+ include Whiteprint::Model
96
+
97
+ @association = association
98
+ @join_table = association.join_table
99
+
100
+ whiteprint(adapter: :has_and_belongs_to_many, id: false, timestamps: false) do
101
+ integer association.foreign_key
102
+ integer association.association_foreign_key
103
+ end
104
+
105
+ class << self
106
+ def name
107
+ "#{@join_table}_habtm_model"
108
+ end
109
+
110
+ def table_name
111
+ @join_table
112
+ end
113
+
114
+ def table_exists?
115
+ ::ActiveRecord::Base.connection.table_exists?(table_name)
116
+ end
117
+ end
118
+ end
119
+ end
120
+ alias_method :habtm, :has_and_belongs_to_many
121
+
122
+ def migration(name)
123
+ self.class.migration(name, [changes_tree])
124
+ end
125
+
126
+ def options_from_column(column)
127
+ [:name, :type, *Whiteprint.config.persisted_attribute_options.keys].map do |option|
128
+ association_by_foreign_key = find_association_by_foreign_key(column)
129
+ overridden_name = association_by_foreign_key && association_by_foreign_key.name || column.name
130
+ current_attribute = attributes[overridden_name]
131
+
132
+ next {name: overridden_name} if option == :name && association_by_foreign_key
133
+ next {type: :references} if option == :type && association_by_foreign_key
134
+ next {polymorphic: true} if option == :polymorphic && association_by_foreign_key && model.column_names.include?(association_by_foreign_key.foreign_type)
135
+ next unless column.respond_to?(option)
136
+ next {default: current_attribute.default} if option == :default && current_attribute && current_attribute.default.is_a?(Symbol)
137
+
138
+ value = column.send(option)
139
+ value = column.type_cast_from_database(value) if option == :default
140
+ next if value == Whiteprint.config.persisted_attribute_options[option]
141
+ { option => value }
142
+ end.compact.inject(&:merge)
143
+ end
144
+
145
+ def persisted_attributes
146
+ attributes = Whiteprint::Attributes.new
147
+ return attributes unless table_exists?
148
+ model.columns.each do |column|
149
+ next if find_association_by_foreign_type(column)
150
+
151
+ attributes.add options_from_column(column)
152
+ end
153
+ attributes.for_persisted
154
+ end
155
+
156
+ def references(name, **options)
157
+ super
158
+ return unless @auto_belongs_to
159
+ model.send :belongs_to, name.to_sym, **options.slice(*BELONGS_TO_OPTIONS)
160
+ end
161
+
162
+ def table_exists?
163
+ model.connection.schema_cache.clear!
164
+ model.connection.table_exists?(table_name)
165
+ end
166
+
167
+ def method_missing(type, name, **options)
168
+ super
169
+
170
+ if options[:default] && options[:default].is_a?(Symbol)
171
+ model.send :after_initialize do
172
+ next if self.send(name) || !new_record?
173
+ self.send "#{name}=", send(options[:default])
174
+ end
175
+ end
176
+ end
177
+
178
+ private
179
+
180
+ def find_association_by_foreign_key(column)
181
+ model.reflect_on_all_associations.find do |association|
182
+ association.foreign_key == column.name
183
+ end
184
+ end
185
+
186
+ def find_association_by_foreign_type(column)
187
+ model.reflect_on_all_associations.find do |association|
188
+ association.polymorphic? && association.foreign_type.to_s == column.name.to_s
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end