├── lib ├── informal.rb └── informal │ ├── version.rb │ ├── model.rb │ └── model_no_init.rb ├── .gitignore ├── Gemfile ├── test ├── test_helper.rb ├── model_test.rb ├── model_no_init_test.rb └── model_test_cases.rb ├── Rakefile ├── LICENSE ├── informal.gemspec └── README.md /lib/informal.rb: -------------------------------------------------------------------------------- 1 | require "informal/model" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | -------------------------------------------------------------------------------- /lib/informal/version.rb: -------------------------------------------------------------------------------- 1 | module Informal 2 | VERSION = "0.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in informal.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | 3 | require "rubygems" 4 | require "bundler/setup" 5 | 6 | require "informal/model" 7 | require "informal/model_no_init" 8 | 9 | require File.expand_path(File.join(File.dirname(__FILE__), "model_test_cases")) 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | require 'rake/testtask' 5 | Rake::TestTask.new("test_all") do |t| 6 | t.test_files = FileList['test/*_test.rb'] 7 | t.verbose = false 8 | end 9 | 10 | desc "Run all tests" 11 | task :default => :test_all 12 | -------------------------------------------------------------------------------- /lib/informal/model.rb: -------------------------------------------------------------------------------- 1 | require "informal/model_no_init" 2 | 3 | module Informal 4 | module Model 5 | def self.included(klass) 6 | klass.class_eval do 7 | include ModelNoInit 8 | end 9 | end 10 | 11 | def initialize(attrs={}) 12 | self.attributes = attrs 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/model_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), "test_helper")) 2 | 3 | class ModelTest < Test::Unit::TestCase 4 | class Poro 5 | include Informal::Model 6 | attr_accessor :x, :y, :z 7 | validates_presence_of :x, :y, :z 8 | end 9 | def poro_class; Poro; end 10 | 11 | include ModelTestCases 12 | end 13 | -------------------------------------------------------------------------------- /test/model_no_init_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), "test_helper")) 2 | 3 | class ModelNoInitTest < Test::Unit::TestCase 4 | class KalEl 5 | attr_accessor :k 6 | def initialize(uber) 7 | @k = uber 8 | end 9 | end 10 | class Poro < KalEl 11 | include Informal::ModelNoInit 12 | attr_accessor :x, :y, :z 13 | validates_presence_of :x, :y, :z 14 | def initialize(attrs={}) 15 | super(true) 16 | self.attributes = attrs 17 | end 18 | end 19 | def poro_class; Poro; end 20 | 21 | include ModelTestCases 22 | 23 | def test_super 24 | assert @model.k 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/informal/model_no_init.rb: -------------------------------------------------------------------------------- 1 | require "active_model" 2 | module Informal 3 | module ModelNoInit 4 | def self.included(klass) 5 | klass.class_eval do 6 | extend ActiveModel::Naming 7 | include ActiveModel::Validations 8 | extend ClassMethods 9 | end 10 | end 11 | 12 | module ClassMethods 13 | if ActiveModel::VERSION::MINOR > 0 14 | def informal_model_name(name) 15 | @_model_name = ActiveModel::Name.new(self, nil, name) 16 | end 17 | end 18 | end 19 | 20 | def attributes=(attrs) 21 | attrs && attrs.each_pair { |name, value| self.send("#{name}=", value) } 22 | end 23 | 24 | def persisted? 25 | false 26 | end 27 | 28 | def new_record? 29 | true 30 | end 31 | 32 | def to_model 33 | self 34 | end 35 | 36 | def to_key 37 | [object_id] 38 | end 39 | 40 | def to_param 41 | nil 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2011 by Josh Susser 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. 22 | -------------------------------------------------------------------------------- /informal.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "informal/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "informal" 7 | s.version = Informal::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Josh Susser"] 10 | s.email = ["josh@hasmanythrough.com"] 11 | s.homepage = "https://github.com/joshsusser/informal" 12 | s.summary = %q{Easily use any Plain Old Ruby Object as the model for Rails form helpers.} 13 | s.description = %q{Easily use any Plain Old Ruby Object as the model for Rails form helpers.} 14 | 15 | s.rubyforge_project = "informal" 16 | 17 | s.files = `git ls-files`.split("\n") 18 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 19 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 20 | s.require_paths = ["lib"] 21 | 22 | s.add_dependency('activemodel', "~> 3.0") 23 | # s.add_dependency('activemodel', "~> 3.0.0") # to test w/o 3.1 only features 24 | # s.add_dependency('activemodel', "~> 3.1.0.rc4") # to test 3.1 only features, ex: informal_model_name 25 | end 26 | -------------------------------------------------------------------------------- /test/model_test_cases.rb: -------------------------------------------------------------------------------- 1 | module ModelTestCases 2 | include ActiveModel::Lint::Tests 3 | 4 | def setup 5 | @model = self.poro_class.new(:x => 1, :y => 2) 6 | end 7 | 8 | def teardown 9 | self.poro_class.instance_variable_set(:@_model_name, nil) 10 | end 11 | 12 | def test_new 13 | assert_equal 1, @model.x 14 | assert_equal 2, @model.y 15 | assert_nil @model.z 16 | end 17 | 18 | def test_new_with_nil 19 | assert_nothing_raised { self.poro_class.new(nil) } 20 | end 21 | 22 | def test_persisted 23 | assert !@model.persisted? 24 | end 25 | 26 | def test_new_record 27 | assert @model.new_record? 28 | end 29 | 30 | def test_to_key 31 | assert_equal [@model.object_id], @model.to_key 32 | end 33 | 34 | def test_naming 35 | assert_equal "Poro", @model.class.model_name.human 36 | end 37 | 38 | if ActiveModel::VERSION::MINOR > 0 39 | def test_model_name_override 40 | self.poro_class.informal_model_name("Alias") 41 | assert_equal "Alias", @model.class.model_name.human 42 | end 43 | end 44 | 45 | def test_validations 46 | assert @model.invalid? 47 | assert_equal [], @model.errors[:x] 48 | assert @model.errors[:z].any? {|err| err =~ /blank/} 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Informal 2 | 3 | Informal is a small gem that enhances a Plain Old Ruby Object so it can be used 4 | with Rails 3 form helpers in place of an ActiveRecord model. It works with the 5 | Rails `form_for` helper, and `simple_form` as well. 6 | 7 | Here's a quick (and slightly insane) example: 8 | 9 | # models/command.rb 10 | require "informal" 11 | class Command 12 | include Informal::Model 13 | attr_accessor :command, :args 14 | validates_presence_of :command 15 | def run; `#{command} #{args}`; end 16 | end 17 | 18 | # views/commands/new.html.erb 19 | <%= form_for @command do |form| %> 20 | <%= form.text_field :command %> 21 | <%= form.text_field :args %> 22 | <%= form.submit "Do It!" %> 23 | <% end %> 24 | 25 | # controllers/commands_controller.rb 26 | def create 27 | command = Command.new(params[:command]) 28 | if command.valid? 29 | command.run 30 | end 31 | end 32 | 33 | ## Installation 34 | 35 | It's a Ruby gem, so just install it with `gem install informal`, add it to your 36 | bundler Gemfile, or do whatever you like to do with gems. There is nothing to 37 | configure. 38 | 39 | ## Usage 40 | 41 | The insanity of the above example aside, Informal is pretty useful for creating 42 | simple RESTful resources that don't map directly to ActiveRecord models. It 43 | evolved from handling login credentials to creating model objects that were 44 | stored in a serialized attribute of a parent resource. 45 | 46 | In many ways using an informal model is just like using an AR model in 47 | controllers and views. The biggest difference is that you don't `save` an 48 | informal object, but you can add validations and check if it's `valid?`. If 49 | there are any validation errors, the object will have all the usual error 50 | decorations so that error messages will display properly in the form view. 51 | 52 | ### Initialization, #super and attributes 53 | 54 | If you include `Informal::Model`, your class automatically gets an 55 | `#initialize` method that takes a params hash and calls setters for all 56 | attributes in the hash. If your model class inherits from a class that has its 57 | own `#initialize` method that needs to get the super call, you should instead 58 | include `Informal::ModelNoInit`, which does not create an `#initialize` method. 59 | Make your own `#initialize` method, and in that you can assign the attributes 60 | using the `#attributes=` method and also call super with whatever args are 61 | needed. 62 | 63 | ## Overriding the `model_name` 64 | 65 | If you name your model `InformalCommand`, form params get passed to your controller 66 | in the `params[:informal_command]` hash. As that's a bit ugly and perhaps doesn't 67 | play well with standing in for a real ActiveRecord model, Informal provides a 68 | method to override the model name. 69 | 70 | class InformalCommand 71 | informal_model_name "Command" 72 | # ... 73 | end 74 | 75 | Note: the `informal_model_name` feature is available only in Rails 3.1 or greater 76 | (unless somebody back-ports the required API change to 3.0.x). 77 | 78 | ## Idiosyncrasies 79 | 80 | The standard way that Rails generates ids for new records is to name them like 81 | `command_new`, as opposed to `command_17` for persisted records. I've found that 82 | when using informal models I often want more than one per page, and it's helpful 83 | to have a unique id for JavaScript to use. Therefore Informal uses the model's 84 | `object_id` to get a unique id for the record. Those ids in the DOM will look like 85 | `command_2157193640`, which would be scary if you did anything with those memory 86 | addresses except use them for attaching scripts. 87 | 88 | ## License 89 | 90 | Copyright © 2011 Josh Susser. Released under the MIT License. See the LICENSE file. 91 | --------------------------------------------------------------------------------