can_be 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +5 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +7 -0
- data/README.md +93 -27
- data/can_be.gemspec +2 -2
- data/gemfiles/3.1.gemfile +7 -0
- data/gemfiles/3.2.gemfile +7 -0
- data/lib/can_be.rb +3 -1
- data/lib/can_be/builder.rb +7 -0
- data/lib/can_be/builder/can_be.rb +106 -0
- data/lib/can_be/builder/can_be_detail.rb +22 -0
- data/lib/can_be/config.rb +33 -0
- data/lib/can_be/model_extensions.rb +13 -1
- data/lib/can_be/processor.rb +7 -0
- data/lib/can_be/processor/instance.rb +61 -0
- data/lib/can_be/processor/klass.rb +36 -0
- data/lib/can_be/version.rb +1 -1
- data/spec/can_be/config_spec.rb +62 -0
- data/spec/can_be/model_extensions_spec.rb +391 -0
- data/spec/support/models.rb +28 -0
- data/spec/support/schema.rb +35 -0
- metadata +28 -8
- data/lib/can_be/initializer.rb +0 -90
- data/spec/can_be/model_additions_spec.rb +0 -173
data/.travis.yml
CHANGED
data/.yardopts
ADDED
data/CHANGELOG.md
ADDED
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
# CanBe [![Build Status](https://secure.travis-ci.org/mstarkman/can_be.png?branch=master)](https://travis-ci.org/mstarkman/can_be)
|
1
|
+
# CanBe [![Build Status](https://secure.travis-ci.org/mstarkman/can_be.png?branch=master)](https://travis-ci.org/mstarkman/can_be) [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/mstarkman/can_be)
|
2
2
|
|
3
|
-
|
3
|
+
CanBe allows you to track the type of your ActiveRecord model in a consistent simple manner. With just a little configuration on your part, each type of record can contain different attributes that are specifc to that type of record. From a data modelling perspective this is preferred over [ActiveRecord STI](http://api.rubyonrails.org/classes/ActiveRecord/Base.html#label-Single+table+inheritance) since you will not have many columns in your database that have null values. Under the hood, CanBe uses one-to-one [Polymorphic Associations](http://guides.rubyonrails.org/association_basics.html#polymorphic-associations) to accomplish the different attributes per type.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
@@ -16,11 +16,11 @@ Or install it yourself as:
|
|
16
16
|
|
17
17
|
$ gem install can_be
|
18
18
|
|
19
|
-
##
|
19
|
+
## Database Configuration (via migrations)
|
20
|
+
|
21
|
+
In its simplest form, you only need to add a string attribute (column) to the model can be different types. By default, this attribute must be named `can_be_type`. However, you can have the attribute be named anything that you would like, you just need to tell CanBe what it is. Indexing this column is your choice.
|
20
22
|
|
21
|
-
|
22
|
-
your model. By default, the `can_be` gem expects your field to be
|
23
|
-
called `can_be_type`. However, this can be changed.
|
23
|
+
Example migration:
|
24
24
|
|
25
25
|
```ruby
|
26
26
|
class AddCanBeTypeToAddresses < ActiveRecord::Migration
|
@@ -31,51 +31,117 @@ class AddCanBeTypeToAddresses < ActiveRecord::Migration
|
|
31
31
|
end
|
32
32
|
```
|
33
33
|
|
34
|
-
|
34
|
+
### Details Configuration
|
35
|
+
|
36
|
+
If you want to store different attributes (columns), there are some more columns that you will need to add to your model, `details_id` and `details_type`. These fields will be used to store the relationships to the details information. Indexing these columns is your choice.
|
37
|
+
|
38
|
+
Example migration:
|
35
39
|
|
36
40
|
```ruby
|
37
|
-
class
|
38
|
-
|
41
|
+
class AddCanBeDetailsToAddresses < ActiveRecord::Migration
|
42
|
+
def change
|
43
|
+
add_column :addresses, :details_id, :integer
|
44
|
+
add_column :addresses, :details_type, :string
|
45
|
+
add_index :addresses, [:details_id, :details_type]
|
46
|
+
end
|
39
47
|
end
|
40
48
|
```
|
41
49
|
|
42
|
-
|
50
|
+
You will also need to create the models that will be used to represent the details attributes for each type. You will need to configure the model to be a details model be calling the `can_be_detail` method in your model. You do not need to specify a details model for each CanBe type if there are not any extra attributes required for that type.
|
43
51
|
|
44
|
-
|
52
|
+
## Model Configuration
|
45
53
|
|
46
|
-
|
54
|
+
To add CanBe to your model, you simply need to call the `can_be` method
|
55
|
+
on your model.
|
47
56
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
57
|
+
```ruby
|
58
|
+
class Address < ActiveRecord::Base
|
59
|
+
can_be :home_address, :work_address, :vacation_address
|
60
|
+
end
|
61
|
+
```
|
52
62
|
|
53
|
-
|
63
|
+
The `can_be` method will take in a list of valid types that will be used by CanBe. There is an optional last parameter that is a hash of the options. This is a list of valid options.
|
54
64
|
|
55
|
-
* `
|
56
|
-
* `
|
57
|
-
* `home_address?` - Returns true if the record is a `home_address` type
|
65
|
+
* `:default_type` - Sets the default value for when a new record is instantiated or created (it is the first value in the list by default)
|
66
|
+
* `:field_name` - Sets the ActiveRecord field name that is to be used (if not specified, CanBe expects a `can_be_type` attribute to be present)
|
58
67
|
|
59
|
-
|
68
|
+
Here is an example of the options.
|
60
69
|
|
61
|
-
|
70
|
+
```ruby
|
71
|
+
class Person < ActiveRecord::Base
|
72
|
+
can_be :male, :female, field_name: :gender, default_type: :female
|
73
|
+
end
|
74
|
+
```
|
62
75
|
|
63
|
-
|
64
|
-
* `field_name` - Sets the ActiveRecord field name that is to be used (by default it expects a `can_be_type` field to be present)
|
76
|
+
### Details Model Configuration
|
65
77
|
|
66
|
-
|
78
|
+
In order to wire up a model to be a CanBe details model, you will need
|
79
|
+
to call the `can_be_detail` method on that model.
|
67
80
|
|
68
81
|
```ruby
|
69
|
-
class
|
70
|
-
|
82
|
+
class HomeAddressDetail < ActiveRecord::Base
|
83
|
+
can_be_detail :address, :home_address
|
71
84
|
end
|
72
85
|
```
|
86
|
+
The `can_be_detail` method take in two parameters.
|
87
|
+
|
88
|
+
The first is the link to the CanBe model. This must be a symbol that will reference the CanBe model. In order to create the proper symbol, you can execute the following into your Rails console: `<ModelName>.name.underscore.to_sym`. Here is an example: `Address.name.underscore.to_sym`. In the above example, this will be used for the `Address` CanBe model.
|
89
|
+
|
90
|
+
The second parameter is the CanBe type that this model is to be used for. In the example above, the `HomeAddressDetail` model will used for the `:home_address` CanBe type.
|
91
|
+
|
92
|
+
## Usage
|
93
|
+
|
94
|
+
The CanBe gem will provide you a lot methods to handle your type processing in an easy and consistent manner.
|
95
|
+
|
96
|
+
### Instantiating New Models
|
97
|
+
|
98
|
+
You can continue to instantiate your CanBe models by using the `new` method. When you do, CanBe will ensure that the type of the record is assigned the detault CanBe type for your model.
|
99
|
+
|
100
|
+
There are also some helper methods put on your model to make it easier to instantiate the type of model that you want. These methods will take the form of `new_<CanBe type>`. For example, you can call `Address.new_home_address`. These methods will take the same parameters as the base `new` method provided by ActiveRecord.
|
101
|
+
|
102
|
+
### Creating New Models
|
103
|
+
|
104
|
+
You can continue to create your CanBe models by using the `create` method. When you do, CanBe will ensure that the type of the record is assigned the detault CanBe type for your model.
|
105
|
+
|
106
|
+
There are also some helper methods put on your model to make it easier to create the type of model that you want. These methods will take the form of `create_<CanBe type>`. For example, you can call `Address.create_home_address`. These methods will take the same parameters as the base `create` method provided by ActiveRecord.
|
107
|
+
|
108
|
+
### Changing CanBe Types
|
109
|
+
|
110
|
+
There are several ways to change the type of record that you are working with. You can access the `can_be_type` attribute (or other attribute if you specified the field to be used) and change the value directly.
|
111
|
+
|
112
|
+
There are also instance methods provided on your model that allow for changing to a specific CanBe type.
|
113
|
+
|
114
|
+
You can change the type of record and not persist it immediately to the database by calling the appropriate `change_to_<CanBe type>` method. For example, you can call `Address.new.change_to_work_address` method to change the record to be of CanBe type `:work_address`.
|
115
|
+
|
116
|
+
If you want to change the type of the record and persist it to the database immediately, you can call the appropriate `change_to_<CanBe type>!` method. For example, this method call will change the type of record to `:work_address` and persist the change to the database: `Address.create.change_to_work_address!`
|
117
|
+
|
118
|
+
There is a validator for the CanBe field, that will unsure that the CanBe field is set to one of the CanBe types before persisting the record.
|
119
|
+
|
120
|
+
NOTE: that when you are changing the type of record the details record will be changed to the correct CanBe details record. New records will only be persisted to the database when the CanBe model is persisted. If you change the CanBe model to a type that does not have a corresponding details model, `nil` will be stored for the details.
|
121
|
+
|
122
|
+
### Boolean Evaluation
|
123
|
+
|
124
|
+
With CanBe, it is easy to determine the type of record that you are working with. This is accomplished by calling the `<CanBe type>?` on the instance of your model. For example if you wanted to see if the `Address` instance you are working with, you would call `Address.first.home_address?` and it would return `true` or `false` depending on the CanBe type of the record.
|
125
|
+
|
126
|
+
### Finding Records
|
127
|
+
|
128
|
+
There are two ways to find specific types of records. You can use the `find_by_can_be_types` method, which takes in a list of the CanBe types that you want to find. For example, if you wanted to find all of the home and work addresses you would call `Address.find_by_can_be_types :home_address, :work_address`.
|
129
|
+
|
130
|
+
Methods are also defined on your CanBe model that will find all of the records for a specific CanBe type. These methods take the form of `<pluralized CanBe type>`. For example, `Address.home_addresses` would return all of the records with a type of `:home_address`.
|
131
|
+
|
132
|
+
### Accessing the Details
|
133
|
+
|
134
|
+
If you want to access the details model, you can call the `details` method on your instance and the instance of your model will be returned. If the type of model that you are using does not have a details model, `nil` will be returned.
|
135
|
+
|
136
|
+
When you persist your CanBe model to the database, your details model will automatically be persisted.
|
73
137
|
|
74
138
|
## Contributing
|
75
139
|
|
76
140
|
1. Fork it
|
77
141
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
78
142
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
143
|
+
* Make sure to include the appropriate specs
|
144
|
+
* Specs can be run by executing the `rake` command in the terminal
|
79
145
|
4. Push to the branch (`git push origin my-new-feature`)
|
80
146
|
5. Create new Pull Request
|
81
147
|
|
data/can_be.gemspec
CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |gem|
|
|
8
8
|
gem.version = CanBe::VERSION
|
9
9
|
gem.authors = ["Mark Starkman"]
|
10
10
|
gem.email = ["mrstarkman@gmail.com"]
|
11
|
-
gem.description = %q{
|
12
|
-
gem.summary = %q{
|
11
|
+
gem.description = %q{CanBe allows you to track the type of your ActiveRecord model in a consistent simple manner. With just a little configuration on your part, each type of record can contain different attributes that are specifc to that type of record.}
|
12
|
+
gem.summary = %q{CanBe allows you to track the type of your ActiveRecord model in a consistent simple manner. With just a little configuration on your part, each type of record can contain different attributes that are specifc to that type of record. From a data modelling perspective this is preferred over ActiveRecord STI since you will not have many columns in your database that have null values. Under the hood, CanBe uses one-to-one Polymorphic Associations to accomplish the different attributes per type.}
|
13
13
|
gem.homepage = ""
|
14
14
|
|
15
15
|
gem.files = `git ls-files`.split($/)
|
data/lib/can_be.rb
CHANGED
@@ -0,0 +1,106 @@
|
|
1
|
+
module CanBe
|
2
|
+
module Builder
|
3
|
+
class CanBe
|
4
|
+
def self.build(klass)
|
5
|
+
new(klass).define_methods
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(klass)
|
9
|
+
@klass = klass
|
10
|
+
end
|
11
|
+
|
12
|
+
def define_methods
|
13
|
+
define_processor
|
14
|
+
define_instance_methods
|
15
|
+
define_class_methods
|
16
|
+
define_validations
|
17
|
+
define_details
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def define_processor
|
22
|
+
@klass.instance_eval do
|
23
|
+
define_method :can_be_processor do
|
24
|
+
@can_be_processor ||= Processor::Instance.new self
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
@klass.class_eval do
|
29
|
+
define_singleton_method :can_be_processor do
|
30
|
+
@can_be_processor ||= Processor::Klass.new self
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def define_instance_methods
|
36
|
+
klass = @klass
|
37
|
+
|
38
|
+
klass.instance_eval do
|
39
|
+
define_method "#{klass.can_be_config.field_name}=" do |value|
|
40
|
+
can_be_processor.field_value = value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
klass.can_be_config.types.each do |t|
|
45
|
+
klass.instance_eval do
|
46
|
+
define_method "#{t}?" do
|
47
|
+
can_be_processor.boolean_eval(t)
|
48
|
+
end
|
49
|
+
|
50
|
+
define_method "change_to_#{t}" do
|
51
|
+
can_be_processor.update_field(t)
|
52
|
+
end
|
53
|
+
|
54
|
+
define_method "change_to_#{t}!" do
|
55
|
+
can_be_processor.update_field(t, true)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def define_class_methods
|
62
|
+
@klass.class_eval do
|
63
|
+
define_singleton_method :find_by_can_be_types do |*types|
|
64
|
+
can_be_processor.find_by_types(*types)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
@klass.can_be_config.types.each do |t|
|
69
|
+
@klass.class_eval do
|
70
|
+
define_singleton_method "create_#{t}" do |*args, &block|
|
71
|
+
can_be_processor.create(t, *args, &block)
|
72
|
+
end
|
73
|
+
|
74
|
+
define_singleton_method "new_#{t}" do |*args, &block|
|
75
|
+
can_be_processor.instantiate(t, *args, &block)
|
76
|
+
end
|
77
|
+
|
78
|
+
define_singleton_method t.pluralize.to_sym do
|
79
|
+
can_be_processor.find_by_types t
|
80
|
+
end
|
81
|
+
|
82
|
+
after_initialize do |model|
|
83
|
+
model.can_be_processor.set_default_field_value
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def define_validations
|
90
|
+
@klass.class_eval do
|
91
|
+
validates_inclusion_of self.can_be_config.field_name.to_sym, in: self.can_be_config.types
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def define_details
|
96
|
+
@klass.class_eval do
|
97
|
+
belongs_to :details, polymorphic: true, autosave: true, dependent: :destroy
|
98
|
+
|
99
|
+
after_initialize do |model|
|
100
|
+
model.can_be_processor.initialize_details
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module CanBe
|
2
|
+
module Builder
|
3
|
+
class CanBeDetail
|
4
|
+
def self.build(klass, can_be_model)
|
5
|
+
new(klass, can_be_model).define_association
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(klass, can_be_model)
|
9
|
+
@klass = klass
|
10
|
+
@can_be_model = can_be_model
|
11
|
+
end
|
12
|
+
|
13
|
+
def define_association
|
14
|
+
can_be_model = @can_be_model
|
15
|
+
|
16
|
+
@klass.class_eval do
|
17
|
+
has_one can_be_model, as: :details, dependent: :destroy
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module CanBe
|
2
|
+
class Config
|
3
|
+
DEFAULT_CAN_BE_FIELD = :can_be_type
|
4
|
+
|
5
|
+
attr_reader :types
|
6
|
+
|
7
|
+
def field_name
|
8
|
+
@field_name || CanBe::Config::DEFAULT_CAN_BE_FIELD
|
9
|
+
end
|
10
|
+
|
11
|
+
def types=(types)
|
12
|
+
@types = types.map(&:to_s)
|
13
|
+
end
|
14
|
+
|
15
|
+
def default_type
|
16
|
+
@default_type || @types.first
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse_options(options = {})
|
20
|
+
@default_type = options[:default_type].to_s
|
21
|
+
@field_name = options[:field_name]
|
22
|
+
end
|
23
|
+
|
24
|
+
def details
|
25
|
+
@details ||= {}
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.add_detail_model(klass, can_be_class, can_be_type)
|
29
|
+
config = can_be_class.to_s.camelize.constantize.can_be_config
|
30
|
+
config.details[can_be_type] = klass.name
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -5,13 +5,25 @@ module CanBe
|
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
7
|
module ClassMethods
|
8
|
+
def can_be_config
|
9
|
+
@can_be_config ||= CanBe::Config.new
|
10
|
+
end
|
11
|
+
|
8
12
|
def can_be(*types)
|
9
13
|
if types.last.is_a?(Hash)
|
10
14
|
options = types.last
|
11
15
|
types.delete types.last
|
12
16
|
end
|
13
17
|
|
14
|
-
|
18
|
+
can_be_config.types = types
|
19
|
+
can_be_config.parse_options options if options
|
20
|
+
|
21
|
+
CanBe::Builder::CanBe.build(self)
|
22
|
+
end
|
23
|
+
|
24
|
+
def can_be_detail(can_be_model, can_be_type)
|
25
|
+
CanBe::Config.add_detail_model self, can_be_model, can_be_type
|
26
|
+
CanBe::Builder::CanBeDetail.build(self, can_be_model)
|
15
27
|
end
|
16
28
|
end
|
17
29
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module CanBe
|
2
|
+
module Processor
|
3
|
+
class Instance
|
4
|
+
def initialize(model)
|
5
|
+
@model = model
|
6
|
+
@config = model.class.can_be_config
|
7
|
+
@field_name = @config.field_name
|
8
|
+
end
|
9
|
+
|
10
|
+
def boolean_eval(t)
|
11
|
+
field_value == t
|
12
|
+
end
|
13
|
+
|
14
|
+
def update_field(t, save = false)
|
15
|
+
if save
|
16
|
+
original_details = @model.details
|
17
|
+
@model.update_attributes(@field_name => t)
|
18
|
+
original_details.destroy unless original_details.class == @model.details.class
|
19
|
+
else
|
20
|
+
self.field_value = t
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def field_value=(t)
|
25
|
+
set_details(t)
|
26
|
+
@model.send(:write_attribute, @field_name, t)
|
27
|
+
end
|
28
|
+
|
29
|
+
def field_value
|
30
|
+
@model.read_attribute(@field_name)
|
31
|
+
end
|
32
|
+
|
33
|
+
def set_default_field_value
|
34
|
+
self.field_value = @config.default_type if self.field_value.nil?
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize_details
|
38
|
+
set_details(field_value.to_sym) if has_details? && !@model.details_id
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def has_details?
|
43
|
+
@model.respond_to?(:details) && @model.respond_to?(:details_id) && @model.respond_to?(:details_type)
|
44
|
+
end
|
45
|
+
|
46
|
+
def set_details(t)
|
47
|
+
return unless has_details?
|
48
|
+
|
49
|
+
classname = @config.details[t.to_sym]
|
50
|
+
|
51
|
+
if classname
|
52
|
+
@model.details = classname.constantize.new
|
53
|
+
else
|
54
|
+
@model.details_id = nil
|
55
|
+
@model.details_type = nil
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|