commit d910e0e83798b9f142bca663222d01e5262baa21 Author: John Bintz Date: Tue Jul 9 17:27:58 2013 -0400 initial commit, yay super-easy form objects diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d87d4be --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +*.gem +*.rbc +.bundle +.config +.yardoc +Gemfile.lock +InstalledFiles +_yardoc +coverage +doc/ +lib/bundler/man +pkg +rdoc +spec/reports +test/tmp +test/version_tmp +tmp diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..abd70bd --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +# Specify your gem's dependencies in carapace.gemspec +gemspec diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..ebe6135 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2013 John Bintz + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fbc0251 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# CANDY WRAPPER + +Use form objects with ease. Plugs into `inherited_resources` easily: + +``` ruby +# app/models/database_object.rb +class DatabaseObject < Persistence::Base + # ... persistence and relationships only ... +end +``` + +``` ruby +# app/controllers/database_objects_controller.rb + +class DatabaseObjectsController < ApplicationController + inherit_resources + + # for the parts of inherited_resources that actually persist models, + # ensure that persistence takes placed within a CandyWrapper::ModelWrapper + # form object. Those respond to save, assign_attributes, and update_attributes. + wrap_in_form_object! +end +``` + +``` ruby +# app/form_objects/database_object_form_object.rb + +class DatabaseObjectFormObject < CandyWrapper::ModelWrapper + def complex_parameter=(database_object, parameter_value) + # do complex formatting here, probably for nested objects + # database_object will be saved by this point + end + + # perform these actions before database_object is saved + before_wrapped_save :process_first_parameter + + def process_first_parameter=(database_object, parameter_value) + # use normal accessors to set properties on database_object, it will be + # saved when all before_wrapped_saved methods are run + end + + # there is no guarantee in the order that these will run, don't make them + # depend on each other! +end +``` + +## Coming soon! + +* Form objects that hang off of models as if they were relationships +* A better readme! + diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..2995527 --- /dev/null +++ b/Rakefile @@ -0,0 +1 @@ +require "bundler/gem_tasks" diff --git a/candy_wrapper.gemspec b/candy_wrapper.gemspec new file mode 100644 index 0000000..ef67ccc --- /dev/null +++ b/candy_wrapper.gemspec @@ -0,0 +1,26 @@ +# coding: utf-8 +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'candy_wrapper/version' + +Gem::Specification.new do |spec| + spec.name = "candy_wrapper" + spec.version = CandyWrapper::VERSION + spec.authors = ["John Bintz"] + spec.email = ["john@coswellproductions.com"] + spec.description = %q{Use form objects with ease.} + spec.summary = %q{Use form objects with ease.} + spec.homepage = "" + spec.license = "MIT" + + spec.files = `git ls-files`.split($/) + spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } + spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) + spec.require_paths = ["lib"] + + spec.add_development_dependency "bundler", "~> 1.3" + spec.add_development_dependency "rake" + spec.add_development_dependency "rspec" + spec.add_dependency 'activesupport' +end + diff --git a/lib/candy_wrapper.rb b/lib/candy_wrapper.rb new file mode 100644 index 0000000..3a25d2c --- /dev/null +++ b/lib/candy_wrapper.rb @@ -0,0 +1,4 @@ +require "candy_wrapper/version" +require "candy_wrapper/model_wrapper" +require "candy_wrapper/inherited_resources" + diff --git a/lib/candy_wrapper/inherited_resources.rb b/lib/candy_wrapper/inherited_resources.rb new file mode 100644 index 0000000..2d7c7fa --- /dev/null +++ b/lib/candy_wrapper/inherited_resources.rb @@ -0,0 +1,59 @@ +require 'active_support/concern' + +module CandyWrapper + module InheritedResources + extend ActiveSupport::Concern + + module ClassMethods + def __candy_wrapper__ + @__candy_wrapper__ + end + + def wrap_in_form_object(options = {}) + @__candy_wrapper__ = { + with: resource_class.name + "FormObject", + only: [ :create, :update ] + }.merge(options) + end + + def wrap_in_form_object!(*args) + wrap_in_form_object(*args) + end + end + + protected + def build_resource + get_resource_ivar || set_resource_ivar( + if self.class.__candy_wrapper__[:only].include?(action_name.to_sym) + object = end_of_association_chain.send(method_for_build) + else + super + end + ) + end + + def create_resource(object) + if self.class.__candy_wrapper__[:only].include?(action_name.to_sym) + wrapped_object = candy_wrapper_class.new(object, *resource_params) + + wrapped_object.save + else + super + end + end + + def update_resource(object, attributes) + if self.class.__candy_wrapper__[:only].include?(action_name.to_sym) + wrapped_object = candy_wrapper_class.new(object) + wrapped_object.update_attributes(*attributes) + else + super + end + end + + def candy_wrapper_class + @__candy_wrapper_class__ ||= self.class.__candy_wrapper__[:with].constantize + end + end +end + diff --git a/lib/candy_wrapper/model_wrapper.rb b/lib/candy_wrapper/model_wrapper.rb new file mode 100644 index 0000000..d550167 --- /dev/null +++ b/lib/candy_wrapper/model_wrapper.rb @@ -0,0 +1,94 @@ +require 'delegate' + +module CandyWrapper + class ModelWrapper < SimpleDelegator + def self.inherited(klass) + klass.send(:extend, ClassMethods) + + if klass.name && original = klass.name[/^(.*)FormObject$/, 1] + klass.wraps original.constantize + end + end + + module ClassMethods + def wraps(klass = nil) + if klass + @__wraps__ = klass + else + @__wraps__ + end + end + + def before_wrapped_save(*args) + @__before_wrapped_save__ ||= [] + + if args.empty? + @__before_wrapped_save__ + else + @__before_wrapped_save__ += args + end + end + end + + def __wraps__ + self.class.wraps + end + + def initialize(object, params = {}) + @__object__ = object + + assign_attributes(params) + end + + def __getobj__ + @__object__ + end + + def assign_attributes(attributes) + @__params__ = attributes.dup + end + + def update_attributes(attributes) + assign_attributes(attributes) + + save + end + + def save + self.class.before_wrapped_save.each do |before| + send("#{before}=", @__object__, @__params__[before]) + end + + @__object__.assign_attributes(__object_params__) + + if result = @__object__.save + setters_and_setter_params.each do |setter, setter_param| + send(setter, @__object__, @__params__[setter_param]) + end + end + end + + def __object_params__ + object_params = @__params__.dup + setter_params.each { |key| object_params.delete(key) } + object_params + end + + def setter_params + @__setter_params__ ||= setters.collect { |param| param.gsub('=', '').to_sym } + end + + def setters + @__setters__ ||= (my_instance_methods.collect(&:to_s).find_all { |param| param[/=\Z/] } - self.class.before_wrapped_save) + end + + def setters_and_setter_params + setters.zip(setter_params) + end + + def my_instance_methods + @my_instance_methods ||= (self.class.instance_methods - ModelWrapper.instance_methods) + end + end +end + diff --git a/lib/candy_wrapper/version.rb b/lib/candy_wrapper/version.rb new file mode 100644 index 0000000..bee3ecf --- /dev/null +++ b/lib/candy_wrapper/version.rb @@ -0,0 +1,3 @@ +module CandyWrapper + VERSION = "0.0.1" +end diff --git a/spec/candy_wrapper/model_wrapper_spec.rb b/spec/candy_wrapper/model_wrapper_spec.rb new file mode 100644 index 0000000..b6a5fc4 --- /dev/null +++ b/spec/candy_wrapper/model_wrapper_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' +require 'candy_wrapper/model_wrapper' + +describe CandyWrapper::ModelWrapper do + describe '#update_attributes' do + let(:model_class) { + Class.new do + attr_reader :saved, :attributes + + def save + @saved = true + end + + def assign_attributes(attributes) + @attributes = attributes + end + end + } + + let(:wrap_class) { + klass = Class.new(CandyWrapper::ModelWrapper) do + attr_reader :resource, :value + + def setter=(resource, value) + @resource, @value = resource, value + end + end + + klass.send(:wraps, model_class) + klass + } + + let(:setter_value) { 'setter value' } + let(:base_attributes) { { :base => 'base' } } + let(:attributes) { { :setter => setter_value }.merge(base_attributes) } + + it 'should save the model and trigger the setters on the wrapper' do + model = model_class.new + wrap = wrap_class.new(model) + + wrap.update_attributes(attributes) + + model.saved.should be_true + model.attributes.should be == base_attributes + + wrap.resource.should == model + wrap.value.should == setter_value + end + end +end + diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..68cb5f7 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,4 @@ +require 'rspec' + +$: << File.expand_path('../../lib') +