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 CHANGED
@@ -1,4 +1,9 @@
1
1
  language: ruby
2
+
2
3
  rvm:
3
4
  - 1.9.3
4
5
  - 1.9.2
6
+
7
+ gemfile:
8
+ - gemfiles/3.1.gemfile
9
+ - gemfiles/3.2.gemfile
data/.yardopts ADDED
@@ -0,0 +1,4 @@
1
+ LICENSE.txt
2
+ README.md
3
+ CHANGELOG.md
4
+ lib/**/*.rb
data/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ ## Version 0.1.0
2
+
3
+ Initial release.
4
+
5
+ ## Version 0.2.0
6
+
7
+ Added details functionality for storing custom fields per type.
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
- Adds helper methods to your Active Record models to control the type of record.
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
- ## Usage
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
- Before adding it to your model, you will need to add a database field to
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
- Once you've installed the gem, you will have access to the `can_be` method in your models. The `can_be` method will take a list of types (as symbols) that your model can be represented as.
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 Address < ActiveRecord::Base
38
- can_be :home_address, :work_address, :vacation_address
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
- ### Methods Added to your model
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
- The following methods will then be added to your model for each of the types based on the example above.
52
+ ## Model Configuration
45
53
 
46
- #### Class Methods
54
+ To add CanBe to your model, you simply need to call the `can_be` method
55
+ on your model.
47
56
 
48
- * `create_home_address` - Creates a new address model in the database with the type of `home_address`
49
- * `find_by_can_be_types` - Accepts a list of types and returns the results of the database query (note: this is only added once, not for each type)
50
- * `home_addresses` - Returns a list of the records with a type of `home_address`
51
- * `new_home_address` - Instantiates a new address model instance with the type of `home_address`
57
+ ```ruby
58
+ class Address < ActiveRecord::Base
59
+ can_be :home_address, :work_address, :vacation_address
60
+ end
61
+ ```
52
62
 
53
- #### Instance Methods
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
- * `change_to_home_address` - Changes the record to a `home_address` type (does not save it to the database)
56
- * `change_to_home_address!` - Changes the record to a `home_address` type and saves it to the database
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
- ### Options
68
+ Here is an example of the options.
60
69
 
61
- The following options can be added to the `can_be` method call.
70
+ ```ruby
71
+ class Person < ActiveRecord::Base
72
+ can_be :male, :female, field_name: :gender, default_type: :female
73
+ end
74
+ ```
62
75
 
63
- * `default_type` - Sets the default value for when a new record is instantiated (it is the first value in the list by default)
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
- Here is an example.
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 Person < ActiveRecord::Base
70
- can_be :male, :female, field_name: :gender, default_type: :female
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{Adds helper methods to your Active Record models to control the type of record.}
12
- gem.summary = %q{Adds helper methods to your Active Record models to control the type of record.}
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($/)
@@ -0,0 +1,7 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "activerecord", "~> 3.1.0"
4
+ gem "activesupport", "~> 3.1.0"
5
+
6
+ gemspec :path=>"../"
7
+
@@ -0,0 +1,7 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "activerecord", "~> 3.2.0"
4
+ gem "activesupport", "~> 3.2.0"
5
+
6
+ gemspec :path=>"../"
7
+
data/lib/can_be.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  require "can_be/version"
2
- require "can_be/initializer"
2
+ require "can_be/config"
3
+ require "can_be/processor"
4
+ require "can_be/builder"
3
5
  require "can_be/model_extensions"
4
6
  require "can_be/railtie" if defined? Rails
5
7
 
@@ -0,0 +1,7 @@
1
+ require 'can_be/builder/can_be'
2
+ require 'can_be/builder/can_be_detail'
3
+
4
+ module CanBe
5
+ module Builder
6
+ end
7
+ end
@@ -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
- CanBe::Initializer.new(self, types.map(&:to_s), options).define_methods
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,7 @@
1
+ require 'can_be/processor/klass'
2
+ require 'can_be/processor/instance'
3
+
4
+ module CanBe
5
+ module Processor
6
+ end
7
+ 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
+