attribute_access_controllable 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ lib/bundler/man
11
+ pkg
12
+ rdoc
13
+ spec/reports
14
+ test/tmp
15
+ test/version_tmp
16
+ tmp
data/.pairs ADDED
@@ -0,0 +1,7 @@
1
+ pairs:
2
+ cb: Chris Barretto; chris
3
+ km: Ken Mayer; ken@bitwrangler.com
4
+ email:
5
+ prefix: pair
6
+ domain: socialchorus.com
7
+ no_solo_prefix: true
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in attribute_access_controllable.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Social Chorus
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # AttributeAccessControllable
2
+
3
+ This gem allows you to control write access at the _attribute_ level, on a per-instance basis. For example, let's say you had a model `Person` that has an attribute, `birthday`, which, for security purposes, once this attribute is set, cannot be changed again (except, perhaps by an administrator with extraordinary privileges). You would want any attempts to change this value to raise a validation error.
4
+
5
+ e.g.
6
+
7
+ alice = Person.create(:birthday => '12/12/12')
8
+ => <Person...>
9
+ alice.birthday= '1/1/01'
10
+ => '1/1/01'
11
+ alice.save!
12
+ => ActiveRecord::RecordInvalid ... "birthday is invalid: birthday is read_only"
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ gem 'attribute_access_controllable'
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install attribute_access_controllable
27
+
28
+ ## Usage
29
+
30
+ Generate a migration for your class
31
+
32
+ $ rails generate attribute_access Person
33
+ $ rake db:migrate
34
+
35
+ In your model, add the following:
36
+
37
+ class Person < ActiveRecord::Base
38
+ include AttributeAccessControllable
39
+
40
+ Add hooks to mark attributes as read_only:
41
+
42
+ before_save :mark_birthday_read_only
43
+
44
+ private
45
+
46
+ def mark_birthday_read_only
47
+ attr_read_only(:birthday)
48
+ end
49
+
50
+ If, in the future, you need by-pass the validations, pass `:skip_read_only => true` to the instance's `save` or `save!` methods.
51
+
52
+ ## RSpec support
53
+
54
+ Adding this to your model spec will exercise the feature.
55
+
56
+ require 'attribute_access_controllable/spec_support'
57
+
58
+ describe Person do
59
+ it_should_behave_like "it has AttributeAccessControllable", :test_column
60
+ end
61
+
62
+ ## Contributing
63
+
64
+ 1. Fork it
65
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
66
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
67
+ 4. Push to the branch (`git push origin my-new-feature`)
68
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require 'rspec/core/rake_task'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :spec
7
+
8
+ desc "Run all specs"
9
+ RSpec::Core::RakeTask.new do |t|
10
+ t.pattern = 'spec/**/*_spec.rb'
11
+ t.rspec_opts = ["-c", "-f progress"]
12
+ end
@@ -0,0 +1,35 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/attribute_access_controllable/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Social Chorus", "Kenneth Mayer"]
6
+ gem.email = ["tech@socialchorus.com", "kmayer@bitwrangler.com"]
7
+ gem.description = %q{Allow attribute-level read/write access control, per instance, of any ActiveModel class.}
8
+ gem.summary = <<'EOT'
9
+ This gem allows you to control write access at the _attribute_ level, on a
10
+ per-instance basis. For example, let's say you had a model `Person` that has
11
+ an attribute, `birthday`, which, for security purposes, once this attribute is
12
+ set, cannot be changed again (except, perhaps by an administrator with
13
+ extraordinary privileges). Any attempts to change this value to
14
+ raise a validation error.
15
+ EOT
16
+ gem.homepage = "https://github.com/halogenguides/Attribute-Access-Controllable"
17
+
18
+ gem.files = `git ls-files`.split($\)
19
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
20
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
21
+ gem.name = "attribute_access_controllable"
22
+ gem.require_paths = ["lib"]
23
+ gem.version = AttributeAccessControllable::VERSION
24
+
25
+ gem.add_runtime_dependency('activesupport', ['~> 3.2.1'])
26
+ gem.add_runtime_dependency('activemodel', ['~> 3.2.1'])
27
+ gem.add_runtime_dependency('railties', ['~> 3.2.1'])
28
+
29
+ gem.add_development_dependency('rspec', ['~> 2.8.0'])
30
+ gem.add_development_dependency('pivotal_git_scripts')
31
+ gem.add_development_dependency('informal')
32
+ gem.add_development_dependency('activerecord')
33
+ gem.add_development_dependency('aruba')
34
+ gem.add_development_dependency('ammeter')
35
+ end
data/cucumber.yml ADDED
@@ -0,0 +1 @@
1
+ default: --tags ~@slow_process --no-source
data/doc/blog-post.md ADDED
@@ -0,0 +1,212 @@
1
+ # Read-only access at attribute granularity
2
+
3
+ One of the many pleasures of working at Pivotal Labs is that we are encouraged to release some of our work as open source. Often during the course of our engagements, we write code that might have wide-spread use. Due to the nature of our contracts, we can not unilaterally release such code. Those rights belong to the client. And rightly so. So, it is an even greater pleasure when one of our clients believes in "giving back" to the community, as well.
4
+
5
+ One such example is this modest gem, `attribute_access_controllable` which allows you to set read-only access at the _attribute_ level, on a per-instance basis. For example, let's say that you have a model `Person` with an attribute `birthday`, which, for security purposes, cannot be changed once this attribute is set (except, perhaps, by an administrator with extraordinary privileges). Any future attempts to change this attribute will result in a validation error.
6
+
7
+ e.g.
8
+
9
+ > alice = Person.new(:birthday => '12/12/12')
10
+ => #<Person id: nil, attr1: nil, created_at: nil, updated_at: nil, read_only_attributes: nil, birthday: "0012-12-12">
11
+ > alice.attr_read_only(:birthday)
12
+ => #<Set: {"birthday"}>
13
+ > alice.save!
14
+ => true
15
+ > alice.birthday = "2012-12-12"
16
+ => "2012-12-12"
17
+ > alice.save!
18
+ ActiveRecord::RecordInvalid: Validation failed: Birthday is invalid, Birthday is read_only
19
+ > alice.save!(:skip_read_only => true)
20
+ => true
21
+
22
+ Setting this up is trivial, thanks to a Rails generator which does most of the heavy lifting for you.
23
+
24
+ rails generate attribute_access Person
25
+
26
+ After that, you need only know about one new method added to your class:
27
+
28
+ #attr_read_only(*attributes) # Marks attributes as read-only
29
+
30
+ There are a few others, but this one, plus the new functionality added to `#save` and `#save!` will get you quite far.
31
+
32
+ And if that's all that you were looking for when you stumbled across this article, then there's no need to read any further. Go install the gem and have fun (and may your tests be green when you expect them to be).
33
+
34
+ ## From customer requirements to releasable gem
35
+
36
+ On the other hand, if you are interested in how we got from the original customer story to a releasable open sourced gem, read on. The source code for the module is a [mere 34 lines long](https://github.com/halogenguides/Attribute-Access-Controllable/blob/master/lib/attribute_access_controllable.rb). It implements 2 new methods, a validator and (gently) overrides `#save` and `#save!`. Being good Test Driven Developers, we wrote our specs first, and since we wanted this behavior to be included in several models, we wrote our specs as a [_shared behavior_](https://github.com/halogenguides/Attribute-Access-Controllable/blob/master/lib/attribute_access_controllable/spec_support/shared_examples.rb) as well. The spec clocks in at 44 lines, slightly longer than our implementation. All in all, tiny. The whole commit was less than 100 lines of code.
37
+
38
+ AttributeAccessControllable
39
+ it should behave like it has AttributeAccessControllable
40
+ #attr_read_only(:attribute, ...) marks an attribute as read-only
41
+ #read_only_attribute?(:attribute) returns true when marked read-only
42
+ #read_only_attribute?(:attribute) returns false when not marked read-only (or not marked at all)
43
+ #save! raises error when :attribute is read-only
44
+ #save!(:context => :skip_read_only) is okay
45
+ #save is invalid when :attribute is read-only
46
+ #save(:context => :skip_read_only) is okay
47
+
48
+ In order to get to something "releasable" we needed a few more things, which we put on our To-Do list:
49
+
50
+ #### To do
51
+ 1. MIT License
52
+ 2. A gem specification
53
+ 3. Basic documentation in a README file
54
+
55
+ The list got longer as we fleshed out both the documentation and the integration tests, as you'll see in a moment, but first, let's talk about
56
+
57
+ ### Getting the legal issues resolved, easily and quickly
58
+
59
+ Pivotal's open sourcing policy is straightforward and simple to execute; We don't touch it. We write code for our clients, it's their code to do with as they please. My particular client liked the work we did for them and thought it would make a great open source gem. The Director of Engineering signed off on the idea and I paired with him to create the github repository during a lunch break. The first commit was tiny, just a basic directory structure and the existing code. I don't think the tests passed because they lacked a proper RSpec infrastructure.
60
+
61
+ ### Creating the gem
62
+
63
+ bundler gem DIRECTORY
64
+
65
+ is your best friend. It set up the layout for us, including an MIT License and a gem specification. It had a boilerplate README, too.
66
+
67
+ ### Writing the documentation for the code you wished you had
68
+
69
+ Next, we wrote a draft of the README file which documented what we knew: You needed a migration to create a column called `:read_only_attributes` and you needed to include the module into the class. Then we started thinking about the pain points of using our code as is. Wouldn't it be nice if we could create the migration automatically? Rails generators do that sort of thing, how hard could it be? (Famous last words...) It became clear that we needed to test drive out some new features of the _gem_ that supported the actual _module_.
70
+
71
+ #### To do
72
+ 1. ~~MIT License~~
73
+ 2. ~~A gem specification~~
74
+ 3. ~~Basic documentation in a README file~~
75
+ 4. Integration test
76
+
77
+ ### I am not a big cucumber fan, but...
78
+
79
+ Really, I'm not. I used to write Cucumber features all the time, but nowadays, I use a combination of RSpec and Capybara to get most of my day-to-day integration testing done. There is, however, one sweet spot for Cucumber that I'm finding more and more useful; A very high-level document that describes essential features in a way that a reader will say, "Ahhh, so _that_ is how it is supposed to work!" Here's a copy of the spec I wrote:
80
+
81
+ Feature: Read only attributes
82
+
83
+ Scenario: In a simple rails application
84
+ Given a new rails application
85
+ And I generate a new migration for the class "Person"
86
+ And I generate an attribute access migration for the class "Person"
87
+ And I have a test that exercises read-only
88
+ When I run `rake spec`
89
+ Then the output should contain "7 examples, 0 failures"
90
+
91
+ You probably won't find any web-steps out there to handle these lines. I use [Aruba](https://github.com/cucumber/aruba) to handle the dirty work of executing shell commands in a safe sandbox-y way. The [step definition file](https://github.com/halogenguides/Attribute-Access-Controllable/blob/master/features/step_definitions/steps.rb) hides most of the ugliness. Even so, most readers could figure out what to do, by hand, for each step.
92
+
93
+ #### To do
94
+ 1. ~~MIT License~~
95
+ 2. ~~A gem specification~~
96
+ 3. ~~Basic documentation in a README file~~
97
+ 4. ~~Integration test~~
98
+ 5. Generator
99
+
100
+ ### Big generators
101
+
102
+ This gem was my first attempt at writing a generator, so it was awkward. I still don't understand [Thor](https://github.com/wycats/thor) properly. Fortunately, I happened upon [Ammeter](https://github.com/alexrothenberg/ammeter), which helped me write out test specs for the generator. If you've got good specs, then you can sometimes stumble along until you learn enough to get it right. Alex Rothenberg's original [blog post](http://www.alexrothenberg.com/2011/10/10/ammeter-the-way-to-write-specs-for-your-rails-generators.html) about the gem was quite informative, as were the test cases from the [Devise](https://github.com/plataformatec/devise/tree/master/test/generators) gem.
103
+
104
+ I have to admit; constructing the generator was more complex than the original module! There are more "moving parts;" templates, usage files, specs, in addition to the generator itself. So there is a certain amount of overhead that might overwhelm the original content. On the other hand, I learned quite a bit, and the gem is far more useful.
105
+
106
+ require "spec_helper"
107
+ require 'generators/attribute_access/attribute_access_generator'
108
+
109
+ describe AttributeAccessGenerator do
110
+ before do
111
+ prepare_destination
112
+ Rails::Generators.options[:rails][:orm] = :active_record
113
+ end
114
+
115
+ describe "the migration" do
116
+ before { run_generator %w(Person) }
117
+ subject { migration_file('db/migrate/create_people.rb') }
118
+ it { should exist }
119
+ it { should be_a_migration }
120
+ it { should contain 'class CreatePeople < ActiveRecord::Migration' }
121
+ it { should contain 'create_table :people do |t|'}
122
+ it { should contain 't.text :read_only_attributes'}
123
+ end
124
+
125
+ describe "the class" do
126
+ before { run_generator %w(Person) }
127
+ subject { file('app/models/person.rb') }
128
+ it { should exist }
129
+ it { should contain 'include AttributeAccessControllable' }
130
+ end
131
+
132
+ Some interesting things to note; you must `require` the generator, since it is not pulled in by default. The subject of each suite is a _file_, not the class `AttributeAccessGenerator`. The `migration_file` helper prepends the TIMESTAMP onto the migration file for you. If you need to set up more things for your test, `destination_root` is a helper with a path to the temporary directory. It remains after the tests have run, which makes it useful when debugging.
133
+
134
+ Here's something else that I did not know, but it might help new generator writers; the order in which you define your methods in the generator class is _significant_. I don't know how this is done, but each "method" in the generator class is executed in turn. This is important for my generator; the model class definition _must_ exist before I inject the new content that mixes in the module, so I had to write the `generate_model` method before the `inject_attribute_access_content` method. I was scratching my head over that one for quite awhile.
135
+
136
+ require "rails/generators/active_record"
137
+
138
+ class AttributeAccessGenerator < ActiveRecord::Generators::Base
139
+ source_root File.expand_path('../templates', __FILE__)
140
+
141
+ def create_migration_file
142
+ if (behavior == :invoke && model_exists?)
143
+ migration_template "migration.rb", "db/migrate/add_read_only_attributes_to_#{table_name}"
144
+ else
145
+ migration_template "migration_create.rb", "db/migrate/create_#{table_name}"
146
+ end
147
+ end
148
+
149
+ def generate_model
150
+ invoke "active_record:model", [name], :migration => false unless model_exists? && behavior == :invoke
151
+ end
152
+
153
+ def inject_attribute_access_content
154
+ class_path = class_name.to_s.split('::')
155
+
156
+ indent_depth = class_path.size
157
+ content = " " * indent_depth + 'include AttributeAccessControllable' + "\n"
158
+
159
+ inject_into_class(model_path, class_path.last, content)
160
+ end
161
+
162
+ #### To do
163
+ 1. ~~MIT License~~
164
+ 2. ~~A gem specification~~
165
+ 3. ~~Basic documentation in a README file~~
166
+ 4. ~~Integration test~~
167
+ 5. ~~Generator~~
168
+ 6. Shareable tests
169
+
170
+ ## Yo, I hear you like tests in your tests
171
+
172
+ Lastly, we want to share the testing love. The gem consumer should not have to _write_ tests to drive out the same feature that we have already tested. That would not be very DRY. So, in order to make our shared behavior, er, um, _shareable_, we moved it into `lib` with a few wrappers, namely, the `spec_support.rb` file, which you can include in your own spec files to test drive adding the module to your own classes.
173
+
174
+ Which is where `And I have a test that exercises read-only` comes in. You can see this in the `steps.rb` file:
175
+
176
+ require 'spec_helper'
177
+ require 'attribute_access_controllable/spec_support'
178
+
179
+ describe Person do
180
+ it_should_behave_like "it has AttributeAccessControllable", :attr1
181
+ end
182
+
183
+ #### To do
184
+ 1. ~~MIT License~~
185
+ 2. ~~A gem specification~~
186
+ 3. ~~Basic documentation in a README file~~
187
+ 4. ~~Integration test~~
188
+ 5. ~~Generator~~
189
+ 6. ~~Shareable tests~~
190
+
191
+ ## Don't be afraid to release v1.0.0
192
+
193
+ I am a strong believer in [semantic versioning](http://semver.org/). I simply can not understand why some core ruby tools are still living in version zero land, even after years and years of development and use. So, after a couple of internal commits, we released v1.0.0 of the gem, and less than a day later released v1.1.0 and then v1.1.1! (You probably shouldn't use anything less than v1.1.1)
194
+
195
+ ## An interesting mix
196
+
197
+ In summary, we used a lot of tools and techniques to go from a simple commit to a shareable gem:
198
+
199
+ * Rails generators
200
+ * Cucumber
201
+ * Aruba
202
+ * Ammeter
203
+ * RSpec shared behaviors
204
+ * Integration tests
205
+ * Generator tests
206
+ * Module tests
207
+
208
+ I encourage everyone to release as much of their work as possible because it raises the state of the art for us all. There are limits, of course, but that still affords lots of wiggle room. Small gems like `attribute_access_controllable` won't change the world, but they ease the pain of staying DRY and we all get to learn a little something.
209
+
210
+ ### Thanks
211
+
212
+ To Social Chorus for choosing to open source this code. And to Pivotal Labs for encouraging a better way to do software engineering.
@@ -0,0 +1,12 @@
1
+ @announce-cmd
2
+ Feature: Read only attributes
3
+
4
+ Scenario: In a simple rails application
5
+ Given a new rails application
6
+ And I generate a new migration for the class "Person"
7
+ And I generate an attribute access migration for the class "Person"
8
+ And I have a test that exercises read-only
9
+ When I run `rake spec`
10
+ Then the output should contain "8 examples, 0 failures"
11
+
12
+
@@ -0,0 +1,49 @@
1
+ World(Aruba::Api)
2
+
3
+ Given /^a new rails application$/ do
4
+ run_simple "rails new test_app --quiet --force --skip-bundle --skip-test-unit --skip-javascript --skip-sprockets"
5
+ cd '/test_app'
6
+ use_clean_gemset 'test_app'
7
+ write_file '.rvmrc', "rvm use default@test_app\n"
8
+ append_to_file 'Gemfile', <<EOT
9
+ gem 'attribute_access_controllable', :path => '../../..'
10
+ gem 'rspec-rails'
11
+ EOT
12
+ run_simple 'bundle install --quiet'
13
+ run_simple 'script/rails generate rspec:install'
14
+ end
15
+
16
+ Given /^I generate a new migration for the class "Person"$/ do
17
+ cmd = "rails generate model Person attr1:string --test-framework"
18
+ run_simple cmd
19
+ output = stdout_from cmd
20
+ assert_partial_output "invoke active_record", output
21
+
22
+ cmd = "rake db:migrate"
23
+ run_simple cmd
24
+ output = stdout_from cmd
25
+ assert_matching_output %q{== Create.+: migrating}, output
26
+ assert_matching_output %q{== Create.+: migrated}, output
27
+ end
28
+
29
+ Given /^I generate an attribute access migration for the class "Person"$/ do
30
+ cmd = "rails generate attribute_access Person"
31
+ run_simple cmd
32
+ output = stdout_from cmd
33
+ assert_matching_output "create\\s+db/migrate/\\d+_add_read_only_attributes_to_.+\.rb", output
34
+ assert_matching_output "insert\\s+app/models/.+\.rb", output
35
+
36
+ cmd = "rake db:migrate"
37
+ run_simple cmd
38
+ end
39
+
40
+ Given /^I have a test that exercises read\-only$/ do
41
+ overwrite_file 'spec/models/person_spec.rb', <<EOT
42
+ require 'spec_helper'
43
+ require 'attribute_access_controllable/spec_support'
44
+
45
+ describe Person do
46
+ it_should_behave_like "it has AttributeAccessControllable", :attr1
47
+ end
48
+ EOT
49
+ end
@@ -0,0 +1,7 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
2
+
3
+ require 'aruba/cucumber'
4
+
5
+ Before do
6
+ @aruba_timeout_seconds = 60
7
+ end
@@ -0,0 +1,35 @@
1
+ module AttributeAccessControllable
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ include ActiveModel::Validations
6
+ serialize :read_only_attributes
7
+ attr_accessor :skip_read_only # TODO: Really should name this something private (e.g. :_#{class_name}_skip_read_only)
8
+ validate :read_only_attributes_unchanged, :on => :update, :unless => :skip_read_only
9
+ end
10
+
11
+ def attr_read_only(*attributes)
12
+ self.read_only_attributes = Set.new(attributes.map { |a| a.to_s }) + (read_only_attributes || [])
13
+ end
14
+
15
+ def read_only_attribute?(attribute)
16
+ return false if read_only_attributes.nil?
17
+ read_only_attributes.member?(attribute.to_s)
18
+ end
19
+
20
+ def read_only_attributes_unchanged
21
+ changed_attributes.each_key do |attr|
22
+ errors.add(attr) << "is read_only" if read_only_attribute?(attr)
23
+ end
24
+ end
25
+
26
+ def save(options = {})
27
+ self.skip_read_only = options.delete(:skip_read_only)
28
+ super
29
+ end
30
+
31
+ def save!(options = {})
32
+ self.skip_read_only = options.delete(:skip_read_only)
33
+ super
34
+ end
35
+ end
@@ -0,0 +1 @@
1
+ require 'attribute_access_controllable/spec_support/shared_examples'
@@ -0,0 +1,51 @@
1
+ shared_examples_for "it has AttributeAccessControllable" do |test_column|
2
+ it "#attr_read_only(:attribute, ...) marks an attribute as read-only" do
3
+ expect {
4
+ subject.attr_read_only(test_column)
5
+ }.to change(subject, :read_only_attributes).from(nil).to(Set.new([test_column.to_s]))
6
+ end
7
+
8
+ it "#read_only_attribute?(:attribute) returns true when marked read-only" do
9
+ subject.read_only_attributes = Set.new([test_column.to_s])
10
+ subject.should be_read_only_attribute(test_column)
11
+ end
12
+
13
+ it "#read_only_attribute?(:attribute) returns false when not marked read-only (or not marked at all)" do
14
+ subject.should_not be_read_only_attribute(test_column)
15
+ end
16
+
17
+ it "#save! raises error when :attribute is read-only" do
18
+ subject.attr_read_only(test_column)
19
+ subject.send(test_column.to_s + '=', 0)
20
+ expect { subject.save!(context: :update) }.to raise_error ActiveRecord::RecordInvalid
21
+ subject.errors[test_column].should =~ ["is invalid", "is read_only"]
22
+ end
23
+
24
+ it "#save!(:context => :skip_read_only) is okay" do
25
+ subject.attr_read_only(test_column)
26
+ subject.send(test_column.to_s + '=', 0)
27
+ expect { subject.save!(:skip_read_only => true) }.to_not raise_error
28
+ end
29
+
30
+ it "#save is invalid when :attribute is read-only" do
31
+ subject.attr_read_only(test_column)
32
+ subject.send(test_column.to_s + '=', 0)
33
+ subject.save(context: :update).should be_false
34
+ subject.errors[test_column].should =~ ["is invalid", "is read_only"]
35
+ end
36
+
37
+ it "#save(:context => :skip_read_only) is okay" do
38
+ subject.attr_read_only(test_column)
39
+ subject.send(test_column.to_s + '=', 0)
40
+ subject.save!(:skip_read_only => true).should_not be_false
41
+ end
42
+
43
+ it "serializes :read_only_attributes" do
44
+ subject.attr_read_only(test_column)
45
+ subject.save!
46
+ subject.reload
47
+ subject.read_only_attributes.should == Set.new([test_column.to_s])
48
+ end
49
+
50
+ #it "#attr_writable(:attribute)"
51
+ end
@@ -0,0 +1,3 @@
1
+ module AttributeAccessControllable
2
+ VERSION = "1.1.2"
3
+ end
@@ -0,0 +1,14 @@
1
+ Description:
2
+ Add attribute access control to an ActiveRecord model
3
+
4
+ Example:
5
+ rails generate attribute_access Thing
6
+
7
+ This will create a migration thats adds
8
+ :read_only_attributes to your Thing model
9
+
10
+ You will then need to add
11
+
12
+ include AttributeAccessControllable
13
+
14
+ to your model to enable the new behavior
@@ -0,0 +1,36 @@
1
+ require "rails/generators/active_record"
2
+
3
+ class AttributeAccessGenerator < ActiveRecord::Generators::Base
4
+ source_root File.expand_path('../templates', __FILE__)
5
+
6
+ def create_migration_file
7
+ if (behavior == :invoke && model_exists?)
8
+ migration_template "migration.rb", "db/migrate/add_read_only_attributes_to_#{table_name}"
9
+ else
10
+ migration_template "migration_create.rb", "db/migrate/create_#{table_name}"
11
+ end
12
+ end
13
+
14
+ def generate_model
15
+ invoke "active_record:model", [name], :migration => false unless model_exists? && behavior == :invoke
16
+ end
17
+
18
+ def inject_attribute_access_content
19
+ class_path = class_name.to_s.split('::')
20
+
21
+ indent_depth = class_path.size
22
+ content = " " * indent_depth + 'include AttributeAccessControllable' + "\n"
23
+
24
+ inject_into_class(model_path, class_path.last, content)
25
+ end
26
+
27
+ private
28
+
29
+ def model_exists?
30
+ File.exists?(File.join(destination_root, model_path))
31
+ end
32
+
33
+ def model_path
34
+ @model_path ||= File.join("app", "models", "#{file_path}.rb")
35
+ end
36
+ end
@@ -0,0 +1,5 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration
2
+ def change
3
+ add_column :<%= table_name %>, :read_only_attributes, :text
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration
2
+ def change
3
+ create_table :<%= table_name %> do |t|
4
+ t.text :read_only_attributes
5
+
6
+ t.timestamps
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+ require 'informal'
3
+ require 'active_record/errors'
4
+ require 'active_record/validations'
5
+
6
+ require 'attribute_access_controllable'
7
+ require 'attribute_access_controllable/spec_support'
8
+
9
+ class TestFakePersistance
10
+ include Informal::Model
11
+ include ActiveRecord::Validations
12
+
13
+ def save(options={})
14
+ perform_validations(options) ? :pretend_to_call_super : false
15
+ end
16
+
17
+ def save!(options={})
18
+ perform_validations(options) ? :pretend_to_call_super : raise(ActiveRecord::RecordInvalid.new(self))
19
+ end
20
+
21
+ def reload
22
+ self
23
+ end
24
+
25
+ def self.serialize(*)
26
+ end
27
+ end
28
+
29
+ class TestAttributeAccessControllable < TestFakePersistance
30
+ attr_accessor :read_only_attributes # usually created via a migration
31
+ include ActiveModel::Dirty
32
+
33
+ define_attribute_methods [:test_column]
34
+
35
+ def test_column
36
+ @test_column
37
+ end
38
+
39
+ def test_column=(val)
40
+ test_column_will_change! unless val == @test_column
41
+ @test_column = val
42
+ end
43
+
44
+ include AttributeAccessControllable # code under test
45
+ end
46
+
47
+ describe AttributeAccessControllable do
48
+ subject { TestAttributeAccessControllable.new }
49
+
50
+ it_should_behave_like "it has AttributeAccessControllable", :test_column
51
+ end
52
+
@@ -0,0 +1,45 @@
1
+ require "spec_helper"
2
+ require 'generators/attribute_access/attribute_access_generator'
3
+
4
+ describe AttributeAccessGenerator do
5
+ before do
6
+ prepare_destination
7
+ Rails::Generators.options[:rails][:orm] = :active_record
8
+ end
9
+
10
+ context "new class" do
11
+ describe "the migration" do
12
+ before { run_generator %w(Person) }
13
+ subject { migration_file('db/migrate/create_people.rb') }
14
+ it { should exist }
15
+ it { should be_a_migration }
16
+ it { should contain 'class CreatePeople < ActiveRecord::Migration' }
17
+ it { should contain 'create_table :people do |t|'}
18
+ it { should contain 't.text :read_only_attributes'}
19
+ end
20
+ end
21
+
22
+ context "existing class" do
23
+ describe "the migration" do
24
+ before do
25
+ FileUtils.mkdir_p(File.join(destination_root, "app/models"))
26
+ File.open(File.join(destination_root, "app/models/person.rb"), "w") {|f|
27
+ f.puts "class Person < ActiveRecord::Base\nend\n"
28
+ }
29
+ run_generator %w(Person)
30
+ end
31
+ subject { migration_file('db/migrate/add_read_only_attributes_to_people.rb') }
32
+ it { should exist }
33
+ it { should be_a_migration }
34
+ it { should contain 'class AddReadOnlyAttributesToPeople < ActiveRecord::Migration' }
35
+ it { should contain 'add_column :people, :read_only_attributes, :text'}
36
+ end
37
+ end
38
+
39
+ describe "the class" do
40
+ before { run_generator %w(Person) }
41
+ subject { file('app/models/person.rb') }
42
+ it { should exist }
43
+ it { should contain 'include AttributeAccessControllable' }
44
+ end
45
+ end
@@ -0,0 +1,13 @@
1
+ require 'bundler'
2
+ Bundler.setup
3
+ require 'ammeter/init'
4
+
5
+ SPEC_ROOT = File.dirname(__FILE__)
6
+
7
+ Dir[File.join(SPEC_ROOT, "support/**/*.rb")].each {|f| require f}
8
+
9
+ RSpec.configure do |config|
10
+ config.treat_symbols_as_metadata_keys_with_true_values = true
11
+ config.run_all_when_everything_filtered = true
12
+ config.filter_run :focus
13
+ end
metadata ADDED
@@ -0,0 +1,226 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: attribute_access_controllable
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Social Chorus
9
+ - Kenneth Mayer
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-05-08 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activesupport
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ~>
21
+ - !ruby/object:Gem::Version
22
+ version: 3.2.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ~>
29
+ - !ruby/object:Gem::Version
30
+ version: 3.2.1
31
+ - !ruby/object:Gem::Dependency
32
+ name: activemodel
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ~>
37
+ - !ruby/object:Gem::Version
38
+ version: 3.2.1
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ version: 3.2.1
47
+ - !ruby/object:Gem::Dependency
48
+ name: railties
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 3.2.1
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ~>
61
+ - !ruby/object:Gem::Version
62
+ version: 3.2.1
63
+ - !ruby/object:Gem::Dependency
64
+ name: rspec
65
+ requirement: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ~>
69
+ - !ruby/object:Gem::Version
70
+ version: 2.8.0
71
+ type: :development
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ~>
77
+ - !ruby/object:Gem::Version
78
+ version: 2.8.0
79
+ - !ruby/object:Gem::Dependency
80
+ name: pivotal_git_scripts
81
+ requirement: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ type: :development
88
+ prerelease: false
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ - !ruby/object:Gem::Dependency
96
+ name: informal
97
+ requirement: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ! '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: activerecord
113
+ requirement: !ruby/object:Gem::Requirement
114
+ none: false
115
+ requirements:
116
+ - - ! '>='
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ none: false
123
+ requirements:
124
+ - - ! '>='
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ - !ruby/object:Gem::Dependency
128
+ name: aruba
129
+ requirement: !ruby/object:Gem::Requirement
130
+ none: false
131
+ requirements:
132
+ - - ! '>='
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ type: :development
136
+ prerelease: false
137
+ version_requirements: !ruby/object:Gem::Requirement
138
+ none: false
139
+ requirements:
140
+ - - ! '>='
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ - !ruby/object:Gem::Dependency
144
+ name: ammeter
145
+ requirement: !ruby/object:Gem::Requirement
146
+ none: false
147
+ requirements:
148
+ - - ! '>='
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
151
+ type: :development
152
+ prerelease: false
153
+ version_requirements: !ruby/object:Gem::Requirement
154
+ none: false
155
+ requirements:
156
+ - - ! '>='
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ description: Allow attribute-level read/write access control, per instance, of any
160
+ ActiveModel class.
161
+ email:
162
+ - tech@socialchorus.com
163
+ - kmayer@bitwrangler.com
164
+ executables: []
165
+ extensions: []
166
+ extra_rdoc_files: []
167
+ files:
168
+ - .gitignore
169
+ - .pairs
170
+ - .rspec
171
+ - Gemfile
172
+ - LICENSE
173
+ - README.md
174
+ - Rakefile
175
+ - attribute_access_controllable.gemspec
176
+ - cucumber.yml
177
+ - doc/blog-post.md
178
+ - features/read_only_attributes.feature
179
+ - features/step_definitions/steps.rb
180
+ - features/support/env.rb
181
+ - lib/attribute_access_controllable.rb
182
+ - lib/attribute_access_controllable/spec_support.rb
183
+ - lib/attribute_access_controllable/spec_support/shared_examples.rb
184
+ - lib/attribute_access_controllable/version.rb
185
+ - lib/generators/attribute_access/USAGE
186
+ - lib/generators/attribute_access/attribute_access_generator.rb
187
+ - lib/generators/attribute_access/templates/migration.rb
188
+ - lib/generators/attribute_access/templates/migration_create.rb
189
+ - spec/attribute_access_controllable_spec.rb
190
+ - spec/generators/attribute_access_generator_spec.rb
191
+ - spec/spec_helper.rb
192
+ homepage: https://github.com/halogenguides/Attribute-Access-Controllable
193
+ licenses: []
194
+ post_install_message:
195
+ rdoc_options: []
196
+ require_paths:
197
+ - lib
198
+ required_ruby_version: !ruby/object:Gem::Requirement
199
+ none: false
200
+ requirements:
201
+ - - ! '>='
202
+ - !ruby/object:Gem::Version
203
+ version: '0'
204
+ required_rubygems_version: !ruby/object:Gem::Requirement
205
+ none: false
206
+ requirements:
207
+ - - ! '>='
208
+ - !ruby/object:Gem::Version
209
+ version: '0'
210
+ requirements: []
211
+ rubyforge_project:
212
+ rubygems_version: 1.8.21
213
+ signing_key:
214
+ specification_version: 3
215
+ summary: This gem allows you to control write access at the _attribute_ level, on
216
+ a per-instance basis. For example, let's say you had a model `Person` that has an
217
+ attribute, `birthday`, which, for security purposes, once this attribute is set,
218
+ cannot be changed again (except, perhaps by an administrator with extraordinary
219
+ privileges). Any attempts to change this value to raise a validation error.
220
+ test_files:
221
+ - features/read_only_attributes.feature
222
+ - features/step_definitions/steps.rb
223
+ - features/support/env.rb
224
+ - spec/attribute_access_controllable_spec.rb
225
+ - spec/generators/attribute_access_generator_spec.rb
226
+ - spec/spec_helper.rb