declare_schema 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.dependabot/config.yml +10 -0
- data/.github/workflows/gem_release.yml +38 -0
- data/.gitignore +14 -0
- data/.jenkins/Jenkinsfile +72 -0
- data/.jenkins/ruby_build_pod.yml +19 -0
- data/.rspec +2 -0
- data/.rubocop.yml +189 -0
- data/.ruby-version +1 -0
- data/Appraisals +14 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +24 -0
- data/Gemfile.lock +203 -0
- data/LICENSE.txt +22 -0
- data/README.md +11 -0
- data/Rakefile +56 -0
- data/bin/declare_schema +11 -0
- data/declare_schema.gemspec +25 -0
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/rails_4.gemfile +25 -0
- data/gemfiles/rails_5.gemfile +25 -0
- data/gemfiles/rails_6.gemfile +25 -0
- data/lib/declare_schema.rb +44 -0
- data/lib/declare_schema/command.rb +65 -0
- data/lib/declare_schema/extensions/active_record/fields_declaration.rb +28 -0
- data/lib/declare_schema/extensions/module.rb +36 -0
- data/lib/declare_schema/field_declaration_dsl.rb +40 -0
- data/lib/declare_schema/model.rb +242 -0
- data/lib/declare_schema/model/field_spec.rb +162 -0
- data/lib/declare_schema/model/index_spec.rb +175 -0
- data/lib/declare_schema/railtie.rb +12 -0
- data/lib/declare_schema/version.rb +5 -0
- data/lib/generators/declare_schema/migration/USAGE +47 -0
- data/lib/generators/declare_schema/migration/migration_generator.rb +184 -0
- data/lib/generators/declare_schema/migration/migrator.rb +567 -0
- data/lib/generators/declare_schema/migration/templates/migration.rb.erb +9 -0
- data/lib/generators/declare_schema/model/USAGE +19 -0
- data/lib/generators/declare_schema/model/model_generator.rb +12 -0
- data/lib/generators/declare_schema/model/templates/model_injection.rb.erb +25 -0
- data/lib/generators/declare_schema/support/eval_template.rb +21 -0
- data/lib/generators/declare_schema/support/model.rb +64 -0
- data/lib/generators/declare_schema/support/thor_shell.rb +39 -0
- data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +28 -0
- data/spec/spec_helper.rb +28 -0
- data/test/api.rdoctest +136 -0
- data/test/doc-only.rdoctest +76 -0
- data/test/generators.rdoctest +60 -0
- data/test/interactive_primary_key.rdoctest +56 -0
- data/test/migration_generator.rdoctest +846 -0
- data/test/migration_generator_comments.rdoctestDISABLED +74 -0
- data/test/prepare_testapp.rb +15 -0
- data/test_responses.txt +2 -0
- metadata +109 -0
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
data/test/api.rdoctest
ADDED
@@ -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
|
+
|