declare_schema 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.dependabot/config.yml +10 -0
  3. data/.github/workflows/gem_release.yml +38 -0
  4. data/.gitignore +14 -0
  5. data/.jenkins/Jenkinsfile +72 -0
  6. data/.jenkins/ruby_build_pod.yml +19 -0
  7. data/.rspec +2 -0
  8. data/.rubocop.yml +189 -0
  9. data/.ruby-version +1 -0
  10. data/Appraisals +14 -0
  11. data/CHANGELOG.md +11 -0
  12. data/Gemfile +24 -0
  13. data/Gemfile.lock +203 -0
  14. data/LICENSE.txt +22 -0
  15. data/README.md +11 -0
  16. data/Rakefile +56 -0
  17. data/bin/declare_schema +11 -0
  18. data/declare_schema.gemspec +25 -0
  19. data/gemfiles/.bundle/config +2 -0
  20. data/gemfiles/rails_4.gemfile +25 -0
  21. data/gemfiles/rails_5.gemfile +25 -0
  22. data/gemfiles/rails_6.gemfile +25 -0
  23. data/lib/declare_schema.rb +44 -0
  24. data/lib/declare_schema/command.rb +65 -0
  25. data/lib/declare_schema/extensions/active_record/fields_declaration.rb +28 -0
  26. data/lib/declare_schema/extensions/module.rb +36 -0
  27. data/lib/declare_schema/field_declaration_dsl.rb +40 -0
  28. data/lib/declare_schema/model.rb +242 -0
  29. data/lib/declare_schema/model/field_spec.rb +162 -0
  30. data/lib/declare_schema/model/index_spec.rb +175 -0
  31. data/lib/declare_schema/railtie.rb +12 -0
  32. data/lib/declare_schema/version.rb +5 -0
  33. data/lib/generators/declare_schema/migration/USAGE +47 -0
  34. data/lib/generators/declare_schema/migration/migration_generator.rb +184 -0
  35. data/lib/generators/declare_schema/migration/migrator.rb +567 -0
  36. data/lib/generators/declare_schema/migration/templates/migration.rb.erb +9 -0
  37. data/lib/generators/declare_schema/model/USAGE +19 -0
  38. data/lib/generators/declare_schema/model/model_generator.rb +12 -0
  39. data/lib/generators/declare_schema/model/templates/model_injection.rb.erb +25 -0
  40. data/lib/generators/declare_schema/support/eval_template.rb +21 -0
  41. data/lib/generators/declare_schema/support/model.rb +64 -0
  42. data/lib/generators/declare_schema/support/thor_shell.rb +39 -0
  43. data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +28 -0
  44. data/spec/spec_helper.rb +28 -0
  45. data/test/api.rdoctest +136 -0
  46. data/test/doc-only.rdoctest +76 -0
  47. data/test/generators.rdoctest +60 -0
  48. data/test/interactive_primary_key.rdoctest +56 -0
  49. data/test/migration_generator.rdoctest +846 -0
  50. data/test/migration_generator_comments.rdoctestDISABLED +74 -0
  51. data/test/prepare_testapp.rb +15 -0
  52. data/test_responses.txt +2 -0
  53. metadata +109 -0
