liaison 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ *swp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in liaison.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,30 @@
1
+ Copyright (c)2011, Mike Burns
2
+
3
+ All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ * Redistributions of source code must retain the above copyright
9
+ notice, this list of conditions and the following disclaimer.
10
+
11
+ * Redistributions in binary form must reproduce the above
12
+ copyright notice, this list of conditions and the following
13
+ disclaimer in the documentation and/or other materials provided
14
+ with the distribution.
15
+
16
+ * Neither the name of Mike Burns nor the names of other
17
+ contributors may be used to endorse or promote products derived
18
+ from this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,116 @@
1
+ Liaison
2
+ =======
3
+
4
+ A Rails presenter class.
5
+
6
+ A major idea of [the presenter pattern](http://blog.jayfields.com/2007/03/rails-presenter-pattern.html) is to break off the business logic from the view object, letting the view logic be a dumb instance that knows how to get, set, and validate values. The business logic can then query the presenter object for the values as needed.
7
+
8
+ Look, here's an example business object:
9
+
10
+ class SignUp
11
+ attr_reader :user
12
+
13
+ def initialize(presenter, account_builder = Account)
14
+ @email = presenter[:email]
15
+ @password = presenter[:password]
16
+ @account_name = presenter[:account_name]
17
+
18
+ @presenter = presenter
19
+ @account_builder = account_builder
20
+ end
21
+
22
+ def save
23
+ if presenter.valid?
24
+ account = account_builder.new(:name => account_name)
25
+ @user = account.users.build(:email => email, :password => password)
26
+ account.save.tap do |succeeded|
27
+ presenter.add_errors(account.errors) unless succeeded
28
+ end
29
+ end
30
+ end
31
+
32
+ protected
33
+
34
+ attr_accessor :email, :password, :account_name, :account_builder, :presenter
35
+ end
36
+
37
+ It's just a class, which you can unit test as you please. A presenter object is passed in, then we pull the values out, make sure it's valid, and add errors to it as needed. This class does not deal directly with validations, state, or any of the ActiveModel nonsense.
38
+
39
+ Now you need to know how to use a `Presenter` object, so this is what the controller looks like:
40
+
41
+ class SignupsController < ApplicationController
42
+ def new
43
+ @sign_up = presenter
44
+ end
45
+
46
+ def create
47
+ @sign_up = presenter.with_params(params[:sign_up])
48
+ db = SignUp.new(@sign_up)
49
+
50
+ if db.save
51
+ sign_in_as(db.user)
52
+ redirect_to root_url
53
+ else
54
+ render :new
55
+ end
56
+ end
57
+
58
+ protected
59
+
60
+ def presenter
61
+ Presenter.new('sign_up',
62
+ :fields => [:email, :password, :account_name],
63
+ :validator => SignUpValidator.new)
64
+ end
65
+ end
66
+
67
+ In our `new` action we simply set the `@sign_up` i-var to an instance of the `Presenter`. In `create` we use that `Presenter` instance, adding CGI params in. Then we pass that to the `SignUp` class defined above and it's all boring from there.
68
+
69
+ The `presenter` method in the above example produces a new `Presenter` instance. This instance has a model name (`sign_up`), fields the form will handle (`email`, `password`, and `account_name`), and a validator (`SignUpValidator`). The validator is any instance of `ActiveModel::Validator`, for example:
70
+
71
+ class SignUpValidator < ActiveModel::EachValidator
72
+ def validate_each(record, attribute, value)
73
+ record.errors.add(attribute, "can't be blank") if value.blank?
74
+ end
75
+ end
76
+
77
+ You, the author of the business logic class, are in charge of checking in on these validations and errors. For example, before saving any objects you should check `Presenter#valid?`. And after you've saved something to the database you should add any errors onto the presenter using `Presenter#add_errors`.
78
+
79
+ Getting Data
80
+ ----------
81
+
82
+ An instance of the `Presenter` object is Hash-like: it implements the `Enumerable` module, which means it has an `#each` method among many others; it also has a `#[]` method, which you can use to access values just like with the CGI `params` hash.
83
+
84
+ Testing
85
+ -------
86
+
87
+ When writing your unit tests it'll be handy to have a mock presenter around, which is why we package a `MockPresenter` class for you to use. It gives you access to the `#have_errors` and `#have_no_errors` RSpec matchers.
88
+
89
+
90
+ describe SignUp, 'invalid' do
91
+ let(:params) { { :email => '',
92
+ :password => 'bar',
93
+ :account_name => 'baz' } }
94
+ let(:errors) { { :email => "can't be blank" } }
95
+ let(:presenter) do
96
+ MockPresenter.new(:valid => false,
97
+ :params => params,
98
+ :errors => errors)
99
+ end
100
+ let(:account_builder) { MockAccount.new(:valid => true) }
101
+
102
+ subject { SignUp.new(presenter, account_builder) }
103
+
104
+ it "does not save the account or user" do
105
+ subject.save.should be_false
106
+
107
+ presenter.should have_errors(errors)
108
+ end
109
+ end
110
+
111
+ Contact
112
+ -------
113
+
114
+ Copyright 2011 [Mike Burns](http://mike-burns.com/).
115
+
116
+ Please [open a pull request on Github](https://github.com/mike-burns/liaison/pulls) as needed.
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "liaison/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "liaison"
7
+ s.version = Liaison::VERSION
8
+ s.authors = ["Mike Burns"]
9
+ s.email = ["mike@mike-burns.com"]
10
+ s.homepage = "https://github.com/mike-burns/liaison"
11
+ s.license = 'BSD'
12
+ s.summary = %q{A Rails presenter class.}
13
+ s.description = %q{An object that works with form_for that encapsulates validations and data management, leaving the business logic up to your testable old self.}
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.require_paths = ["lib"]
19
+
20
+ s.add_dependency('activemodel')
21
+
22
+ s.add_development_dependency('rspec')
23
+ s.add_development_dependency('rake')
24
+ end
@@ -0,0 +1,3 @@
1
+ require "liaison/version"
2
+ require 'liaison/presenter'
3
+ require 'liaison/mock_presenter'
@@ -0,0 +1,31 @@
1
+ class MockPresenter
2
+ def initialize(opts = {})
3
+ @valid = opts.delete(:valid) != false
4
+ @fields = opts[:params]
5
+ @errors = opts[:errors] || {}
6
+ end
7
+
8
+ def [](key)
9
+ @fields[key]
10
+ end
11
+
12
+ def valid?
13
+ @errors.empty? && @valid
14
+ end
15
+
16
+ def errors
17
+ @errors
18
+ end
19
+
20
+ def add_errors(errs)
21
+ errs.each {|k,v| @errors[k] = v}
22
+ end
23
+
24
+ def has_no_errors?
25
+ @errors.empty?
26
+ end
27
+
28
+ def has_errors?(errors)
29
+ errors.all? {|k,v| @errors[k] == v}
30
+ end
31
+ end
@@ -0,0 +1,95 @@
1
+ require 'active_model'
2
+
3
+ class Presenter
4
+ extend ActiveModel::Naming
5
+ include ActiveModel::Conversion
6
+ include ActiveModel::Validations
7
+
8
+ validate :instance_validations
9
+
10
+ # Constructs a new Presenter object which can be passed to a form or
11
+ # generally filled with values. It must take a model name, which is a string
12
+ # that is the name of the model you are presenting. It can also take a
13
+ # validator and fields.
14
+ #
15
+ # Presenter.new('sign_up',
16
+ # :fields => [:account_name, :email, :password],
17
+ # :validator => SignUpValidator)
18
+ def initialize(model_name, opts = {})
19
+ @@model_name = model_name
20
+
21
+ @validator = opts[:validator]
22
+ @fields = opts[:fields]
23
+
24
+ self.class.send(:attr_accessor,*@fields) unless @fields.nil? || @fields.empty?
25
+ end
26
+
27
+ def instance_validations
28
+ validates_with(@validator, :attributes => @fields) if @validator
29
+ end
30
+
31
+ def self.model_name # :nodoc:
32
+ ActiveModel::Name.new(Class.new do
33
+ attr_reader :name
34
+ def initialize(n)
35
+ @name = n
36
+ end
37
+ end.new(@@model_name))
38
+ end
39
+
40
+ def persisted? # :nodoc:
41
+ false
42
+ end
43
+
44
+ # Set the params from the form using this.
45
+ #
46
+ # @sign_up_presenter.with_params(params[:sign_up])
47
+ def with_params(params = {})
48
+ params.each {|k,v| self.send("#{k}=", v)}
49
+ self
50
+ end
51
+
52
+ # Combine error messages from any ActiveModel object with the presenter's, so
53
+ # they will show on the form.
54
+ #
55
+ # @sign_up_presenter.add_errors(account.errors)
56
+ #
57
+ # You will probably use it like this:
58
+ #
59
+ # class SignUp
60
+ # attr_accessor :presenter
61
+ # def save
62
+ # account = Account.new
63
+ # account.save.tap do |succeeded|
64
+ # presenter.add_errors(account.errors) unless succeeded
65
+ # end
66
+ # end
67
+ # end
68
+ def add_errors(errs)
69
+ errs.each {|k,v| errors.add(k,v)}
70
+ end
71
+
72
+ # Access individual values as if this were the CGI params hash.
73
+ #
74
+ # @sign_up_presenter[:account_name]
75
+ def [](key)
76
+ to_hash[key]
77
+ end
78
+
79
+ # This is an instance of Enumerable, which means you can iterate over the
80
+ # keys and values as set by the form.
81
+ #
82
+ # @sign_up_presenter.each {|k,v| puts "the form set #{k} to #{v}" }
83
+ def each(&block)
84
+ to_hash.each(&block)
85
+ end
86
+
87
+ protected
88
+
89
+ def to_hash
90
+ @fields.inject({}) do |acc,field|
91
+ acc[field] = send(field)
92
+ acc
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,3 @@
1
+ module Liaison
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+
3
+ describe MockPresenter, 'params' do
4
+ let(:params) {{'a' => 'one', 'b' => 'two'}}
5
+
6
+ subject { MockPresenter.new(:params => params) }
7
+
8
+ it "can access values using []" do
9
+ params.each do |k,v|
10
+ subject[k].should == v
11
+ end
12
+ end
13
+ end
14
+
15
+ describe MockPresenter, 'validations and errors' do
16
+ let(:errors) {{ :account_name => "can't be blank" }}
17
+
18
+ subject { MockPresenter.new(:valid => false, :errors => errors) }
19
+
20
+ it "is invalid with errors" do
21
+ subject.should_not be_valid
22
+ subject.errors.should_not be_blank
23
+ errors.each do |k,v|
24
+ subject.errors[k].should include(v)
25
+ end
26
+ end
27
+ end
28
+
29
+ describe MockPresenter, 'adding errors' do
30
+ let(:errors) {{ :account_name => "can't be blank" }}
31
+
32
+ subject { MockPresenter.new }
33
+
34
+ it "can add errors" do
35
+ subject.add_errors(errors)
36
+
37
+ subject.should_not be_valid
38
+ subject.errors.should_not be_blank
39
+ errors.each do |k,v|
40
+ subject.errors[k].should include(v)
41
+ end
42
+ subject.should have_errors(errors)
43
+ end
44
+
45
+ it "has no errors by default" do
46
+ subject.should be_valid
47
+ subject.errors.should be_blank
48
+ subject.should have_no_errors
49
+ end
50
+ end
@@ -0,0 +1,99 @@
1
+ require 'spec_helper'
2
+
3
+ describe Presenter do
4
+ let(:model_name) { 'sign_up' }
5
+
6
+ subject { Presenter.new(model_name) }
7
+
8
+ it "handles the ActiveModel naming" do
9
+ subject.class.model_name.singular.should == model_name
10
+ end
11
+
12
+ it "is an ActiveModel conversion" do
13
+ subject.should_not be_persisted
14
+ subject.to_model.should == subject
15
+ subject.to_key.should be_nil
16
+ subject.to_param.should be_nil
17
+ end
18
+ end
19
+
20
+ describe Presenter, 'validations' do
21
+ let(:model_name) { 'sign_up' }
22
+ let(:fields) { [:a, :b] }
23
+ let(:failing_validator) do
24
+ Class.new(ActiveModel::Validator) do
25
+ def initialize(opts)
26
+ @@attributes = opts[:attributes]
27
+ super(opts)
28
+ end
29
+
30
+ def validate(record)
31
+ record.errors[:base] << 'invalid'
32
+ end
33
+
34
+ def self.has_set_attributes_to?(attribs)
35
+ @@attributes == attribs
36
+ end
37
+ end
38
+ end
39
+ let(:succeeding_validator) do
40
+ Class.new(ActiveModel::Validator) do
41
+ def validate(record)
42
+ nil
43
+ end
44
+ end
45
+ end
46
+ let(:errors) { {:name => "can't be blank"} }
47
+
48
+ it "is valid by default" do
49
+ presenter = Presenter.new(model_name)
50
+ presenter.should be_valid
51
+ presenter.errors.should be_empty
52
+ end
53
+
54
+ it "runs validations as given" do
55
+ presenter = Presenter.new(model_name,
56
+ :validator => failing_validator,
57
+ :fields => fields)
58
+ presenter.should be_invalid
59
+ presenter.errors.should_not be_empty
60
+ failing_validator.should have_set_attributes_to(fields)
61
+ end
62
+
63
+ it "runs validations as given" do
64
+ presenter = Presenter.new(model_name, :validator => succeeding_validator)
65
+ presenter.should be_valid
66
+ presenter.errors.should be_empty
67
+ end
68
+
69
+ it "adds errors" do
70
+ presenter = Presenter.new(model_name)
71
+ presenter.add_errors(errors)
72
+ presenter.errors.should_not be_blank
73
+ errors.each do |k,v|
74
+ presenter.errors[k].should include(v)
75
+ end
76
+ end
77
+ end
78
+
79
+ describe Presenter, "enumerations" do
80
+ let(:model_name) { 'sign_up' }
81
+ subject do
82
+ Presenter.
83
+ new(model_name, :fields => [:a,:b]).
84
+ with_params('a' => 'hello', 'b' => 'goodbye')
85
+ end
86
+
87
+ it "eaches" do
88
+ subject.each do |k,v|
89
+ fail unless [:a,:b].include?(k)
90
+ v.should == 'hello' if k == :a
91
+ v.should == 'goodbye' if k == :b
92
+ end
93
+ end
94
+
95
+ it "is indexable" do
96
+ subject[:a].should == 'hello'
97
+ subject[:b].should == 'goodbye'
98
+ end
99
+ end
@@ -0,0 +1 @@
1
+ require 'liaison'
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: liaison
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Mike Burns
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-09-10 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: activemodel
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: rspec
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ hash: 3
43
+ segments:
44
+ - 0
45
+ version: "0"
46
+ type: :development
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: rake
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ hash: 3
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ type: :development
61
+ version_requirements: *id003
62
+ description: An object that works with form_for that encapsulates validations and data management, leaving the business logic up to your testable old self.
63
+ email:
64
+ - mike@mike-burns.com
65
+ executables: []
66
+
67
+ extensions: []
68
+
69
+ extra_rdoc_files: []
70
+
71
+ files:
72
+ - .gitignore
73
+ - Gemfile
74
+ - LICENSE
75
+ - README.md
76
+ - Rakefile
77
+ - liaison.gemspec
78
+ - lib/liaison.rb
79
+ - lib/liaison/mock_presenter.rb
80
+ - lib/liaison/presenter.rb
81
+ - lib/liaison/version.rb
82
+ - spec/mock_presenter_spec.rb
83
+ - spec/presenter_spec.rb
84
+ - spec/spec_helper.rb
85
+ homepage: https://github.com/mike-burns/liaison
86
+ licenses:
87
+ - BSD
88
+ post_install_message:
89
+ rdoc_options: []
90
+
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ hash: 3
99
+ segments:
100
+ - 0
101
+ version: "0"
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ none: false
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ hash: 3
108
+ segments:
109
+ - 0
110
+ version: "0"
111
+ requirements: []
112
+
113
+ rubyforge_project:
114
+ rubygems_version: 1.7.2
115
+ signing_key:
116
+ specification_version: 3
117
+ summary: A Rails presenter class.
118
+ test_files: []
119
+