├── .gitignore ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib ├── simple_form_object.rb └── simple_form_object │ └── version.rb ├── simple_form_objects.gemspec └── spec ├── attribute_spec.rb ├── delegation_spec.rb ├── model_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in simple_form_objects.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Leonard Garvey 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. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimpleFormObject 2 | 3 | Allows you to make really simple non-persisted form objects or models. 4 | 5 | **Only suitable for Rails 4 applications.** 6 | 7 | You don't need to remember to: 8 | 9 | 1. `include ActiveModel::Model` 10 | 2. Set the class `model_name` so Rails url generation works for forms. 11 | 12 | It gives you: 13 | 14 | 1. Default values for your form attributes. 15 | 2. Integration with simple_form so you don't need to specify the field type on the form. 16 | 3. Thanks to `ActiveModel::Model` you can use standard Rails validations on your attributes. 17 | 18 | ## Installation 19 | 20 | Add this line to your application's Gemfile: 21 | 22 | ```ruby 23 | gem 'simple_form_object' 24 | ``` 25 | 26 | And then execute: 27 | 28 | $ bundle 29 | 30 | ## Usage 31 | 32 | Create a form class. I like to put them inside `app/forms/`. If you 33 | prefer to create models then that will work too. 34 | 35 | ```ruby 36 | class PostForm 37 | include SimpleFormObject 38 | 39 | attribute :body, :text 40 | attribute :title, :string 41 | attribute :publish_date, :datetime, default: Time.now 42 | 43 | validates_presence_of :body 44 | validates_presence_of :title 45 | end 46 | ``` 47 | 48 | `SimpleFormObject` includes `ActiveModel::Model` so you don't need to. 49 | It also intelligently sets the `model_name` on the class so that Rails 50 | routing works as expected. As an example: 51 | 52 | ```erb 53 | <%= simple_form_for @post_form do |f| %> 54 | <%= f.input :title # renders a simple string input %> 55 | <%= f.input :body # renders a textarea %> 56 | <%= f.input :publish_date # renders a datetime select html element %> 57 | <% end %> 58 | ``` 59 | 60 | Will create a HTML form which will `POST` to `posts_path`. In your controller, you can do something like this: 61 | 62 | ```ruby 63 | def new 64 | @post_form = PostForm.new 65 | end 66 | 67 | def create 68 | @post_form = PostForm.new(param[:post]) 69 | if @post_form.valid? 70 | # save the data 71 | flash[:notice] = "Post created successfully." 72 | # redirect somewhere 73 | else 74 | flash[:error] = @post_form.errors.full_messages 75 | render 'new' 76 | end 77 | end 78 | ``` 79 | 80 | ## Todo: 81 | 82 | 1. Automatically add good validations for types. 83 | 2. It's tested in `spec` but better tests wouldn't hurt. 84 | 85 | ## Contributing 86 | 87 | 1. Fork it ( http://github.com/reInteractive-open/simple_form_objects/fork ) 88 | 2. Create your feature branch (`git checkout -b my-new-feature`) 89 | 3. Commit your changes (`git commit -am 'Add some feature'`) 90 | 4. Push to the branch (`git push origin my-new-feature`) 91 | 5. Create new Pull Request 92 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /lib/simple_form_object.rb: -------------------------------------------------------------------------------- 1 | require "simple_form_object/version" 2 | require "active_model" 3 | require "active_support" 4 | 5 | module SimpleFormObject 6 | extend ActiveSupport::Concern 7 | include ActiveModel::Model 8 | 9 | module ClassMethods 10 | def attribute(name, type = :string, options = {}) 11 | self.send(:attr_accessor, name) 12 | 13 | _attributes << Attribute.new(name, type, options) 14 | end 15 | 16 | def delegate_all(options = {}) 17 | @_delegation_target = options.fetch(:to) 18 | end 19 | 20 | def _delegation_target 21 | @_delegation_target 22 | end 23 | 24 | def _attributes 25 | @_attributes ||= [] 26 | end 27 | 28 | def _attribute(attribute_name) 29 | _attributes.select{|a| a.name == attribute_name}.first 30 | end 31 | 32 | def model_name 33 | ActiveModel::Name.new(self, nil, self.to_s.gsub(/Form$/, '')) 34 | end 35 | end 36 | 37 | def method_missing(method, *args, &block) 38 | return super unless delegatable?(method) 39 | 40 | # TODO: Figure out why self.class.delegate(method, to: self.class._delegation_target) 41 | # doesn't work. 42 | 43 | self.class.send(:define_method, method) do |*args, &block| 44 | _delegation_target.send(method, *args, &block) 45 | end 46 | 47 | send(method, *args, &block) 48 | end 49 | 50 | def delegatable?(method) 51 | 52 | if !_delegation_target.nil? 53 | _delegation_target.respond_to?(method) 54 | else 55 | false 56 | end 57 | end 58 | 59 | def _delegation_target 60 | target = self.class._delegation_target 61 | 62 | if target.is_a? Symbol 63 | self.send(target) 64 | else 65 | target 66 | end 67 | end 68 | 69 | def column_for_attribute(attribute) 70 | self.class._attribute(attribute).fake_column 71 | end 72 | 73 | def has_attribute?(attribute_name) 74 | self.class._attribute(attribute_name).present? 75 | end 76 | 77 | def initialize(attributes={}) 78 | super 79 | self.class._attributes.each do |attribute| 80 | attribute.apply_default_to(self) 81 | end 82 | end 83 | 84 | def attributes 85 | attribs = {} 86 | self.class._attributes.each do |a| 87 | attribs[a.name] = self.send(a.name) 88 | end 89 | attribs 90 | end 91 | 92 | class Attribute 93 | def initialize(name, type = nil, options) 94 | @name = name 95 | @type = type || :string 96 | @options = options 97 | 98 | extract_options 99 | end 100 | 101 | attr_accessor :name, :type, :options 102 | 103 | def fake_column 104 | self 105 | end 106 | 107 | def apply_default_to(form) 108 | if form.send(@name).nil? 109 | form.send("#{@name}=", default_value(form)) if @apply_default 110 | end 111 | end 112 | 113 | private 114 | 115 | def default_value(context) 116 | if @default.respond_to?(:call) 117 | context.instance_eval(&@default) 118 | else 119 | @default 120 | end 121 | end 122 | 123 | def extract_options 124 | @apply_default = true 125 | @default = options.fetch(:default) { @apply_default = false; nil } 126 | @skip_validations = options.fetch(:skip_validations, false) 127 | end 128 | end 129 | 130 | end 131 | -------------------------------------------------------------------------------- /lib/simple_form_object/version.rb: -------------------------------------------------------------------------------- 1 | module SimpleFormObject 2 | VERSION = "0.0.8" 3 | end 4 | -------------------------------------------------------------------------------- /simple_form_objects.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'simple_form_object/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "simple_form_object" 8 | spec.version = SimpleFormObject::VERSION 9 | spec.authors = ["Leonard Garvey"] 10 | spec.email = ["lengarvey@gmail.com"] 11 | spec.summary = %q{Simple form objects for simple form and rails} 12 | spec.description = %q{Very simple form objects for simple form and rails} 13 | spec.homepage = "http://github.com/reinteractive-open/simple_form_object" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency "activemodel", "~> 4.0" 22 | spec.add_dependency "activesupport", "~> 4.0" 23 | spec.add_development_dependency "bundler", "~> 1.5" 24 | spec.add_development_dependency "rake" 25 | spec.add_development_dependency "rspec" 26 | end 27 | -------------------------------------------------------------------------------- /spec/attribute_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SimpleFormObject::Attribute do 4 | describe 'initialization' do 5 | subject(:attribute) { SimpleFormObject::Attribute.new(name, type, options) } 6 | 7 | let(:name) { :foo } 8 | let(:type) { :boolean } 9 | let(:options) { { default: 'hello' } } 10 | 11 | it 'should be initialized with a name, type and options hash' do 12 | expect(attribute.name).to eq name 13 | expect(attribute.type).to eq type 14 | expect(attribute.options).to eq options 15 | end 16 | 17 | describe 'type' do 18 | let(:type) { nil } 19 | 20 | it 'is :string when nil or not provided' do 21 | expect(attribute.type).to eq :string 22 | end 23 | end 24 | end 25 | 26 | describe '#apply_default_to' do 27 | let(:name) { :foo } 28 | let(:type) { :boolean } 29 | let(:options) { { default: default } } 30 | let(:default) { true } 31 | let(:attribute) { SimpleFormObject::Attribute.new(name, type, options) } 32 | let(:form) { double(foo: nil) } 33 | 34 | it 'should apply the default value to the attribute on the object' do 35 | expect(form).to receive("#{name}=").with(default) 36 | attribute.apply_default_to(form) 37 | end 38 | 39 | context 'with a falsey default' do 40 | let(:default) { false } 41 | it 'should apply the default value to the attribute on the object' do 42 | expect(form).to receive("#{name}=").with(default) 43 | attribute.apply_default_to(form) 44 | end 45 | end 46 | 47 | context 'when the form object attribute has a value' do 48 | let(:form) { double(foo: false) } 49 | 50 | it 'should not apply the default' do 51 | expect(form).to_not receive("#{name}=").with(default) 52 | attribute.apply_default_to(form) 53 | expect(form.foo).to eq false 54 | end 55 | end 56 | 57 | context 'with a lambda/proc default' do 58 | let(:default) { Proc.new { :bar } } 59 | 60 | it 'calls the lambda' do 61 | expect(form).to receive("#{name}=").with(:bar) 62 | attribute.apply_default_to(form) 63 | end 64 | 65 | context 'when the proc references the form' do 66 | let(:default) { Proc.new { current_foo } } 67 | let(:form) { double('Form', foo: nil, current_foo: :bar) } 68 | 69 | it 'calls the lambda' do 70 | expect(form).to receive("#{name}=").with(:bar) 71 | attribute.apply_default_to(form) 72 | end 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /spec/delegation_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'SimpleFormObject delegation' do 4 | let(:base_klass) { Class.new.tap{|k| k.include SimpleFormObject } } 5 | let(:klass) { base_klass } 6 | let(:instance) { klass.new } 7 | 8 | describe '.delegate_all' do 9 | let(:target) { double } 10 | 11 | before do 12 | klass.delegate_all to: target 13 | end 14 | 15 | it 'should delegate foo to the target' do 16 | expect(target).to receive(:foo) 17 | instance.foo 18 | 19 | expect(target).to receive(:foo).with([1,2,3,4]) 20 | instance.foo([1,2,3,4]) 21 | end 22 | end 23 | 24 | describe 'a more complex delegation' do 25 | let(:base_klass) do 26 | Class.new do 27 | include SimpleFormObject 28 | 29 | def initialize(target_object, attributes = {}) 30 | @target_object = target_object 31 | super(attributes) 32 | end 33 | 34 | attr_reader :target_object 35 | 36 | delegate_all to: :target_object 37 | end 38 | end 39 | 40 | let(:target) { double } 41 | let(:instance) { klass.new(target) } 42 | 43 | it 'should delegate foo to the target' do 44 | expect(target).to receive(:foo) 45 | instance.foo 46 | 47 | expect(target).to receive(:foo).with([1,2,3,4]) 48 | instance.foo([1,2,3,4]) 49 | end 50 | 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/model_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'SimpleFormObject' do 4 | let(:base_klass) { Class.new.tap{|k| k.include SimpleFormObject } } 5 | let(:klass) { base_klass } 6 | let(:instance) { klass.new } 7 | 8 | context 'when included' do 9 | it 'should have ActiveModel::Model' do 10 | expect(instance).to respond_to :valid? 11 | end 12 | end 13 | 14 | describe '.attribute' do 15 | let(:attribute) { :foo } 16 | 17 | before do 18 | klass.attribute :foo 19 | end 20 | 21 | it 'should respond to "attribute"' do 22 | expect(instance).to respond_to(attribute) 23 | end 24 | 25 | it 'should respond to "attribute="' do 26 | expect(instance).to respond_to("#{attribute}=") 27 | end 28 | 29 | describe 'type' do 30 | let(:types) { %i(boolean string email url tel password search text file hidden integer float decimal range datetime date time select radio_buttons check_boxes country time_zone) } 31 | 32 | let(:type) { :boolean } 33 | let(:attr) { :la } 34 | 35 | before do 36 | klass.attribute attr, type 37 | end 38 | 39 | it 'should return a fake column with the correct type' do 40 | expect(instance.column_for_attribute(attr).type).to eq type 41 | end 42 | 43 | it 'should say that the attribute exists' do 44 | expect(instance.has_attribute?(attr)).to eq true 45 | end 46 | end 47 | 48 | describe 'options' do 49 | let(:type) { :boolean } 50 | let(:attr) { :doh } 51 | 52 | before do 53 | klass.attribute attr, type, default: true 54 | end 55 | 56 | describe 'default' do 57 | it 'should set the attribute to the default' do 58 | expect(instance.doh).to eq true 59 | end 60 | 61 | context 'when a value is supplied in the initialization hash' do 62 | let(:instance) { klass.new(doh: false) } 63 | 64 | it 'should use the value in the hash' do 65 | expect(instance.doh).to eq false 66 | end 67 | end 68 | 69 | end 70 | end 71 | end 72 | 73 | describe '.model_name' do 74 | let(:klass) do 75 | class KlassForm 76 | include SimpleFormObject 77 | end 78 | 79 | KlassForm 80 | end 81 | 82 | it 'should return an ActiveModel::Name' do 83 | expect(klass.model_name).to be_a_kind_of ActiveModel::Name 84 | end 85 | 86 | it 'should remove Form from the name of the class' do 87 | expect(klass.model_name.name).to eq "Klass" 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simple_form_object' 2 | --------------------------------------------------------------------------------