@@ -0,0 +1,9 @@
1
+ class <%= @migration_class_name %> < ActiveRecord::Migration<%= Rails::VERSION::MAJOR > 4 ? '[4.2]' : '' %>
2
+ def self.up
3
+ <%= @up %>
4
+ end
5
+
6
+ def self.down
7
+ <%= @down %>
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ Description:
2
+ Invokes the active_record:model generator, but the generated
3
+ model file contains the fields block, (and the migration option
4
+ is false by default).
5
+
6
+ Examples:
7
+ $ rails generate declare_schema:model account
8
+
9
+ creates an Account model, test and fixture:
10
+ Model: app/models/account.rb
11
+ Test: test/unit/account_test.rb
12
+ Fixtures: test/fixtures/accounts.yml
13
+
14
+ $ rails generate declare_schema:model post title:string body:text published:boolean
15
+
16
+ creates a Post model with a string title, text body, and published flag.
17
+
18
+ After the model is created, and the fields are specified, use declare_schema:migration
19
+ to create the migrations incrementally.
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/active_record'
4
+ require 'generators/declare_schema/support/model'
5
+
6
+ module DeclareSchema
7
+ class ModelGenerator < ActiveRecord::Generators::Base
8
+ source_root File.expand_path('templates', __dir__)
9
+
10
+ include DeclareSchema::Support::Model
11
+ end
12
+ end
@@ -0,0 +1,25 @@
1
+
2
+ fields do
3
+ <% for attribute in field_attributes -%>
4
+ <%= "%-#{max_attribute_length}s" % attribute.name %> :<%= attribute.type %><%=
5
+ case attribute.type.to_s
6
+ when 'string'
7
+ ', limit: 255'
8
+ else
9
+ ''
10
+ end
11
+ %>
12
+ <% end -%>
13
+ <% if options[:timestamps] -%>
14
+ timestamps
15
+ <% end -%>
16
+ end
17
+
18
+ <% for bt in bts -%>
19
+ belongs_to :<%= bt %>
20
+ <% end -%>
21
+ <%= "\n" unless bts.empty? -%>
22
+ <% for hm in hms -%>
23
+ has_many :<%= hm %>, dependent: :destroy
24
+ <% end -%>
25
+ <%= "\n" unless hms.empty? -%>
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeclareSchema
4
+ module Support
5
+ module EvalTemplate
6
+ class << self
7
+ def included(base)
8
+ base.class_eval do
9
+ private
10
+
11
+ def eval_template(template_name)
12
+ source = File.expand_path(find_in_source_paths(template_name))
13
+ context = instance_eval('binding')
14
+ ERB.new(::File.binread(source), trim_mode: '-').result(context)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './eval_template'
4
+
5
+ module DeclareSchema
6
+ module Support
7
+ module Model
8
+ class << self
9
+ def included(base)
10
+ base.class_eval do
11
+ include EvalTemplate
12
+
13
+ argument :attributes, type: :array, default: [], banner: "field:type field:type"
14
+
15
+ class << self
16
+ def banner
17
+ "rails generate declare_schema:model #{arguments.map(&:usage).join(' ')} [options]"
18
+ end
19
+ end
20
+
21
+ class_option :timestamps, type: :boolean
22
+
23
+ def generate_model
24
+ invoke "active_record:model", [name], { migration: false }.merge(options)
25
+ end
26
+
27
+ def inject_declare_schema_code_into_model_file
28
+ gsub_file(model_path, / # attr_accessible :title, :body\n/m, "")
29
+ inject_into_class model_path, class_name do
30
+ eval_template('model_injection.rb.erb')
31
+ end
32
+ end
33
+
34
+ protected
35
+
36
+ def model_path
37
+ @model_path ||= File.join("app", "models", "#{file_path}.rb")
38
+ end
39
+
40
+ def max_attribute_length
41
+ attributes.map { |attribute| attribute.name.length }.max
42
+ end
43
+
44
+ def field_attributes
45
+ attributes.reject { |a| a.name == "bt" || a.name == "hm" }
46
+ end
47
+
48
+ def accessible_attributes
49
+ field_attributes.map(&:name) + bts.map { |bt| "#{bt}_id" } + bts + hms
50
+ end
51
+
52
+ def hms
53
+ attributes.select { |a| a.name == "hm" }.map(&:type)
54
+ end
55
+
56
+ def bts
57
+ attributes.select { |a| a.name == "bt" }.map(&:type)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeclareSchema
4
+ module Support
5
+ module ThorShell
6
+ PREFIX = ' => '
7
+
8
+ private
9
+
10
+ def ask(statement, default = '', color = :magenta)
11
+ result = super(statement, color)
12
+ result = default if result.blank?
13
+ say PREFIX + result.inspect
14
+ result
15
+ end
16
+
17
+ def yes_no?(statement, _color=:magenta)
18
+ result = choose(statement + ' [y|n]', /^(y|n)$/i)
19
+ result == 'y'
20
+ end
21
+
22
+ def choose(prompt, format, default=nil)
23
+ choice = ask prompt, default
24
+ if choice =~ format
25
+ choice
26
+ elsif choice.blank? && !default.blank?
27
+ default
28
+ else
29
+ say 'Unknown choice! ', :red
30
+ choose(prompt, format, default)
31
+ end
32
+ end
33
+
34
+ def say_title(title)
35
+ say "\n #{title} \n", "\e[37;44m"
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../lib/declare_schema/field_declaration_dsl'
4
+
5
+ RSpec.describe DeclareSchema::FieldDeclarationDsl do
6
+ class TestModel < ActiveRecord::Base
7
+ fields do
8
+ name :string, limit: 127
9
+
10
+ timestamps
11
+ end
12
+ end
13
+
14
+ let(:model) { TestModel.new }
15
+ subject { declared_class.new(model) }
16
+
17
+ it 'has fields' do
18
+ expect(TestModel.field_specs).to be_kind_of(Hash)
19
+ expect(TestModel.field_specs.keys).to eq(['name', 'created_at', 'updated_at'])
20
+ expect(TestModel.field_specs.values.map(&:type)).to eq([:string, :datetime, :datetime])
21
+ end
22
+
23
+ it 'stores limits' do
24
+ expect(TestModel.field_specs['name'].limit).to eq(127)
25
+ end
26
+
27
+ # TODO: fill out remaining tests
28
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "declare_schema"
5
+ require "climate_control"
6
+
7
+ RSpec.configure do |config|
8
+ # Enable flags like --only-failures and --next-failure
9
+ config.example_status_persistence_file_path = ".rspec_status"
10
+
11
+ # Disable RSpec exposing methods globally on `Module` and `main`
12
+ config.disable_monkey_patching!
13
+
14
+ config.expect_with :rspec do |expectations|
15
+ expectations.syntax = :expect
16
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
17
+ end
18
+ config.mock_with :rspec do |mocks|
19
+ mocks.verify_partial_doubles = true
20
+ end
21
+ config.shared_context_metadata_behavior = :apply_to_host_groups
22
+
23
+ RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length = 2_000
24
+
25
+ def with_modified_env(options, &block)
26
+ ClimateControl.modify(options, &block)
27
+ end
28
+ end
@@ -0,0 +1,136 @@
1
+ # DeclareSchema API
2
+
3
+ In order for the API examples to run we need to load the rails generators of our testapp:
4
+ {.hidden}
5
+
6
+ doctest: prepare testapp environment
7
+ doctest_require: 'prepare_testapp'
8
+ {.hidden}
9
+
10
+ ## Example Models
11
+
12
+ Let's define some example models that we can use to demonstrate the API. With DeclareSchema we can use the 'declare_schema:model' generator like so:
13
+
14
+ $ rails generate declare_schema:model advert title:string body:text
15
+
16
+ This will generate the test, fixture and a model file like this:
17
+
18
+ >> Rails::Generators.invoke 'declare_schema:model', %w(advert title:string body:text)
19
+ {.hidden}
20
+
21
+ class Advert < ActiveRecord::Base
22
+ fields do
23
+ title :string
24
+ body :text, limit: 0xffff, null: true
25
+ end
26
+ end
27
+
28
+ The migration generator uses this information to create a migration. The following creates and runs the migration so we're ready to go.
29
+
30
+ $ rails generate declare_schema:migration -n -m
31
+
32
+ We're now ready to start demonstrating the API
33
+
34
+ >> require_relative "#{Rails.root}/app/models/advert.rb" if Rails::VERSION::MAJOR > 5
35
+ >> Rails::Generators.invoke 'declare_schema:migration', %w(-n -m)
36
+ >> Rails::Generators.invoke 'declare_schema:migration', %w(-n -m)
37
+ {.hidden}
38
+
39
+ ## The Basics
40
+
41
+ The main feature of DeclareSchema, aside from the migration generator, is the ability to declare rich types for your fields. For example, you can declare that a field is an email address, and the field will be automatically validated for correct email address syntax.
42
+
43
+ ### Field Types
44
+
45
+ Field values are returned as the type you specify.
46
+
47
+ >> a = Advert.new :body => "This is the body", id: 1, title: "title"
48
+ >> a.body.class
49
+ => String
50
+
51
+ This also works after a round-trip to the database
52
+
53
+ >> a.save
54
+ >> b = Advert.find(a.id)
55
+ >> b.body.class
56
+ => String
57
+
58
+ ## Names vs. Classes
59
+
60
+ The full set of available symbolic names is
61
+
62
+ * `:integer`
63
+ * `:float`
64
+ * `:decimal`
65
+ * `:string`
66
+ * `:text`
67
+ * `:boolean`
68
+ * `:date`
69
+ * `:datetime`
70
+ * `:html`
71
+ * `:textile`
72
+ * `:markdown`
73
+ * `:password`
74
+
75
+ You can add your own types too. More on that later.
76
+
77
+
78
+ ## Model extensions
79
+
80
+ DeclareSchema adds a few features to your models.
81
+
82
+ ### `Model.attr_type`
83
+
84
+ Returns the type (i.e. class) declared for a given field or attribute
85
+
86
+ >> Advert.connection.schema_cache.clear!
87
+ >> Advert.reset_column_information
88
+ >> Advert.attr_type :title
89
+ => String
90
+ >> Advert.attr_type :body
91
+ => String
92
+
93
+ ## Field validations
94
+
95
+ DeclareSchema gives you some shorthands for declaring some common validations right in the field declaration
96
+
97
+ ### Required fields
98
+
99
+ The `:required` argument to a field gives a `validates_presence_of`:
100
+
101
+ >>
102
+ class Advert
103
+ fields do
104
+ title :string, :required, limit: 255
105
+ end
106
+ end
107
+ >> a = Advert.new
108
+ >> a.valid?
109
+ => false
110
+ >> a.errors.full_messages
111
+ => ["Title can't be blank"]
112
+ >> a.id = 2
113
+ >> a.body = "hello"
114
+ >> a.title = "Jimbo"
115
+ >> a.save
116
+ => true
117
+
118
+
119
+ ### Unique fields
120
+
121
+ The `:unique` argument in a field declaration gives `validates_uniqueness_of`:
122
+
123
+ >>
124
+ class Advert
125
+ fields do
126
+ title :string, :unique, limit: 255
127
+ end
128
+ end
129
+ >> a = Advert.new :title => "Jimbo", id: 3, body: "hello"
130
+ >> a.valid?
131
+ => false
132
+ >> a.errors.full_messages
133
+ => ["Title has already been taken"]
134
+ >> a.title = "Sambo"
135
+ >> a.save
136
+ => true
@@ -0,0 +1,76 @@
1
+ # DeclareSchema
2
+
3
+ ## Introduction
4
+
5
+ Welcome to DeclareSchema -- a spin-off from HoboFields part of the Hobo project (Hobo not required!).
6
+
7
+ **DeclareSchema writes your Rails migrations for you! Your migration writing days are over!**
8
+
9
+ All we ask is that you declare your fields in the model. It's still perfectly DRY because you're not having to repeat that in the migration -- DeclareSchema does that for you. In fact, you'll come to love having them there.
10
+
11
+ It still has all the benefits of writing your own migrations, for example if you want to add some special code to migrate your old data, you can just edit the generated migration.
12
+
13
+ ## Example
14
+
15
+ First off, pass the `--skip-migration` option when generating your models:
16
+
17
+ $ rails generate model blog_post --skip-migration
18
+
19
+ Now edit your model as follows:
20
+
21
+ class BlogPost < ActiveRecord::Base
22
+ fields do
23
+ title :string
24
+ body :text
25
+ timestamps
26
+ end
27
+ end
28
+ {: .ruby}
29
+
30
+
31
+ Then, simply run
32
+
33
+ $ rails generate declare_schema:migration
34
+
35
+ And voila
36
+
37
+ ---------- Up Migration ----------
38
+ create_table :blog_posts do |t|
39
+ t.string :title
40
+ t.text :body
41
+ t.datetime :created_at
42
+ t.datetime :updated_at
43
+ end
44
+ ----------------------------------
45
+
46
+ ---------- Down Migration --------
47
+ drop_table :blog_posts
48
+ ----------------------------------
49
+ {: .ruby}
50
+
51
+ The migration generator has created a migration to change from the schema that is currently in your database, to the schema that your models need. That's really all there is to it. You are now free to simply hack away on your app and run the migration generator every time the database needs to play catch-up.
52
+
53
+ Note that the migration generator is interactive -- it can't tell the difference between renaming something vs. adding one thing and removing another, so sometimes it will ask you to clarify. It's a bit picky about what it makes you type in response, because we really don't want you to lose data when someone's amazing twitter distracts you at the wrong moment.
54
+
55
+ ## Installing
56
+
57
+ The simplest and recommended way to install DeclareSchema is as a gem:
58
+
59
+ $ gem install declare_schema
60
+
61
+ ## API
62
+
63
+ ## Migration Generator Details
64
+
65
+ The migration generator doctests provide a lot more detail. They're not really that great as documentation because doctests run in a single irb session, and that doesn't fit well with the concept of a generator. Skip these unless you're really keen to see the details of the migration generator in action
66
+
67
+ - [Migration Generator Details](/manual/declare_schema/migration_generator)
68
+
69
+ ## About Doctests
70
+
71
+ DeclareSchema is documented and tested using *doctests*. This is an idea that comes from Python that we've been experimenting with for Hobo. Whenever you see code-blocks that start "`>>`", read them as IRB sessions. The `rdoctest` tool extracts these and runs them to verify they behave as advertised.
72
+
73
+ Doctests are a great way to get both documentation and tests from the same source. We're still experimenting with exactly how this all works though, so if the docs seem strange in places, please bear with us!
74
+
75
+ You may download rubydoctest via [github](http://www.github.com/tablatom/rubydoctest)
76
+