object_state 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b954a8472c703394a859740677330050ca02f5af
4
+ data.tar.gz: a501a9e2030d067cc173c1b9cbd93cd2e41fc0ef
5
+ SHA512:
6
+ metadata.gz: 3b6ec2625a2c7edec5cf64e22470827ef9f868ae0e823466ec099dfb8b9459d1c5ee793da5654c2ca0da7cbaad51e87a182903bfaf875bf31e09644e89a43bb5
7
+ data.tar.gz: 2f37413a9e6f788bf0364c70cfe1d186f6d80b51df9b88baad7099ecb59520b985b6f30acfb637891f14cb276d883e1f712d63275c512312d5434ab9909dccbe
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,13 @@
1
+ sudo: false
2
+ language: ruby
3
+ script: 'bundle exec rake'
4
+ rvm:
5
+ - 2.2.5
6
+ before_install:
7
+ - gem install bundler -v 1.12.5
8
+ notifications:
9
+ email:
10
+ recipients:
11
+ - tomas.celizna@gmail.com
12
+ on_failure: change
13
+ on_success: never
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in object_state.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard :minitest do
2
+ watch(%r{^lib/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" }
3
+ watch(%r{^test/.+_test\.rb$})
4
+ watch(%r{^test/test_helper\.rb$}) { 'test' }
5
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Tomas Celizna
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # Object State
2
+
3
+ [![Build Status](https://travis-ci.org/tomasc/object_state.svg)](https://travis-ci.org/tomasc/object_state) [![Gem Version](https://badge.fury.io/rb/object_state.svg)](http://badge.fury.io/rb/object_state) [![Coverage Status](https://img.shields.io/coveralls/tomasc/object_state.svg)](https://coveralls.io/r/tomasc/object_state)
4
+
5
+ This gem helps to encapsulate state of objects of (typically) Ruby on Rails applications.
6
+
7
+ * The state becomes easy to identify in the source code.
8
+ * There is a standard method for converting the state to and from query params.
9
+ * The state can be expressed in the form of a `cache_key`.
10
+ * It is possible to additionally typecast and/or validate its attributes.
11
+
12
+ Useful for pagination, filtering etc.
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem 'object_state'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ ```
25
+ $ bundle
26
+ ```
27
+
28
+ Or install it yourself as:
29
+
30
+ ```
31
+ $ gem install object_state
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ### Encapsulate object's state
37
+
38
+ ```ruby
39
+ class MyObj
40
+ include ObjectState::Owner
41
+
42
+ object_state do
43
+ attr_accessor :current_date
44
+ end
45
+ end
46
+ ```
47
+
48
+ ### Export state hash
49
+
50
+ ```ruby
51
+ my_obj.to_object_state_hash # => { my_obj => { id: "123", current_date: "2016-08-27" } }
52
+ ```
53
+
54
+ This hash can be easily used as query params, for example:
55
+
56
+ ```ruby
57
+ my_obj_path(my_obj, my_obj.to_object_state_hash)
58
+ ```
59
+
60
+ An attribute can be easily overridden:
61
+
62
+ ```ruby
63
+ my_obj_path(my_obj, my_obj.to_object_state_hash(current_date: Date.tomorow))
64
+ ```
65
+
66
+ ### Assign values from state hash
67
+
68
+ ```ruby
69
+ my_obj.assign_attributes_from_object_state_hash(…)
70
+ ```
71
+
72
+ The attributes will be assigned only if the id in the state hash matches the id of your object. This is helpful for example when used in controllers.
73
+
74
+ ## Supported attribute definitions
75
+
76
+ * PoRo (`attr_accessor`)
77
+ * Mongoid fields
78
+ * Virtus attributes
79
+
80
+ ## Advanced usage
81
+
82
+ Optionally the state can be processed by a custom class. This is useful when the values need to be typecast, validated, or transformed. The `ObjectState::State` includes `ActiveModel::Model` and `Virtus` so you can use [Virtus' attribute definition](https://github.com/solnic/virtus) and [ActiveModel validations](http://api.rubyonrails.org/classes/ActiveModel/Validations.html). For example:
83
+
84
+ ```ruby
85
+ class MyObj::State < ObjectState::State
86
+ attribute :current_date, Date
87
+
88
+ validates :current_date, inclusion: { in: Date.today..Date.tomorrow }, presence: true
89
+ end
90
+ ```
91
+
92
+ Only valid values will be assigned to your object.
93
+
94
+ ```ruby
95
+ class MyObj
96
+ include ObjectState::Owner
97
+
98
+ object_state class_name: 'MyObj::State' do
99
+ attr_accessor :current_date
100
+ end
101
+ end
102
+ ```
103
+
104
+ ## Cache
105
+
106
+ Often values or views associated with the object need to be cached (and the cache expired) depending on its state. The `:object_state_cache_key` generates a cache key based on the state's values. For example the `MyObj` from the above example:
107
+
108
+ ```ruby
109
+ my_obj.object_state_cache_key # => '2016-08-27'
110
+ ```
111
+
112
+ In fact the `object_state` method automatically merges the state object's cache key with the object's cache_key:
113
+
114
+ ```ruby
115
+ my_obj.cache_key # => '<object-cache-key>/2016-08-27'
116
+ ```
117
+
118
+ This can be disabled as follows:
119
+
120
+ ```ruby
121
+ class MyObj
122
+ include ObjectState::Owner
123
+
124
+ object_state merge_cache_key: false do
125
+ attr_accessor :current_date
126
+ end
127
+ end
128
+ ```
129
+
130
+ ## Development
131
+
132
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
133
+
134
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
135
+
136
+ ## Contributing
137
+
138
+ Bug reports and pull requests are welcome on GitHub at <https://github.com/tomasc/object_state>.
139
+
140
+ ## License
141
+
142
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "object_state"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,45 @@
1
+ require 'active_support/concern'
2
+
3
+ module ObjectState
4
+ module Owner
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ def object_state_class
9
+ @object_state_class ||= Class.new(ObjectState::State)
10
+ end
11
+
12
+ def object_state(options = {}, &block)
13
+ cls = self
14
+ class_name = options.fetch(:class_name, nil)
15
+ @object_state_class ||= class_name ? class_name.constantize : Class.new(ObjectState::State)
16
+ @object_state_class.class_eval do
17
+ setup_attributes cls, &block
18
+ end
19
+
20
+ if instance_methods.include?(:cache_key) && options.fetch(:merge_cache_key, true)
21
+ define_method :cache_key do |*args|
22
+ return super(*args) unless object_state
23
+ [super(*args), object_state.cache_key].reject(&:blank?).join('/')
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ def object_state(attrs = {})
30
+ self.class.object_state_class.new(self, attrs)
31
+ end
32
+
33
+ def to_object_state_hash(attrs = {})
34
+ object_state(attrs).try(:to_hash)
35
+ end
36
+
37
+ def assign_attributes_from_state_hash(attrs)
38
+ key = respond_to?(:model_name) ? model_name.singular : self.class.to_s.underscore
39
+ return unless model_attrs = attrs.stringify_keys[key]
40
+ model_id = model_attrs.stringify_keys['id'] || model_attrs.stringify_keys['_id']
41
+ return if respond_to?(:id) && model_id.to_s != id.to_s
42
+ object_state(model_attrs.except(*%i(_id id))).try(:update_model!)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,104 @@
1
+ require 'active_model'
2
+ require 'virtus'
3
+
4
+ module ObjectState
5
+ class State
6
+ class UnsupportedAttributeType < StandardError; end
7
+
8
+ include ActiveModel::Model
9
+ include ActiveSupport::Rescuable
10
+ include Virtus.model(nullify_blank: true)
11
+
12
+ attr_accessor :model
13
+
14
+ validates :model, presence: true
15
+
16
+ def self.attribute_names
17
+ attribute_set.map(&:name)
18
+ end
19
+
20
+ def self.setup_attributes(owner_class, &block)
21
+ temp_class = Class.new(Object)
22
+
23
+ # override attr_accessor to collect attr_accessor :names
24
+ temp_class.define_singleton_method :attr_accessor do |*names|
25
+ @attr_accessor_names ||= []
26
+ @attr_accessor_names.concat Array(names)
27
+ super(*names)
28
+ end
29
+
30
+ temp_class.define_singleton_method :attr_accessor_names do
31
+ Array(@attr_accessor_names)
32
+ end
33
+
34
+ is_mongoid_document = !!defined?(::Mongoid::Document) && owner_class.ancestors.include?(::Mongoid::Document)
35
+ is_virtus_model = owner_class.respond_to?(:attribute_set) # FIXME: is there a better way to check for virtus?
36
+
37
+ temp_class.class_eval do
38
+ include ::Mongoid::Document if is_mongoid_document
39
+ include Virtus.model if is_virtus_model
40
+ instance_eval(&block)
41
+ end
42
+
43
+ # setup attributes for :attr_accessor names
44
+ excluded_attr_accessor_names = %i(validation_context _index before_callback_halted)
45
+ (temp_class.attr_accessor_names - excluded_attr_accessor_names).each do |name|
46
+ next if attribute_set.map(&:name).include?(name.to_sym)
47
+ attribute name
48
+ end
49
+
50
+ # setup attributes for Mongoid fields
51
+ if is_mongoid_document
52
+ temp_class.fields.except(*%w(_id)).each do |name, field|
53
+ next if attribute_set.map(&:name).include?(name.to_sym)
54
+ attribute name, field.type
55
+ end
56
+ end
57
+
58
+ # setup attributes for Virtus attributes
59
+ if is_virtus_model
60
+ temp_class.attribute_set.each do |att|
61
+ next if attribute_set.map(&:name).include?(att.name.to_sym)
62
+ attribute att.name, att.type
63
+ end
64
+ end
65
+
66
+ owner_class.class_eval(&block)
67
+ end
68
+
69
+ def initialize(model, attrs = {})
70
+ self.model = model
71
+ super attributes_from_model.merge(attrs)
72
+ end
73
+
74
+ def update_model!(options = {})
75
+ return unless model.present?
76
+ return unless valid? || options[:skip_validations] == true
77
+ attributes.except(:id).each do |name, value|
78
+ model.send("#{name}=", value) if model.respond_to?(name)
79
+ end
80
+ end
81
+
82
+ def to_hash(attrs = {})
83
+ return unless model.present?
84
+ key = model.respond_to?(:model_name) ? model.model_name.singular : model.class.to_s.underscore
85
+ value = super()
86
+ value = value.merge(id: model.id) if model.respond_to?(:id)
87
+ value = value.merge(attrs)
88
+ { key => value }
89
+ end
90
+
91
+ def cache_key
92
+ attributes.values.flatten.map(&:to_s).reject(&:blank?).join('/')
93
+ end
94
+
95
+ private
96
+
97
+ def attributes_from_model
98
+ self.class.attribute_names.each_with_object({}) do |name, res|
99
+ res[name] = model.send(name) if model.respond_to?(name)
100
+ res
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,3 @@
1
+ module ObjectState
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1,4 @@
1
+ require 'object_state/owner'
2
+ require 'object_state/state'
3
+
4
+ require 'object_state/version'
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'object_state/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'object_state'
8
+ spec.version = ObjectState::VERSION
9
+ spec.authors = ['Tomas Celizna']
10
+ spec.email = ['tomas.celizna@gmail.com']
11
+
12
+ spec.summary = "Encapsulates object's state, converts from/to params and to cache_key."
13
+ spec.homepage = 'https://github.com/tomasc/object_state'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = 'exe'
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'activemodel', '>= 4', '<= 5'
22
+ spec.add_dependency 'activesupport', '>= 4', '<= 5'
23
+ spec.add_dependency 'virtus'
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1.12'
26
+ spec.add_development_dependency 'guard'
27
+ spec.add_development_dependency 'guard-minitest'
28
+ spec.add_development_dependency 'minitest', '~> 5.0'
29
+ spec.add_development_dependency 'mongoid', '>= 5.0', '< 6'
30
+ spec.add_development_dependency 'rake', '~> 10.0'
31
+ end
metadata ADDED
@@ -0,0 +1,202 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: object_state
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tomas Celizna
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-09-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activemodel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '4'
20
+ - - "<="
21
+ - !ruby/object:Gem::Version
22
+ version: '5'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '4'
30
+ - - "<="
31
+ - !ruby/object:Gem::Version
32
+ version: '5'
33
+ - !ruby/object:Gem::Dependency
34
+ name: activesupport
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '4'
40
+ - - "<="
41
+ - !ruby/object:Gem::Version
42
+ version: '5'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '4'
50
+ - - "<="
51
+ - !ruby/object:Gem::Version
52
+ version: '5'
53
+ - !ruby/object:Gem::Dependency
54
+ name: virtus
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ type: :runtime
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ - !ruby/object:Gem::Dependency
68
+ name: bundler
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '1.12'
74
+ type: :development
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '1.12'
81
+ - !ruby/object:Gem::Dependency
82
+ name: guard
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ - !ruby/object:Gem::Dependency
96
+ name: guard-minitest
97
+ requirement: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ - !ruby/object:Gem::Dependency
110
+ name: minitest
111
+ requirement: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - "~>"
114
+ - !ruby/object:Gem::Version
115
+ version: '5.0'
116
+ type: :development
117
+ prerelease: false
118
+ version_requirements: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - "~>"
121
+ - !ruby/object:Gem::Version
122
+ version: '5.0'
123
+ - !ruby/object:Gem::Dependency
124
+ name: mongoid
125
+ requirement: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '5.0'
130
+ - - "<"
131
+ - !ruby/object:Gem::Version
132
+ version: '6'
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '5.0'
140
+ - - "<"
141
+ - !ruby/object:Gem::Version
142
+ version: '6'
143
+ - !ruby/object:Gem::Dependency
144
+ name: rake
145
+ requirement: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - "~>"
148
+ - !ruby/object:Gem::Version
149
+ version: '10.0'
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - "~>"
155
+ - !ruby/object:Gem::Version
156
+ version: '10.0'
157
+ description:
158
+ email:
159
+ - tomas.celizna@gmail.com
160
+ executables: []
161
+ extensions: []
162
+ extra_rdoc_files: []
163
+ files:
164
+ - ".gitignore"
165
+ - ".travis.yml"
166
+ - Gemfile
167
+ - Guardfile
168
+ - LICENSE.txt
169
+ - README.md
170
+ - Rakefile
171
+ - bin/console
172
+ - bin/setup
173
+ - lib/object_state.rb
174
+ - lib/object_state/owner.rb
175
+ - lib/object_state/state.rb
176
+ - lib/object_state/version.rb
177
+ - object_state.gemspec
178
+ homepage: https://github.com/tomasc/object_state
179
+ licenses:
180
+ - MIT
181
+ metadata: {}
182
+ post_install_message:
183
+ rdoc_options: []
184
+ require_paths:
185
+ - lib
186
+ required_ruby_version: !ruby/object:Gem::Requirement
187
+ requirements:
188
+ - - ">="
189
+ - !ruby/object:Gem::Version
190
+ version: '0'
191
+ required_rubygems_version: !ruby/object:Gem::Requirement
192
+ requirements:
193
+ - - ">="
194
+ - !ruby/object:Gem::Version
195
+ version: '0'
196
+ requirements: []
197
+ rubyforge_project:
198
+ rubygems_version: 2.4.5.1
199
+ signing_key:
200
+ specification_version: 4
201
+ summary: Encapsulates object's state, converts from/to params and to cache_key.
202
+ test_files: []