├── .rvmrc ├── .gitignore ├── spec ├── fixtures │ ├── root_partial.bldr │ ├── partial.json.bldr │ ├── some │ │ └── include.json.bldr │ ├── ivar.bldr │ ├── partial_with_locals.json.bldr │ ├── templates │ │ └── rails │ │ │ └── people │ │ │ ├── index.json.bldr │ │ │ └── show.json.bldr │ ├── nested_ivars.bldr │ ├── root_template.json.bldr │ └── nested_objects.json.bldr ├── models │ ├── song.rb │ ├── comment.rb │ ├── person.rb │ └── post.rb ├── functional │ ├── set_current_object_spec.rb │ ├── handlers_spec.rb │ ├── params_spec.rb │ ├── helpers_spec.rb │ └── tilt_template_spec.rb ├── spec_helper.rb ├── unit │ ├── bldr_spec.rb │ └── node_spec.rb └── integration │ ├── rails_32_spec.rb │ └── sinatra_spec.rb ├── .travis.yml ├── lib ├── bldr │ ├── version.rb │ ├── railtie.rb │ ├── template.rb │ └── node.rb ├── action_view │ └── template │ │ └── handlers │ │ └── bldr.rb ├── bldr.rb └── sinatra │ └── bldr.rb ├── Rakefile ├── Gemfile ├── perf ├── bench ├── benchmark.rb └── results.txt ├── MIT-LICENSE ├── bldr.gemspec ├── HISTORY.md └── README.md /.rvmrc: -------------------------------------------------------------------------------- 1 | rvm 1.9.3 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rspec 2 | Gemfile.lock 3 | pkg 4 | /log 5 | coverage 6 | -------------------------------------------------------------------------------- /spec/fixtures/root_partial.bldr: -------------------------------------------------------------------------------- 1 | template 'fixtures/partial.json.bldr' -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.9.3 3 | - ruby-head 4 | - rbx-19mode 5 | -------------------------------------------------------------------------------- /lib/bldr/version.rb: -------------------------------------------------------------------------------- 1 | 2 | module Bldr 3 | VERSION = '1.0.1' 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/partial.json.bldr: -------------------------------------------------------------------------------- 1 | object do 2 | attribute(:foo) { "bar" } 3 | end -------------------------------------------------------------------------------- /spec/fixtures/some/include.json.bldr: -------------------------------------------------------------------------------- 1 | object do 2 | attribute(:foo) { "bar" } 3 | end -------------------------------------------------------------------------------- /spec/fixtures/ivar.bldr: -------------------------------------------------------------------------------- 1 | object :person => @person do 2 | attributes :name, :age 3 | end 4 | -------------------------------------------------------------------------------- /spec/fixtures/partial_with_locals.json.bldr: -------------------------------------------------------------------------------- 1 | object :name => obj do 2 | attribute :foo 3 | end -------------------------------------------------------------------------------- /spec/fixtures/templates/rails/people/index.json.bldr: -------------------------------------------------------------------------------- 1 | 2 | collection @people do 3 | attribute :name 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/nested_ivars.bldr: -------------------------------------------------------------------------------- 1 | object nil do 2 | object :person => @person do 3 | attributes :name, :age 4 | end 5 | end -------------------------------------------------------------------------------- /spec/fixtures/root_template.json.bldr: -------------------------------------------------------------------------------- 1 | 2 | object do 3 | attribute(:name) { name } 4 | attribute(:age) { age } 5 | end 6 | -------------------------------------------------------------------------------- /spec/models/song.rb: -------------------------------------------------------------------------------- 1 | class Song 2 | attr_accessor :name 3 | def initialize(name) 4 | @name = name 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # require 'bundler' 2 | # Bundler::GemHelper.install_tasks 3 | 4 | require 'rspec/core/rake_task' 5 | RSpec::Core::RakeTask.new(:spec) 6 | 7 | task :default => :spec 8 | -------------------------------------------------------------------------------- /spec/fixtures/templates/rails/people/show.json.bldr: -------------------------------------------------------------------------------- 1 | 2 | attribute(:id) { params[:id] } 3 | attribute(:name) { @person.name } 4 | 5 | if params[:use_boss_helper] 6 | attribute(:boss) { boss?(@person) } 7 | end 8 | -------------------------------------------------------------------------------- /spec/models/comment.rb: -------------------------------------------------------------------------------- 1 | 2 | class Comment 3 | attr_accessor :body, :name, :email 4 | 5 | def initialize(body = nil, name = nil, email = nil) 6 | @body, @name, @email = body, name, email 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/models/person.rb: -------------------------------------------------------------------------------- 1 | 2 | class Person 3 | 4 | attr_accessor :name, :age, :friends 5 | 6 | def initialize(name = nil, age = nil, friends = nil) 7 | @name, @age, @friends = name, age, friends 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'coveralls', require: false 6 | gem "rake" 7 | 8 | group :test do 9 | gem 'rails', '~>3.2' 10 | gem "rspec", ">= 2.6.0" 11 | gem 'debugger', :platform => :mri_19 12 | end 13 | -------------------------------------------------------------------------------- /lib/bldr/railtie.rb: -------------------------------------------------------------------------------- 1 | module Bldr 2 | class Railtie < Rails::Railtie 3 | initializer 'bldr.initialize' do |app| 4 | ActiveSupport.on_load(:action_view) do 5 | require 'action_view/template/handlers/bldr' 6 | end 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /spec/fixtures/nested_objects.json.bldr: -------------------------------------------------------------------------------- 1 | 2 | object :person => bert do 3 | attributes :name, :age 4 | attribute :name_age do |person| 5 | "#{person.name} #{person.age}" 6 | end 7 | 8 | object :friend => ernie do 9 | attributes :name, :age 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/models/post.rb: -------------------------------------------------------------------------------- 1 | class Author < Struct.new(:name) 2 | end 3 | 4 | class Post 5 | attr_accessor :title, :body, :comments, :author 6 | 7 | def initialize(title = nil, body = nil) 8 | @title, @body = title, body 9 | @comments = [] 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /spec/functional/set_current_object_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'setting the current object in an object block' do 4 | it 'sets the object' do 5 | klass = Struct.new(:name) 6 | person = klass.new('alex') 7 | Bldr::Node.new(nil, locals: {person: person}) do 8 | object person do 9 | attributes(:name) 10 | end 11 | end.result.should == {name: 'alex'} 12 | end 13 | end -------------------------------------------------------------------------------- /perf/bench: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | output = `ruby -r bench_press ./benchmark.rb` 4 | 5 | results = File.open(File.expand_path(File.dirname(__FILE__)) + "/results.txt", "a") do |f| 6 | f.puts "\n" 7 | f.puts "=" * 80 8 | f.puts "Spec run time: #{Time.now}" 9 | f.puts "Git SHA: #{`cat ../.git/\`cat ../.git/HEAD | awk '{print $2}'\``.chomp}" 10 | f.puts "Latest Tag: #{`git tag | tail -n 1`}" 11 | f.puts output 12 | end -------------------------------------------------------------------------------- /spec/functional/handlers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Defining different types of handlers" do 4 | 5 | describe "Time" do 6 | it "uses the handler for Time when rendering" do 7 | Bldr.handler Time do |time| 8 | "bar" 9 | end 10 | 11 | output = {:foo => 'bar'} 12 | node = Bldr::Node.new do 13 | object { attribute(:foo) { Time.now } } 14 | end 15 | 16 | node.result.should == output 17 | end 18 | end 19 | 20 | end -------------------------------------------------------------------------------- /perf/benchmark.rb: -------------------------------------------------------------------------------- 1 | require 'benchmark' 2 | require File.expand_path(File.dirname(__FILE__) + '/../lib/bldr') 3 | 4 | class Person 5 | attr_reader :name 6 | def initialize(name) 7 | @name = name 8 | end 9 | end 10 | 11 | Benchmark.bm(20) do |bm| 12 | i = 1000 13 | $guy = Person.new "john" 14 | $friends = [Person.new("jim"), Person.new("bob")] 15 | puts "i = #{i}" 16 | 17 | bm.report "node with collection" do 18 | i.times do 19 | Bldr::Node.new do 20 | object :dude => $guy do 21 | attributes :name 22 | 23 | collection :friends => $friends do 24 | attributes :name 25 | end 26 | end 27 | end 28 | end 29 | end 30 | end -------------------------------------------------------------------------------- /spec/functional/params_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Bldr 4 | describe Node, 'access to the params hash' do 5 | let(:params) { {foo: 'bar'} } 6 | let(:parent) { Struct.new(:params).new(params) } 7 | 8 | it 'has access in the root node' do 9 | Node.new(nil, parent: parent) do 10 | attribute(:foo) { params[:foo] } 11 | end.result.should == { 12 | foo: 'bar' 13 | } 14 | end 15 | 16 | it 'has access in child nodes' do 17 | Node.new(nil, parent: parent) do 18 | object(:foo) do 19 | attribute(:baz) { params[:foo] } 20 | end 21 | end.result.should == { 22 | foo: {baz: 'bar'} 23 | } 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /perf/results.txt: -------------------------------------------------------------------------------- 1 | 2 | ================================================================================ 3 | Spec run time: 2011-09-09 11:57:07 -0700 4 | Git SHA: e74a003d8653a4b6cfe0f14e043c554e636f55b5 5 | Latest Tag: v0.2.0 6 | user system total real 7 | i = 1000 8 | node with collection 0.080000 0.000000 0.080000 ( 0.076949) 9 | 10 | ================================================================================ 11 | Spec run time: 2011-09-19 13:48:53 -0700 12 | Git SHA: 09214bfe2c38107ba68dd4c14f17e81d4f5bef72 13 | Latest Tag: v0.2.0 14 | user system total real 15 | i = 1000 16 | node with collection 0.070000 0.010000 0.080000 ( 0.076655) 17 | -------------------------------------------------------------------------------- /lib/action_view/template/handlers/bldr.rb: -------------------------------------------------------------------------------- 1 | module ActionView::Template::Handlers 2 | class Bldr 3 | 4 | # @param [ActionView::Template] template the template instance 5 | # @return [String] the rendered ruby code string to render the template 6 | def self.call(template, opts = {}) 7 | source = if template.source.empty? 8 | File.read(template.identifier) 9 | else 10 | template.source 11 | end 12 | 13 | %{ 14 | node = ::Bldr::Node.new(nil, parent: self, root: true) { 15 | #{source} 16 | } 17 | MultiJson.encode node.result 18 | } 19 | end 20 | 21 | end 22 | end 23 | 24 | ActionView::Template.register_template_handler :bldr, ActionView::Template::Handlers::Bldr 25 | -------------------------------------------------------------------------------- /lib/bldr/template.rb: -------------------------------------------------------------------------------- 1 | require 'tilt' 2 | 3 | module Bldr 4 | 5 | # This class is required for Tilt compatibility 6 | class Template < Tilt::Template 7 | 8 | self.default_mime_type = 'application/json' 9 | 10 | def initialize_engine 11 | require_template_library 'bldr' 12 | end 13 | 14 | def self.engine_initialized? 15 | defined? ::Bldr 16 | end 17 | 18 | # Called at the end of Tilt::Template#initialize. 19 | # Use this method to access or mutate any state available to Tilt::Template 20 | def prepare 21 | # We get NotImplementedError by Tilt when we don't have this method 22 | end 23 | 24 | def precompiled_template(locals) 25 | data.to_s 26 | end 27 | end 28 | 29 | Tilt.register 'bldr', Bldr::Template 30 | end 31 | -------------------------------------------------------------------------------- /lib/bldr.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require 'tilt' 3 | rescue LoadError 4 | end 5 | 6 | require 'multi_json' 7 | require 'bldr/node' 8 | require 'bldr/railtie' if defined?(Rails) 9 | 10 | if defined?(Tilt) 11 | require 'bldr/template' 12 | end 13 | 14 | module Bldr 15 | class << self 16 | 17 | def json_encoder=(encoder) 18 | MultiJson.engine = encoder 19 | end 20 | 21 | # Define a custom handler. 22 | # 23 | # @example Over-riding BSON::ObjectId 24 | # Bldr.handler BSON::ObjectId do |value| 25 | # val.to_s # => "4e77a682364141ecf5000002" 26 | # end 27 | # 28 | # @param [Class] klass The klass name of the class to match 29 | # @param [Proc] block The code to execute to properly format the data 30 | def handler(klass,&block) 31 | raise(ArgumentError, "You must pass a Proc") if block.nil? 32 | raise(ArgumentError, "You must pass only one argument to the Proc") unless block.arity == 1 33 | 34 | handlers[klass] = block 35 | end 36 | 37 | def handlers 38 | @handlers ||= {} 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2013 Alex Sharp 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /bldr.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "bldr/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "bldr" 7 | s.version = Bldr::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Alex Sharp"] 10 | s.email = ["ajsharp@gmail.com"] 11 | s.homepage = "https://github.com/ajsharp/bldr" 12 | s.summary = %q{Templating library with a simple, minimalist DSL.} 13 | s.description = %q{Provides a simple and intuitive templating DSL for serializing objects to JSON.} 14 | 15 | s.rubyforge_project = "bldr" 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 'multi_json' 23 | 24 | s.add_development_dependency 'json_pure' 25 | s.add_development_dependency 'sinatra' 26 | s.add_development_dependency 'tilt' 27 | s.add_development_dependency 'yajl-ruby', '>= 1.0' 28 | s.add_development_dependency 'actionpack' 29 | end 30 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | if ENV['TRAVIS'] 4 | require 'coveralls' 5 | Coveralls.wear! 6 | end 7 | 8 | require 'rspec' 9 | 10 | require 'yajl' 11 | require 'tilt' 12 | require 'sinatra/base' 13 | 14 | $:.unshift(File.dirname(File.expand_path(__FILE__))) 15 | 16 | require File.join(File.dirname(File.expand_path(__FILE__)), "..", "lib", "bldr") 17 | 18 | Dir['spec/models/*'].each { |f| require File.expand_path(f) } 19 | 20 | require 'sinatra/bldr' 21 | 22 | class BaseTestApp < Sinatra::Base 23 | register Sinatra::Bldr 24 | 25 | set :views, File.expand_path(__FILE__ + '/../..') 26 | disable :show_exceptions 27 | enable :raise_errors 28 | end 29 | 30 | RSpec.configure do |c| 31 | def node_wrap(*args, &block) 32 | Bldr::Node.new(*args, &block) 33 | end 34 | 35 | # Parse some json and return a ruby object 36 | def parse_json(str) 37 | Yajl::Parser.parse(str) 38 | end 39 | alias :decode :parse_json 40 | 41 | # Jsonify a ruby object 42 | def jsonify(hash) 43 | Yajl::Encoder.encode(hash) 44 | end 45 | 46 | # render the String template and compare to the jsonified hash 47 | def it_renders_template_to_hash(template,hash) 48 | tpl = Bldr::Template.new {template} 49 | result = tpl.render(Bldr::Node.new) 50 | result.should == jsonify(hash) 51 | end 52 | 53 | c.after do 54 | Bldr.handlers.clear 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/sinatra/bldr.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/base' 2 | 3 | module Sinatra 4 | 5 | module Bldr 6 | module Helpers 7 | # Wrapper for Tilt's `render` method 8 | # 9 | # We use this to properly set the scope the template gets rendered 10 | # within to a `Bldr::Node` object and pass in local variables. 11 | # 12 | # @param [String, Symbol] template the template to render 13 | # Can be a relative file location or a string template. 14 | # The template may also be passed in as the block argument 15 | # to this method, in which case, template argument is nil. 16 | # 17 | # @example Render a template in a file 18 | # get '/users/:id' do 19 | # user = User.find(params['id']) 20 | # bldr :'users/public.bldr', :locals => {:user => user} 21 | # end 22 | # 23 | # @param [Hash] opts a hash of options 24 | # @option opts [Hash] :locals a hash of local variables to be used in the template 25 | def bldr(template, opts = {}, &block) 26 | opts[:parent] = self 27 | opts[:scope] = ::Bldr::Node.new(nil, opts.merge(:views => (settings.views || "./views"))) 28 | 29 | locals = opts.delete(:locals) || {} 30 | 31 | # @todo add support for alternate formats, like plist 32 | MultiJson.encode render(:bldr, template, opts, locals, &block).result 33 | end 34 | end 35 | 36 | def self.registered(app) 37 | app.helpers Helpers 38 | end 39 | end 40 | 41 | register Bldr 42 | end 43 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## 1.0.1 2 | * [#32](https://github.com/ajsharp/bldr/pull/32) Improved `object` syntax 3 | 4 | ## 1.0.0 5 | * Full Rails 3 support 6 | * [#31](https://github.com/ajsharp/bldr/pull/31) BREAKING: Properly handle nil objects 7 | 8 | ## 0.7.0 (2013-02-24) 9 | * Full support for instance variables 10 | * Breaking change: Node#attribute block syntax no longer inherits 11 | context from current_object. See 941608e7 for more 12 | * Block variables for `object` and `collection` methods 13 | * Breaking change: Drop ruby 1.8 support 14 | 15 | ## 0.6.1 (2013-02-18) -- yanked 16 | * Feature: Add the ability to access instance variables set in sinatra 17 | actions in bldr templates. 18 | 19 | ## 0.6.0 (2012-xx-xx) 20 | * Feature: Add the ability to pass-through objects directly to `object` and 21 | `collection` DSL methods 22 | 23 | ## 0.5.5 (2012-05-15) 24 | * Bug: Allow .bldr extensions at the end of partial template names 25 | * Bug: `#attribute` DSL method returns self, allowing use at top level 26 | 27 | ## 0.5.4 (2012-04-24) 28 | * Fix bug to allow using `template` method at the root of a bldr template 29 | * Add `locals` reader method to allow access to locals passed into a bldr template 30 | 31 | ## 0.5.3 32 | * Add ability to use `attribute` method at the root-level in a bldr template 33 | * Fix for when partials return nil (#19) 34 | 35 | ## 0.5.0 (2012-02-08) 36 | * Add support "partials" (@ihunter) 37 | 38 | ## 0.2.0 (2011-09-09) 39 | * Add new `attribute` inferred object syntax (@ihunter) 40 | 41 | ## 0.1.2 (2011-09-08) 42 | * Return an empty collection when a nil value is passed to `collection` method 43 | -------------------------------------------------------------------------------- /spec/functional/helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Bldr 4 | describe Node, 'delegating helper methods' do 5 | before do 6 | # mock version of a module of methods that would be attached 7 | # to an ActionView::Base instance 8 | helpers = Module.new do 9 | def my_helper 10 | 'my helper' 11 | end 12 | 13 | def helper_with_args_and_block(one, two) 14 | yield if block_given? 15 | end 16 | end 17 | 18 | # set up a mock ActionView::Base instance 19 | mock_action_view = Struct.new(:helpers) 20 | @view = mock_action_view.new(helpers) 21 | @view.extend(helpers) 22 | 23 | @node = Node.new(nil, root: true, parent: @view) 24 | end 25 | 26 | it 'delegates the methods to the parent object' do 27 | @view.should_receive(:my_helper) 28 | @node.my_helper 29 | end 30 | 31 | it 'gives access to helper methods to child nodes' do 32 | node = Node.new(nil, root: true, parent: @view) do 33 | object(:foo => Object.new) do 34 | attribute(:bar) { my_helper } 35 | end 36 | end 37 | node.result.should == {foo: {bar: 'my helper'}} 38 | end 39 | 40 | it 'assigns opts[:parent] as a @view instance variable' do 41 | @node.instance_variable_get(:@view).should == @view 42 | end 43 | 44 | it 'delegates arguments and blocks to the parent' do 45 | lam = lambda { } 46 | @view.should_receive(:helper_with_args_and_block).with(1, 2, lam) 47 | @node.helper_with_args_and_block(1, 2, lam) 48 | end 49 | 50 | it 'defines helper methods on a per-instance basis' do 51 | @node.methods.should include :my_helper 52 | 53 | new_parent = Struct.new(nil).new 54 | Node.new(nil, root: true, parent: new_parent).methods.should_not include :my_helper 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/unit/bldr_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "the json encoding library" do 4 | it "uses yajl by default" do 5 | MultiJson.engine.should == MultiJson::Adapters::Yajl 6 | end 7 | 8 | it "allows changing the json encoder to json pure" do 9 | Bldr.json_encoder = :json_pure 10 | MultiJson.engine.should == MultiJson::Adapters::JsonPure 11 | end 12 | 13 | it "allows changing the json encoder to the json gem" do 14 | Bldr.json_encoder = :json_gem 15 | MultiJson.engine.should == MultiJson::Adapters::JsonGem 16 | end 17 | 18 | end 19 | 20 | describe "defining custom handlers" do 21 | 22 | describe "erroneously" do 23 | 24 | it "errors when you don't pass a class arg" do 25 | expect { 26 | Bldr.handler {|foo| 'foo' } 27 | }.to raise_error(ArgumentError) 28 | end 29 | 30 | it "errors when you don't pass a block" do 31 | expect { 32 | Bldr.handler(Object) 33 | }.to raise_error(ArgumentError, "You must pass a Proc") 34 | end 35 | 36 | it "errors when no args are passed to the block" do 37 | expect { 38 | Bldr.handler(Object) do 39 | end 40 | }.to raise_error(ArgumentError, "You must pass only one argument to the Proc") 41 | end 42 | 43 | it "errors when 2 args are passed to the block" do 44 | expect { 45 | Bldr.handler(Object) do |one,two| 46 | end 47 | }.to raise_error(ArgumentError, "You must pass only one argument to the Proc") 48 | end 49 | 50 | end 51 | 52 | describe "successfully" do 53 | 54 | it "adds the handler to the collection for the specific Class" do 55 | Bldr.handler(Object) {|o|} 56 | Bldr.handlers[Object].should respond_to(:call) 57 | end 58 | 59 | it "assigns the lambda passed in" do 60 | code = lambda {|foo| "foo" } 61 | Bldr.handler(Time,&code) 62 | Bldr.handlers[Time].should == code 63 | end 64 | 65 | end 66 | 67 | end 68 | -------------------------------------------------------------------------------- /spec/integration/rails_32_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rails' 3 | require 'action_controller/railtie' 4 | require 'bldr/railtie' 5 | 6 | describe 'a template for a rails controller' do 7 | TestRailsApp = Class.new(Rails::Application) do 8 | routes.append do 9 | resources :people 10 | end 11 | 12 | config.secret_token = "secret" 13 | config.session_store = :disabled 14 | config.active_support.deprecation = nil 15 | config.middleware.delete 'ActionDispatch::Session::CookieStore' 16 | end 17 | 18 | class PeopleController < ActionController::Base 19 | helper_method :boss? 20 | 21 | # GET /people 22 | def index 23 | @people = [Person.new('Dave Chappelle'), Person.new('Chris Rock')] 24 | render 'spec/fixtures/templates/rails/people/index', handlers: [:bldr], formats: [:json] 25 | end 26 | 27 | # GET /people/:id 28 | def show 29 | @person = Person.new('Dave Chappelle') 30 | render 'spec/fixtures/templates/rails/people/show', handlers: [:bldr], formats: [:json] 31 | end 32 | 33 | private 34 | def boss?(person) 35 | person.name == 'Dave Chappelle' 36 | end 37 | end 38 | 39 | TestRailsApp.initialize! 40 | 41 | def app 42 | TestRailsApp.app 43 | end 44 | 45 | def get(url) 46 | Rack::MockRequest.new(app).get(url) 47 | end 48 | 49 | def decode(d) 50 | MultiJson.decode(d) 51 | end 52 | 53 | it 'returns 200' do 54 | get('/people').status.should == 200 55 | end 56 | 57 | it 'returns a json response body' do 58 | decode(get('/people').body).should == [ 59 | {'name' => 'Dave Chappelle'}, 60 | {'name' => 'Chris Rock'} 61 | ] 62 | end 63 | 64 | it 'returns json content type' do 65 | get('/people').content_type.should =~ %r{application/json} 66 | end 67 | 68 | it 'has access to controller helper methods' do 69 | response = get('/people/123?use_boss_helper=true') 70 | response.status.should == 200 71 | decode(response.body).should == { 72 | 'id' => '123', 73 | 'name' => 'Dave Chappelle', 74 | 'boss' => true 75 | } 76 | end 77 | 78 | it 'has access to params' do 79 | decode(get('/people/123').body).should == { 80 | 'id' => '123', 81 | 'name' => 'Dave Chappelle' 82 | } 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /spec/integration/sinatra_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | 4 | describe "Using Bldr with a sinatra app" do 5 | require 'sinatra/bldr' 6 | 7 | class TestApp < Sinatra::Base 8 | register Sinatra::Bldr 9 | 10 | set :views, File.expand_path(__FILE__ + '/../..') 11 | disable :show_exceptions 12 | enable :raise_errors 13 | 14 | get '/people/:id' do 15 | @alex = Person.new("alex", 25) 16 | 17 | tpl = %{ 18 | object(:person => @alex) do 19 | attribute(:id) { params['id'] } 20 | attribute :name 21 | end 22 | } 23 | status 200 24 | bldr tpl 25 | end 26 | 27 | get '/' do 28 | alex = Person.new("alex", 25) 29 | tpl = <<-RUBY 30 | object(:dude => alex) do 31 | attributes :name, :age 32 | end 33 | RUBY 34 | 35 | status(200) 36 | bldr tpl, :locals => {:alex => alex} 37 | end 38 | 39 | get '/collections' do 40 | alex = Person.new('alex', 25) 41 | 42 | tpl = <<-RUBY 43 | object :person => alex do 44 | attributes :name, :age 45 | 46 | collection :friends => [Person.new("john", 24)] do 47 | attributes :name, :age 48 | end 49 | end 50 | RUBY 51 | 52 | bldr tpl, :locals => {:alex => alex} 53 | end 54 | 55 | get '/template' do 56 | bert = Person.new('bert', 25) 57 | ernie = Person.new('ernie', 26) 58 | bldr :'fixtures/nested_objects.json', :locals => {:bert => bert, :ernie => ernie} 59 | end 60 | 61 | get '/root_template' do 62 | name = "john doe" 63 | age = 26 64 | 65 | bldr :'fixtures/root_template.json', :locals => {:name => name, :age => age} 66 | end 67 | 68 | get '/root_partial' do 69 | bldr :'fixtures/root_partial' 70 | end 71 | 72 | get '/ivar' do 73 | @person = Person.new('bert', 99) 74 | bldr :'fixtures/ivar' 75 | end 76 | 77 | get '/nested_ivars' do 78 | @person = Person.new('bert', 99) 79 | bldr :'fixtures/nested_ivars' 80 | end 81 | end 82 | 83 | it 'has access to the params hash in bldr templates' do 84 | response = Rack::MockRequest.new(TestApp).get('/people/123') 85 | decode(response.body).should == { 86 | 'person' => { 'id' => '123', 'name' => 'alex'} 87 | } 88 | end 89 | 90 | it 'passes ivars through to the template' do 91 | response = Rack::MockRequest.new(TestApp).get('/ivar') 92 | decode(response.body).should == {'person' => {'name' => 'bert', 'age' => 99}} 93 | end 94 | 95 | it 'makes ivars available in nested objects' do 96 | response = Rack::MockRequest.new(TestApp).get('/nested_ivars') 97 | decode(response.body).should == {'person' => {'name' => 'bert', 'age' => 99}} 98 | end 99 | 100 | it "properly renders a template that only contains a template call" do 101 | response = Rack::MockRequest.new(TestApp).get('/root_partial') 102 | MultiJson.decode(response.body).should == {'foo' => 'bar'} 103 | end 104 | 105 | it "returns json for a simple single-level template" do 106 | request = Rack::MockRequest.new(TestApp) 107 | response = request.get '/' 108 | response.status.should == 200 109 | parse_json(response.body).should == {'dude' => {'name' => 'alex', 'age' => 25}} 110 | end 111 | 112 | it "properly serializes templates with collections" do 113 | request = Rack::MockRequest.new(TestApp) 114 | response = request.get '/collections' 115 | 116 | response.status.should == 200 117 | parse_json(response.body).should == { 118 | 'person'=> {'name' => 'alex', 'age' => 25, 'friends' => [{'name' => 'john', 'age' => 24}]} 119 | } 120 | end 121 | 122 | it "works with template files" do 123 | request = Rack::MockRequest.new(TestApp) 124 | response = request.get '/template' 125 | 126 | parse_json(response.body).should == { 127 | 'person' => {'name' => 'bert', 'age' => 25, 'name_age' => "bert 25", 128 | 'friend' => {'name' => 'ernie', 'age' => 26} 129 | } 130 | } 131 | end 132 | 133 | it "allows using root-level attributes" do 134 | request = Rack::MockRequest.new(TestApp) 135 | response = request.get '/root_template' 136 | 137 | parse_json(response.body).should == {'name' => 'john doe', 'age' => 26} 138 | end 139 | end 140 | 141 | describe "access to the locals hash inside sinatra bldr templates" do 142 | class Locals < BaseTestApp 143 | disable :show_exceptions 144 | enable :raise_errors 145 | 146 | get '/' do 147 | tpl = <<-RUBY 148 | object(:locals) do 149 | attribute(:key) { locals[:key] } 150 | end 151 | RUBY 152 | 153 | bldr tpl, :locals => {:key => 'val'} 154 | end 155 | end 156 | 157 | it "provides access to the locals hash in the template" do 158 | response = Rack::MockRequest.new(Locals).get('/') 159 | MultiJson.decode(response.body)['locals']['key'].should == 'val' 160 | end 161 | end 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/ajsharp/bldr.png)](https://travis-ci.org/ajsharp/bldr) 2 | 3 | 4 | # Bldr 5 | 6 | Bldr is a minimalist templating DSL that provides a simple syntax for generating 7 | json documents from ruby objects. Bldr supports Sinatra and Rails 8 | 3.2. 9 | 10 | Bldr enables to quickly generate json documents from ruby with a 11 | simple and intuitive DSL. 12 | 13 | ```ruby 14 | # app/views/posts/index.json.bldr 15 | 16 | collection @posts do |post| 17 | attributes :title, :body, :created_at, :slug 18 | 19 | object :author => post.author do |author| 20 | attribute(:name) { author.display_name } 21 | end 22 | 23 | collection :comments => post.comments do |comment| 24 | attribute :spamminess 25 | attribute :created_at 26 | attribute(:body) do 27 | xss_filter(comment.body) 28 | end 29 | end 30 | end 31 | ``` 32 | 33 | This would output the following json document: 34 | 35 | ```json 36 | [ 37 | { 38 | "title": "Some title", 39 | "body": "blah blah", 40 | "slug": "some-title", 41 | "created_at": "2013-04-11T15:46:17-07:00", 42 | "author": { 43 | "name": "Joe Author" 44 | }, 45 | "comments": [ 46 | { 47 | "spamminess": 1.0, 48 | "created_at": "2013-04-11T15:46:17-07:00", 49 | "body": "a comment" 50 | } 51 | ] 52 | } 53 | ] 54 | ``` 55 | 56 | ## Usage 57 | 58 | Bldr is a very concise DSL, containing only four core methods: 59 | 60 | * `object` 61 | * `collection` 62 | * `attribute` 63 | * `attributes` 64 | 65 | These four methods provide a great deal of power and flexibility in describing 66 | json responses. 67 | 68 | ## Why 69 | 70 | If you're building an API, `Model#to_json` just doesn't cut it. Besides the JSON 71 | representation of your models arguably being a presentation concern, trying 72 | to cram all of this logic into an `#as_json` method quickly turns into pure chaos. 73 | 74 | There are other json templating libraries available such as 75 | [rabl](https://github.com/nesquena/rabl) or [json_builder](https://github.com/dewski/json_builder). 76 | Bldr is in the same vein as these libraries, but with a simpler synxtax. 77 | 78 | ## Usage & Examples 79 | 80 | See [Examples on the wiki](https://github.com/ajsharp/bldr/wiki/Documentation-&-Examples) 81 | for documentation and usage examples. 82 | 83 | ## Installation 84 | 85 | In your gemfile: 86 | 87 | ```ruby 88 | gem 'bldr' 89 | ``` 90 | 91 | ## Configuration 92 | 93 | No additional configuration is required for rails applications. 94 | 95 | For sinatra apps, dependening on whether you're using a modular or classic 96 | application style, do one of the following: 97 | 98 | ```ruby 99 | 100 | # Method 1: Classic style 101 | 102 | require 'sinatra/bldr' 103 | 104 | get '/hello' do 105 | bldr :hello 106 | end 107 | 108 | 109 | # Method 2: Modular style 110 | 111 | require 'sinatra/bldr' 112 | 113 | class MyApp < Sinatra::Base 114 | register Sinatra::Bldr 115 | end 116 | ``` 117 | 118 | ## Deprecations & Breaking Changes 119 | 120 | ### 0.7.0: current_object deprecation 121 | 122 | The use of `current_object` is now deprecated. Instead of referencing `current_object` in bldr templates 123 | use block variables in `object` and `collection` methods: 124 | 125 | ```ruby 126 | # OLD (deprecated) 127 | collection :people => people do 128 | attribute(:name) { current_object.name } 129 | end 130 | 131 | # NEW 132 | collection :people => people do |person| 133 | attribute(:name) { person.name } 134 | end 135 | ``` 136 | 137 | Make use of block variables the same way for the `object` method: 138 | 139 | ```ruby 140 | # OLD (deprecated) 141 | object :person => person do 142 | attributes :name, :age 143 | 144 | person = current_object 145 | object :address => person.address do 146 | # current_object here would be assigned to person.address 147 | attribute(:zip) { current_object.zip_code } 148 | attribute(:address_title) { person.display_name } 149 | end 150 | end 151 | 152 | # NEW 153 | object :person => person do |person| 154 | attributes :name, :age 155 | 156 | object :adress => person.address do |address| 157 | attribute(:zip) { address.zip_code } 158 | attribute(:address_title) { person.display_name } 159 | end 160 | end 161 | ``` 162 | 163 | ### 0.7.0: attribute method breaking change 164 | 165 | One of the forms of the `attribute` method has changed in the 0.7.0 release. 166 | Previously, using the dynamic block form of `attribute`, if you did not pass 167 | in a block variable, the block would be eval'd in context of the `current_object`. 168 | This behavior fails the "principle of least surprise" test. 169 | 170 | 0.7.0 changes this behavior by simply executing the block in context of `Bldr::Node`, which provides 171 | access to instance variables and locals available in that context. 172 | 173 | ```ruby 174 | # OLD 175 | object :person => person do 176 | attribute(:name) { display_name } # equivalent to doing attribute(:name) { |person| person.display_name } 177 | end 178 | 179 | # NEW 180 | object :person => @person do 181 | attribute(:name) { @person.display_name } 182 | end 183 | ``` 184 | 185 | See [941608e](https://github.com/ajsharp/bldr/commit/d0bfbd8) and [d0bfbd8](https://github.com/ajsharp/bldr/commit/d0bfbd8) for more info. 186 | 187 | ## Editor Syntax Support 188 | 189 | ### Vim 190 | 191 | Add this line to your .vimrc: 192 | 193 | ``` 194 | au BufRead,BufNewFile *.bldr set filetype=ruby 195 | ``` 196 | 197 | ### Emacs 198 | 199 | Add this to your `~/.emacs.d/init.el`: 200 | 201 | ``` 202 | (add-to-list 'auto-mode-alist '("\\.bldr$" . ruby-mode)) 203 | ``` 204 | 205 | ## TODO 206 | 207 | * XML support 208 | 209 | ## Acknowledgements 210 | 211 | * [RABL](http://github.com/nesquena/rabl) - Inspiration 212 | * [Tilt](https://github.com/rtomayko/tilt) - Mega awesome templating goodness 213 | 214 | ## Contributors 215 | 216 | * Ian Hunter (@ihunter) 217 | * Justin Smestad (@jsmestad) 218 | * Adam LaFave (@lafave) 219 | 220 | ## Copyright 221 | 222 | Copyright (c) 2011-2013 Alex Sharp. See the MIT-LICENSE file for full 223 | copyright information. 224 | -------------------------------------------------------------------------------- /spec/functional/tilt_template_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Bldr 4 | describe 'local variable access' do 5 | it 'provides access to locals in nested object blocks' do 6 | Template.new do 7 | <<-RUBY 8 | object :person => person do 9 | attribute(:name) { person.name } 10 | object :address => Object.new do |address| 11 | attribute(:display_name) { person.name } 12 | end 13 | end 14 | RUBY 15 | end.render(Node.new(nil), {person: Person.new('alex')}) 16 | .result 17 | .should == {:person => {:name => 'alex', :address => {:display_name => 'alex'}}} 18 | end 19 | end 20 | 21 | describe "instance variables" do 22 | let(:ctx) { Object.new } 23 | 24 | describe "collection blocks" do 25 | it 'has access to instance variables' do 26 | ctx.instance_variable_set(:@person, Person.new("John Denver")) 27 | 28 | Template.new do 29 | <<-RUBY 30 | collection :artists => [@person] do 31 | attribute(:name) { @person.name } 32 | end 33 | RUBY 34 | end.render(Node.new(nil, :parent => ctx)) 35 | .result 36 | .should == {:artists => [{:name => 'John Denver'}]} 37 | end 38 | end 39 | 40 | it 'has access to instance variables in included template partials' do 41 | ctx.instance_variable_set(:@person, Person.new('john denver')) 42 | 43 | Template.new { 44 | <<-RUBY 45 | template('spec/fixtures/ivar.bldr') 46 | RUBY 47 | }.render(Node.new(nil, :parent => ctx)) 48 | .result 49 | .should == {:person => {:name => 'john denver', :age => nil}} 50 | end 51 | 52 | it 'has access to ivars in attribute blocks with no arity' do 53 | ctx.instance_variable_set(:@person, Person.new('john denver')) 54 | 55 | Template.new { 56 | <<-RUBY 57 | object :person do 58 | attribute(:name) { @person.name } 59 | end 60 | RUBY 61 | }.render(Node.new(nil, :parent => ctx)) 62 | .result 63 | .should == {:person => {:name => 'john denver'}} 64 | end 65 | 66 | it 'has access to ivars in attribute blocks with arity of 1' do 67 | ctx.instance_variable_set(:@denver, Person.new('john denver')) 68 | ctx.instance_variable_set(:@rich, Person.new('charlie rich')) 69 | Template.new { 70 | <<-RUBY 71 | object :person => @denver do 72 | attribute(:name) { |p| @rich.name } 73 | end 74 | RUBY 75 | }.render(Node.new(nil, :parent => ctx)) 76 | .result 77 | .should == {:person => {:name => 'charlie rich'}} 78 | end 79 | 80 | it 'has access to ivars in nested object blocks' do 81 | ctx.instance_variable_set(:@batman, Person.new('batman')) 82 | ctx.instance_variable_set(:@bane, Person.new('bane')) 83 | Template.new { 84 | <<-RUBY 85 | object :hero => @batman do 86 | attribute(:name) { @batman.name } 87 | object :nemesis do 88 | attribute(:name) { @bane.name } 89 | attribute(:nemesis_name) { @batman.name } 90 | end 91 | end 92 | RUBY 93 | }.render(Node.new(nil, :parent => ctx)) 94 | .result 95 | .should == {hero: {name: 'batman', nemesis: {name: 'bane', nemesis_name: 'batman'}}} 96 | end 97 | end 98 | 99 | 100 | describe "evaluating a tilt template" do 101 | it "registers with Tilt" do 102 | Tilt['test.bldr'].should == Bldr::Template 103 | end 104 | 105 | it "renders a template" do 106 | alex = Person.new 107 | alex.name = 'alex' 108 | 109 | tpl = Bldr::Template.new { "object(:person => alex) { attribute(:name) }" } 110 | tpl.render(Bldr::Node.new, :alex => alex).result.should == {:person => {:name => 'alex'}} 111 | end 112 | 113 | it "allows attribute to be used at the root-level" do 114 | tpl = Bldr::Template.new { 115 | <<-RUBY 116 | attribute(:foo) { "bar" } 117 | RUBY 118 | } 119 | tpl.render(Bldr::Node.new(nil)).result.should == {:foo => 'bar'} 120 | end 121 | 122 | it "works when render two top-level objects" do 123 | alex = Person.new('alex') 124 | john = Person.new('john') 125 | 126 | tpl = Bldr::Template.new { 127 | <<-RUBY 128 | object(:person_1 => alex) { attribute(:name) } 129 | object(:person_2 => john) { attribute(:name) } 130 | RUBY 131 | } 132 | 133 | result = tpl.render(Bldr::Node.new, :alex => alex, :john => john).result 134 | result.should == { 135 | :person_1 => {:name => 'alex'}, 136 | :person_2 => {:name => 'john'} 137 | } 138 | end 139 | 140 | it "renders nil -> null correctly" do 141 | alex = Person.new('alex') 142 | tpl = Bldr::Template.new { 143 | <<-RUBY 144 | object(:person_1 => alex) { attributes(:age) } 145 | RUBY 146 | } 147 | result = tpl.render(Bldr::Node.new, :alex => alex).result 148 | result.should == {:person_1 => {:age => nil}} 149 | end 150 | 151 | describe "root Object nodes" do 152 | 153 | let(:alex) { Person.new('alex', 25) } 154 | let(:ian) { Person.new('ian', 32) } 155 | 156 | it "returns json for a root object" do 157 | tpl = Bldr::Template.new { 158 | <<-RUBY 159 | object :person => alex do 160 | attributes :name, :age 161 | end 162 | RUBY 163 | } 164 | result = tpl.render(Bldr::Node.new, :alex => alex, :ian => ian).result 165 | result.should == {:person => {:name => 'alex', :age => 25}} 166 | end 167 | 168 | it "returns json for root object templates with nested collections" do 169 | tpl = Bldr::Template.new { 170 | <<-RUBY 171 | object :person => alex do 172 | attributes :name, :age 173 | 174 | collection :friends => friends do 175 | attributes :name, :age 176 | end 177 | end 178 | RUBY 179 | } 180 | result = tpl.render(Bldr::Node.new, :alex => alex, :friends => [ian]).result 181 | result.should == { 182 | :person=> {:name => 'alex', :age => 25, :friends => [{:name => 'ian', :age => 32}]} 183 | } 184 | end 185 | 186 | it "renders nil -> null correctly" do 187 | alex = Person.new('alex') 188 | tpl = Bldr::Template.new { 189 | <<-RUBY 190 | object :person_1 => alex do 191 | attributes(:age) 192 | end 193 | RUBY 194 | } 195 | result = tpl.render(Bldr::Node.new, :alex => alex).result 196 | result.should == {:person_1 => {:age => nil}} 197 | end 198 | 199 | end 200 | 201 | describe "root Collection nodes" do 202 | 203 | let(:alex) { Person.new('alex', 25, [Person.new('bo',33)]) } 204 | let(:ian) { Person.new('ian', 32, [Person.new('eric',34)]) } 205 | 206 | it "returns json for a root collection template" do 207 | tpl = Bldr::Template.new { 208 | <<-RUBY 209 | collection :people => people do 210 | attributes :name, :age 211 | end 212 | RUBY 213 | } 214 | result = tpl.render(Bldr::Node.new, :people => [alex,ian]).result 215 | result.should == { 216 | :people => [{:name => 'alex', :age => 25}, {:name => 'ian', :age => 32}] 217 | } 218 | end 219 | 220 | it "returns json for a root collection with embedded collection template" do 221 | tpl = Bldr::Template.new { 222 | <<-RUBY 223 | collection :people => people do |person| 224 | attributes :name, :age 225 | collection :friends => person.friends do 226 | attributes :name, :age 227 | end 228 | end 229 | RUBY 230 | } 231 | result = tpl.render(Bldr::Node.new, :people => [alex,ian]).result 232 | result.should == { 233 | :people=> [{ 234 | :name => 'alex', 235 | :age => 25, 236 | :friends => [{:name => 'bo', :age => 33}] 237 | },{ 238 | :name => 'ian', 239 | :age => 32, 240 | :friends => [{:name => 'eric', :age => 34}] 241 | }] 242 | } 243 | end 244 | 245 | end 246 | end 247 | 248 | describe "using a partial template at the root of another template" do 249 | it "works as expected" do 250 | template = Bldr::Template.new('./spec/fixtures/root_partial.bldr') 251 | template.render(Bldr::Node.new(nil, :views => './spec')).result.should == {:foo => 'bar'} 252 | end 253 | end 254 | end 255 | -------------------------------------------------------------------------------- /lib/bldr/node.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module Bldr 4 | 5 | class Node 6 | extend Forwardable 7 | 8 | # These do not get copied into child nodes. All other instance variables do. 9 | PROTECTED_IVARS = [:@current_object, :@result, :@parent, :@opts, :@views, :@locals] 10 | 11 | # List of bldr public api method. So we don't overwrite them when we do 12 | # crazy ruby metaprogramming when we build nodes. 13 | API_METHODS = [:object, :collection, :attribute, :attributes] 14 | 15 | attr_reader :current_object, :result, :parent, :opts, :views, :locals 16 | 17 | # @!attribute [r] request params from a rails or sinatra controller 18 | attr_reader :params 19 | 20 | # Initialize a new Node instance. 21 | # 22 | # @example Building a simple node 23 | # node = Node.new do 24 | # Node.new(:person => Person.new("alex")) do 25 | # attributes(:name) 26 | # end 27 | # end 28 | # node.to_json # => {"person": {"name": "alex"}} 29 | # 30 | # 31 | # @param [Object] value an object to serialize. 32 | # @param [Hash] opts 33 | # @option opts [Object] :parent The parent object is used to copy instance variables 34 | # into each node in the node tree. 35 | # @option opts [Boolean] :root indicates whether this is the root node or not 36 | # @option [Object] opts :parent used to copy instance variables into self 37 | def initialize(value = nil, opts = {}, &block) 38 | @current_object = value 39 | @opts = opts 40 | @parent = opts[:parent] 41 | @views = opts[:views] 42 | @locals = opts[:locals] 43 | @result = {} # Storage hash for all descendant nodes 44 | 45 | # opts[:parent] will only get set to an ActionView::Base instance 46 | # when rails renders a bldr template. This logic doesn't belong here, 47 | # and there's a concept to be extracted here. 48 | # 49 | # The upshot of initializing the root node like this is that all child 50 | # nodes will have access to rails helper methods. This is necessary 51 | # due to the way bldr makes judicious use of instance_eval. 52 | # 53 | # @todo refactor this 54 | if opts[:root] && @parent 55 | # assign @parent to @view so it will be copied down to each child nod 56 | # @parent is in PROTECTED_IVARS and won't be copied. This effectively 57 | # gives all child nodes access to helper methods passed into the parent 58 | @view = @parent 59 | end 60 | 61 | copy_instance_variables(@parent) if @parent 62 | delegate_helpers if @view 63 | assign_params if @parent 64 | 65 | if block_given? 66 | if value && block.arity > 0 67 | instance_exec(value, &block) 68 | else 69 | instance_eval(&block) 70 | end 71 | end 72 | end 73 | 74 | def current_object 75 | warn "[DEPRECATION] `current_object` is deprecated. Please use object or collection block varibles instead." 76 | @current_object 77 | end 78 | 79 | # Create and render a node. 80 | # 81 | # @example A keyed object 82 | # get '/users/:id' do 83 | # user = User.find(params['id']) 84 | # 85 | # bldr :'users/show.json', :locals => {:user => user} 86 | # end 87 | # 88 | # # views/users/show.json.bldr 89 | # object :user => user do 90 | # attributes :name, :email 91 | # 92 | # attribute(:id) { |person| person.id.to_s } 93 | # end 94 | # 95 | # @example Root-level object with no key 96 | # get '/' do 97 | # url = "http://google.com" 98 | # 99 | # bldr :'template.json', :locals => {:url => url} 100 | # end 101 | # 102 | # # views/template.json.bldr 103 | # object do 104 | # attributes(:url) { url } 105 | # end 106 | # 107 | # @example "Pass-through" objects 108 | # object :person => person do 109 | # object :hobbies => hobbies 110 | # end 111 | # 112 | # @example Setting current object 113 | # object @person do 114 | # attributes :id, :name 115 | # end # => {'id' => 1, 'name' => 'john doe'} 116 | # 117 | # @param [Hash, Nil] hash a key/value pair indicating the output key name 118 | # and the object to serialize. 119 | # @param [Proc] block the code block to evaluate 120 | # 121 | # @return [Bldr::Node] returns self 122 | def object(base = nil, &block) 123 | if block_given? 124 | if keyed_object?(base) 125 | key = base.keys.first 126 | value = base.values.first 127 | 128 | # handle nil objects 129 | if value.nil? 130 | merge_result!(key, nil) 131 | return self 132 | end 133 | else 134 | # e.g. set the keyspace for an object 135 | if base.is_a?(Symbol) || base.is_a?(String) 136 | key = base 137 | value = nil 138 | else 139 | # e.g. set current object 140 | value = base 141 | key = nil 142 | end 143 | end 144 | 145 | node = Node.new(value, opts.merge(:parent => self), &block) 146 | merge_result!(key, node.result) 147 | else 148 | merge_result!(nil, base) 149 | end 150 | 151 | self 152 | end 153 | 154 | # Build a collection of objects, either passing each object 155 | # into the block provided, or rendering the collection 156 | # "pass-through", i.e. exactly as it appears. 157 | # 158 | # @example 159 | # object :person => person do 160 | # attributes :id, :name, :age 161 | # 162 | # collection :friends => person.friends do 163 | # attributes :name, :age, :friend_count 164 | # end 165 | # end 166 | # 167 | # @example "Pass-through" collections 168 | # object :person => person do 169 | # collection :hobbies => hobbies 170 | # end 171 | # 172 | # @param [Array, Hash] items Either an array of items, or a hash. 173 | # If an array is passed in, the objects will be rendered at the 174 | # "top level", i.e. without a key pointing to them. 175 | # @return [Bldr::Node] returns self 176 | def collection(items, &block) 177 | 178 | # Does this collection live in a key, or is it top-level? 179 | if keyed_object?(items) 180 | key = items.keys.first 181 | values = items.values.to_a.first 182 | else 183 | key = nil 184 | values = items 185 | end 186 | 187 | vals = if values 188 | if block_given? 189 | values.map do |item| 190 | Node.new(item, opts.merge(:parent => self), &block).result 191 | end 192 | else 193 | values 194 | end 195 | else 196 | [] 197 | end 198 | 199 | if keyed_object?(items) 200 | merge_result! key, vals 201 | else 202 | @result = massage_value(vals) 203 | end 204 | 205 | self 206 | end 207 | 208 | # Add attributes to the result hash in a variety of ways 209 | # 210 | # @example Simple list of attributes 211 | # object :person => dude do 212 | # attributes :name, :age 213 | # end 214 | # 215 | # @example Attribute aliasing 216 | # object :person => dude do 217 | # attributes :surname => :last_name # invokes dude.last_name 218 | # end 219 | # 220 | # @return [Nil] 221 | def attributes(*args, &block) 222 | if @current_object.nil? 223 | raise(ArgumentError, "No current_object to apply #attributes to.") 224 | end 225 | 226 | args.each do |arg| 227 | if arg.is_a?(Hash) 228 | merge_result!(arg.keys.first, @current_object.send(arg.values.first)) 229 | else 230 | merge_result!(arg, @current_object.send(arg)) 231 | end 232 | end 233 | self 234 | end 235 | 236 | # @example Dynamic attributes 237 | # object :person => employee do 238 | # collection :colleagues => employee.colleagues do |colleague| 239 | # attribute :isBoss do 240 | # employee.works_with?(colleague) && colleague.admin? 241 | # end 242 | # end 243 | # end 244 | # 245 | def attribute(*args, &block) 246 | if block_given? 247 | # e.g. attribute(:one, :two) { "value" } 248 | if args.size > 1 249 | raise(ArgumentError, "You may only pass one argument to #attribute when using the block syntax.") 250 | end 251 | 252 | # e.g. 253 | # object do 254 | # attribute { 'value' } 255 | # end 256 | if block.arity > 0 && @current_object.nil? 257 | raise(ArgumentError, "You cannot use a block of arity > 0 if current_object is not present.") 258 | end 259 | 260 | if block.arity > 0 261 | # object(person: @person) do 262 | # attribute(:name) { |person| person.name } 263 | # end 264 | merge_result! args.first, block.call(@current_object) 265 | else 266 | # object(person: @person) do 267 | # attribute(:name) # i.e. @person.name 268 | # end 269 | merge_result! args.first, block.call 270 | end 271 | else 272 | case args.size 273 | when 1 274 | # object do 275 | # attribute(:name) 276 | # end 277 | raise(ArgumentError, "#attribute can't be used when there is no current_object.") if @current_object.nil? 278 | if args[0].is_a?(Hash) 279 | # object(person: @person) do 280 | # attribute :key => :display_name # i.e. @person.display_name 281 | # end 282 | merge_result!(args[0].keys.first, @current_object.send(args[0].values.first)) 283 | else 284 | # object(person: @person) do 285 | # attribute :name 286 | # end 287 | merge_result!(args[0], @current_object.send(args[0])) 288 | end 289 | when 2 290 | # attribute :name, @person.name 291 | merge_result!(args[0], args[1]) 292 | else 293 | raise(ArgumentError, "You cannot pass more than two arguments to #attribute.") 294 | end 295 | end 296 | self 297 | end 298 | 299 | # Render a template inline within a view 300 | # 301 | # @example Simple render 302 | # object :person => dude do 303 | # template "path/to/template" 304 | # end 305 | # 306 | # @example Using locals 307 | # object :person => dude do 308 | # template "path/to/template", :locals => {:foo => 'bar'} 309 | # end 310 | # 311 | # @return [Bldr::Node] returns self 312 | def template(template, options={}) 313 | locals = options[:locals] || options['locals'] 314 | 315 | if tpl = Bldr::Template.new(find_template(template)).render(self, locals) 316 | merge_result! nil, tpl.result 317 | end 318 | 319 | self 320 | end 321 | 322 | private 323 | 324 | # Retrieves all instance variables from an object and sets them in the 325 | # current scope. 326 | # 327 | # @param [Object] object The object to copy instance variables from. 328 | def copy_instance_variables(object) 329 | ivar_names = (object.instance_variables - PROTECTED_IVARS).map(&:to_s) 330 | ivar_names.map do |name| 331 | instance_variable_set(name, object.instance_variable_get(name)) 332 | end 333 | end 334 | 335 | # Delegate helper methods on the @view to @view 336 | def delegate_helpers 337 | # ActionView::Base instances carry a method called helpers, 338 | # which is a module that contains helper methods available in a rails 339 | # controller. 340 | @_helpers = @view.helpers if @view.respond_to?(:helpers) 341 | 342 | # Delegate all helper methods, minus those with the same name as any 343 | # bldr api methods to @view via this object's metaclass 344 | if @_helpers 345 | (class << self; self; end).def_delegators :@view, *(@_helpers.instance_methods - API_METHODS) 346 | end 347 | end 348 | 349 | # Assigns the params attribute from the parent. 350 | def assign_params 351 | @params = @parent.params if @parent.respond_to?(:params) 352 | end 353 | 354 | # Determines if an object was passed in with a key pointing to it, or if 355 | # it was passed in as the "root" of the current object. Essentially, this 356 | # checks if `obj` quacks like a hash. 357 | # 358 | # @param [Object] obj 359 | # @return [Boolean] 360 | def keyed_object?(obj) 361 | obj.respond_to?(:keys) 362 | end 363 | 364 | def find_template(template) 365 | path = [] 366 | path << views if views 367 | template += ".json.bldr" unless template =~ /\.bldr$/ 368 | path << template 369 | File.join(*path) 370 | end 371 | 372 | # Merges values into the "local" result hash. 373 | def merge_result!(key, val) 374 | if key 375 | result[key] = massage_value(val) 376 | else 377 | result.merge!(massage_value(val)) 378 | end 379 | end 380 | 381 | # put any specializations in here 382 | # @todo: add config handlers to specify your own overridable Class->lambda methods of serialization 383 | def massage_value(val) 384 | if block = Bldr.handlers[val.class] 385 | return block.call(val) 386 | else 387 | val 388 | end 389 | end 390 | 391 | end 392 | end 393 | -------------------------------------------------------------------------------- /spec/unit/node_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | ERROR_MESSAGES = { :attribute_lambda_one_argument => "You may only pass one argument to #attribute when using the block syntax.", 4 | :attribute_inferred_missing_one_argument => "#attribute can't be used when there is no current_object.", 5 | :attribute_more_than_two_arg => "You cannot pass more than two arguments to #attribute.", 6 | :attribute_inferred_missing_arity_too_large => "You cannot use a block of arity > 0 if current_object is not present.", 7 | :attributes_inferred_missing => "No current_object to apply #attributes to." } 8 | 9 | module Bldr 10 | describe Node, "#attributes" do 11 | let(:person) { 12 | Person.new('john', 25) 13 | } 14 | let(:node) { Bldr::Node.new(person) } 15 | 16 | it "returns stuff" do 17 | node.attribute(:age).should == node 18 | end 19 | end 20 | 21 | describe Node, "#attribute" do 22 | it "raises an exception when passed more than one argument and a block" do 23 | expect { 24 | Node.new { 25 | attribute(:one, :two) do |person| 26 | "..." 27 | end 28 | } 29 | }.to raise_error(ArgumentError, ERROR_MESSAGES[:attribute_lambda_one_argument]) 30 | end 31 | 32 | it "raises an exception when passed more than two args" do 33 | expect { 34 | Node.new { 35 | attribute(:one, :two, :three) 36 | } 37 | }.to raise_error(ArgumentError, ERROR_MESSAGES[:attribute_more_than_two_arg]) 38 | end 39 | 40 | it "errors on 1 argument since there is no inferred object" do 41 | expect { 42 | Node.new { 43 | attribute(:one) 44 | } 45 | }.to raise_error(ArgumentError, ERROR_MESSAGES[:attribute_inferred_missing_one_argument]) 46 | end 47 | 48 | describe "wrapped in an object block" do 49 | it "renders 2 arguments statically" do 50 | node = Node.new do 51 | object :person do 52 | attribute(:name, "alex") 53 | end 54 | end 55 | node.result.should == {:person => {:name => 'alex'}} 56 | end 57 | 58 | it "renders 1 argument and one lambda with zero arity" do 59 | node = Node.new do 60 | object :person do 61 | attribute(:name) { "alex" } 62 | end 63 | end 64 | node.result.should == {:person => {:name => 'alex'}} 65 | end 66 | 67 | it "renders 1 argument to the inferred object" do 68 | node = Node.new do 69 | object :person => Person.new('alex', 25) do 70 | attribute(:name) 71 | end 72 | end 73 | node.result.should == {:person => {:name => 'alex'}} 74 | end 75 | 76 | it "renders 1 argument hash to the inferred object as the different key" do 77 | node = Node.new do 78 | object :person => Person.new('alex', 25) do 79 | attribute(:fake => :name) 80 | end 81 | end 82 | node.result.should == {:person => {:fake => 'alex'}} 83 | end 84 | 85 | it "renders 2 arguments statically" do 86 | node = Node.new do 87 | object :person => Person.new('alex', 25) do 88 | attribute(:name, 'ian') 89 | end 90 | end 91 | node.result.should == {:person => {:name => 'ian'}} 92 | end 93 | 94 | it "renders 1 argument and one lambda with zero arity" do 95 | node = Node.new do 96 | object :person => Person.new('alex', 25) do 97 | attribute(:name) { 'ian' } 98 | end 99 | end 100 | 101 | node.result.should == {:person => {:name => 'ian'}} 102 | end 103 | 104 | it "renders 1 argument and one lambda with arity 1" do 105 | node = Node.new do 106 | object :person => Person.new('alex', 25) do 107 | attribute(:name) { |person| person.name } 108 | end 109 | end 110 | 111 | node.result.should == {:person => {:name => 'alex'}} 112 | end 113 | 114 | it "renders nil attributes" do 115 | node = Node.new do 116 | object :person => Person.new('alex') do 117 | attribute :age 118 | end 119 | end 120 | 121 | node.result.should == {:person => {:age => nil}} 122 | end 123 | end 124 | 125 | describe 'when in static key-value form (with two arguments)' do 126 | it "renders 2 arguments statically as key, value" do 127 | node = Node.new { attribute(:name, "alex") } 128 | node.result.should == {:name => 'alex'} 129 | end 130 | 131 | it "renders null attributes to null, not 'null'" do 132 | node = Node.new { attribute(:name, nil) } 133 | node.result.should == {:name => nil} 134 | end 135 | end 136 | 137 | describe 'when in dynamic block form (with 1 argument and a block)' do 138 | it 'sets the attribute default context to bldr node' do 139 | node = Node.new { attribute(:key) { self.class } } 140 | node.result[:key].should == ::Bldr::Node 141 | end 142 | 143 | it "uses the argument as the key and the block result as the value" do 144 | node = Node.new { 145 | attribute(:name) do 146 | "alex" 147 | end 148 | } 149 | node.result.should == {:name => 'alex'} 150 | end 151 | 152 | it "errors on 1 argument and one lambda with arity 1" do 153 | expect { 154 | Node.new { 155 | attribute(:name) do |name| 156 | name 157 | end 158 | } 159 | }.to raise_error(ArgumentError, ERROR_MESSAGES[:attribute_inferred_missing_arity_too_large]) 160 | end 161 | end 162 | 163 | end 164 | 165 | describe Node, "#object" do 166 | it 'renders the object structure for a nil object' do 167 | node = Node.new do 168 | object :person => nil do 169 | attributes :name 170 | end 171 | attribute(:foo) { "bar" } 172 | end 173 | node.result.should == { 174 | person: nil, 175 | foo: 'bar' 176 | } 177 | end 178 | 179 | it 'is passes block the block variable to the block' do 180 | denver = Person.new('John Denver') 181 | node = Node.new do 182 | object :person => denver do |jd| 183 | attribute(:name) { jd.name } 184 | end 185 | end 186 | 187 | node.result.should == {:person => {:name => 'John Denver'}} 188 | end 189 | 190 | context "rendering an object exactly as it exists" do 191 | it "renders the object exactly as it appears when passed an object with no block" do 192 | obj = {'key' => 'val', 'nested' => {'key' => 'val'}} 193 | node = node_wrap do 194 | object obj 195 | end 196 | node.result.should == obj 197 | end 198 | end 199 | 200 | context "a zero arg root object node" do 201 | 202 | def wrap(&block) 203 | Bldr::Node.new do 204 | object(&block) 205 | end 206 | end 207 | 208 | describe "#attributes" do 209 | it "errors since current_object is nil" do 210 | expect { 211 | node_wrap { 212 | attributes(:name) 213 | } 214 | }.to raise_error(ArgumentError, ERROR_MESSAGES[:attributes_inferred_missing]) 215 | end 216 | end 217 | end 218 | 219 | context "a single arg root object node" do 220 | 221 | def wrap(&block) 222 | Bldr::Node.new do 223 | object(:person, &block) 224 | end 225 | end 226 | 227 | describe "#attributes" do 228 | 229 | it "errors since current_object is nil" do 230 | expect { 231 | node_wrap { 232 | attributes(:name) 233 | } 234 | }.to raise_error(ArgumentError, ERROR_MESSAGES[:attributes_inferred_missing]) 235 | end 236 | end 237 | end 238 | 239 | context "a hash-arg root object node" do 240 | def wrap(&block) 241 | alex = Person.new('alex').tap { |p| p.age = 25; p } 242 | Bldr::Node.new do 243 | object(:person => alex, &block) 244 | end 245 | end 246 | 247 | describe "#attributes" do 248 | describe "when an object key is passed a null value" do 249 | subject { 250 | node = node_wrap do 251 | object(:person => nil) do 252 | attributes(:one, :two) do |person| 253 | "..." 254 | end 255 | end 256 | end 257 | } 258 | 259 | it "does not raise an inferred object error" do 260 | expect { 261 | subject 262 | }.not_to raise_error(ArgumentError, ERROR_MESSAGES[:attributes_inferred_missing]) 263 | end 264 | 265 | its(:result) { should == {:person => nil} } 266 | end 267 | 268 | it "renders each argument against the inferred object" do 269 | node = wrap { attributes(:name, :age) } 270 | node.result.should == {:person => {:name => 'alex', :age => 25}} 271 | end 272 | 273 | it "renders nil attributes" do 274 | node = node_wrap do 275 | object :person => Person.new('alex') do 276 | attributes :name, :age 277 | end 278 | end 279 | 280 | node.result.should == {:person => {:name => 'alex', :age => nil}} 281 | end 282 | end 283 | end 284 | 285 | describe "embedded objects" do 286 | it "evaluates the block and returns json" do 287 | node = node_wrap do 288 | object(:dude => Person.new("alex")) do 289 | attributes :name 290 | 291 | object(:bro => Person.new("john")) do 292 | attributes :name 293 | end 294 | end 295 | end 296 | 297 | node.result.should == {:dude => {:name => 'alex', :bro => {:name => 'john'}}} 298 | end 299 | end 300 | 301 | end 302 | 303 | describe "Node#result" do 304 | it "returns an empty hash when not passed an object" do 305 | Bldr::Node.new.result.should == {} 306 | end 307 | 308 | it "a document with a single node with no nesting" do 309 | node = node_wrap do 310 | object :person => Person.new('alex') do 311 | attributes :name 312 | end 313 | end 314 | 315 | node.result.should == {:person => {:name => 'alex'}} 316 | end 317 | 318 | it "works for multiple top-level objects" do 319 | alex, john = Person.new("alex"), Person.new("john") 320 | 321 | node = node_wrap do 322 | object(:alex => alex) do 323 | attributes :name 324 | end 325 | 326 | object(:john => john) do 327 | attributes :name 328 | end 329 | end 330 | 331 | node.result.should == {:alex => {:name => 'alex'}, :john => {:name => 'john'}} 332 | end 333 | 334 | it "recursively renders nested objects" do 335 | node = node_wrap do 336 | object :alex => Person.new("alex") do 337 | attributes :name 338 | 339 | object :friend => Person.new("john") do 340 | attributes :name 341 | end 342 | end 343 | end 344 | 345 | node.result.should == { 346 | :alex => { 347 | :name => 'alex', :friend => {:name => 'john'} 348 | } 349 | } 350 | end 351 | 352 | describe "#attributes syntax" do 353 | it "allows a hash to be sent where the keys are the result keys" do 354 | alex = Person.new("alex").tap do |p| 355 | p.age = 25 356 | p 357 | end 358 | 359 | node = node_wrap do 360 | object(:person => alex) do 361 | attributes({:surname => :name}, :age) 362 | end 363 | end 364 | 365 | node.result.should == {:person => {:surname => 'alex', :age => 25}} 366 | end 367 | end 368 | end 369 | 370 | describe Node, "#to_json" do 371 | it "recursively returns the result json" do 372 | node = node_wrap do 373 | object :person => Person.new("alex") do 374 | attributes :name 375 | 376 | object :friend => Person.new("pete", 30) do 377 | attributes :name, :age 378 | end 379 | end 380 | end 381 | 382 | node.result.should == { 383 | :person => { 384 | :name => 'alex', 385 | :friend => {:name => 'pete', :age => 30} 386 | } 387 | } 388 | end 389 | 390 | it "returns null values for nil attributes" do 391 | node = node_wrap do 392 | object :person => Person.new('alex') do 393 | attributes :name, :age 394 | end 395 | end 396 | 397 | node.result[:person].should have_key(:age) 398 | node.result[:person][:age].should be_nil 399 | end 400 | end 401 | 402 | describe Node, "#collection" do 403 | context "when passed an object with no block" do 404 | it "renders the object exactly as it exists" do 405 | coll = [{'key' => 'val'}] 406 | node = node_wrap do 407 | collection coll 408 | end 409 | 410 | node.result.should == coll 411 | end 412 | 413 | it "renders complex collection objects correctly" do 414 | hobbies = [{'name' => "Gym"}, {'name' => "Tan"}, {'name' => "Laundry"}] 415 | 416 | node = node_wrap do 417 | object 'person' => Person.new("Alex") do 418 | attribute :name 419 | collection 'hobbies' => hobbies 420 | end 421 | end 422 | 423 | node.result.should == {'person' => {:name => "Alex", 'hobbies' => hobbies}} 424 | end 425 | end 426 | 427 | it "iterates through the collection and passes each item as a block variable" do 428 | denver = Person.new('John Denver') 429 | songs = [Song.new('Rocky Mountain High'), Song.new('Take Me Home, Country Roads')] 430 | 431 | node = Node.new do 432 | object :artist => denver do 433 | attribute :name 434 | 435 | collection :songs => songs do |song| 436 | attribute(:name) { song.name } 437 | end 438 | end 439 | end 440 | 441 | node.result.should == { 442 | :artist => {:name => 'John Denver', 443 | :songs => [{:name => 'Rocky Mountain High'}, 444 | {:name => 'Take Me Home, Country Roads'} 445 | ] 446 | } 447 | } 448 | end 449 | 450 | it "iterates through the collection and renders them as nodes" do 451 | node = node_wrap do 452 | object :person => Person.new('alex', 26) do 453 | attributes :name, :age 454 | 455 | collection :friends => [Person.new('john', 24), Person.new('jeff', 25)] do 456 | attributes :name, :age 457 | end 458 | end 459 | end 460 | 461 | node.result.should == { 462 | :person => { 463 | :name => 'alex', :age => 26, 464 | :friends => [ 465 | {:name => 'john', :age => 24}, 466 | {:name => 'jeff', :age => 25}] 467 | } 468 | } 469 | end 470 | 471 | # @todo fix this 472 | it "renders properly when a collection is the named root node" do 473 | nodes = node_wrap do 474 | collection :people => [Person.new('bert'), Person.new('ernie')] do 475 | attributes :name 476 | end 477 | end 478 | 479 | nodes.result.should == {:people => [{:name => 'bert'}, {:name => 'ernie'}]} 480 | end 481 | 482 | it "renders properly when a collection is the root node" do 483 | nodes = node_wrap do 484 | collection [Person.new('bert'), Person.new('ernie')] do 485 | attributes :name 486 | end 487 | end 488 | 489 | nodes.result.should == [{:name => 'bert'}, {:name => 'ernie'}] 490 | end 491 | 492 | it "gracefully handles empty collections" do 493 | nodes = node_wrap do 494 | collection :people => [] do 495 | attributes :name 496 | end 497 | end 498 | 499 | nodes.result.should == {:people => []} 500 | end 501 | 502 | it "gracefully handles nil collections" do 503 | nodes = node_wrap do 504 | collection :people => nil do 505 | attributes :name 506 | end 507 | end 508 | 509 | nodes.result.should == {:people => []} 510 | end 511 | 512 | it "renders nested collections properly" do 513 | post = Post.new("my post") 514 | post.comments << Comment.new('my comment') 515 | 516 | nodes = node_wrap do 517 | collection :posts => [post] do |post| 518 | attributes :title 519 | attribute(:comment_count) { |post| post.comments.count } 520 | 521 | collection :comments => post.comments do 522 | attributes :body 523 | end 524 | end 525 | end 526 | 527 | nodes.result.should == { 528 | :posts => [ 529 | {:title => 'my post', :comment_count => 1, :comments => [{:body => 'my comment'}]} 530 | ] 531 | } 532 | end 533 | 534 | it "renders objects nested in collections properly" do 535 | post = Post.new 'foo' 536 | post.author = Author.new('John Doe') 537 | posts = [post] 538 | 539 | nodes = node_wrap do 540 | collection :data => posts do |post| 541 | attributes :title 542 | 543 | object :author => post.author do 544 | attributes :name 545 | end 546 | end 547 | end 548 | 549 | nodes.result.should == { 550 | :data => [ 551 | {:title => 'foo', :author => {:name => 'John Doe'}} 552 | ] 553 | } 554 | end 555 | 556 | it "renders nested collections with dynamic property values correctly" do 557 | post1 = Post.new("post 1") 558 | post2 = Post.new("post 2") 559 | post1.comments << Comment.new('post 1 comment') 560 | post2.comments << Comment.new('post 2 first comment') 561 | post2.comments << Comment.new('post 2 second comment') 562 | 563 | nodes = node_wrap do 564 | collection :posts => [post1, post2] do |post| 565 | attributes :title 566 | attribute(:comment_count) { |post| post.comments.count } 567 | 568 | collection :comments => post.comments do 569 | attributes :body 570 | end 571 | end 572 | end 573 | 574 | nodes.result.should == { 575 | :posts => [ 576 | { 577 | :title => 'post 1', 578 | :comment_count => 1, 579 | :comments => [{:body => 'post 1 comment'}] 580 | }, 581 | { 582 | :title => 'post 2', 583 | :comment_count => 2, 584 | :comments => [{:body => 'post 2 first comment'}, {:body => 'post 2 second comment'}] 585 | } 586 | ] 587 | } 588 | end 589 | 590 | it "allows root level attributes using local variables" do 591 | node = node_wrap do 592 | name = "john doe" 593 | age = 25 594 | 595 | object do 596 | attribute(:name) { name } 597 | attribute(:age) { age } 598 | end 599 | end 600 | 601 | node.result.should == {:name => 'john doe', :age => 25} 602 | end 603 | 604 | end 605 | 606 | describe Node, "#template" do 607 | it "includes the partial as a top level" do 608 | nodes = node_wrap do 609 | template "spec/fixtures/partial.json.bldr" 610 | end 611 | 612 | nodes.result.should == {:foo => "bar"} 613 | end 614 | 615 | it "includes the partial on a top level object" do 616 | nodes = node_wrap do 617 | object :container do 618 | attribute(:blah) { "baz" } 619 | template "spec/fixtures/partial.json.bldr" 620 | end 621 | end 622 | 623 | nodes.result.should == {:container => {:blah => "baz", :foo => "bar"}} 624 | end 625 | 626 | it "includes the partial on a top level collection" do 627 | nodes = node_wrap do 628 | collection :people => [Person.new('bert'), Person.new('ernie')] do 629 | attribute(:blah) { "baz" } 630 | template "spec/fixtures/partial.json.bldr" 631 | end 632 | end 633 | 634 | nodes.result.should == {:people => [{:blah => "baz", :foo => 'bar'}, {:blah => "baz", :foo => 'bar'}]} 635 | end 636 | 637 | it "includes the partial on a sub object" do 638 | nodes = node_wrap do 639 | object :container do 640 | object :sub do 641 | attribute(:blah) { "baz" } 642 | template "spec/fixtures/partial.json.bldr" 643 | end 644 | end 645 | end 646 | 647 | nodes.result.should == {:container => {:sub => {:blah => "baz", :foo => "bar"}}} 648 | end 649 | 650 | it "includes the partial on a sub collection" do 651 | nodes = node_wrap do 652 | object :container do 653 | collection :people => [Person.new('bert'), Person.new('ernie')] do 654 | attribute(:blah) { "baz" } 655 | template "spec/fixtures/partial.json.bldr" 656 | end 657 | end 658 | end 659 | 660 | nodes.result.should == {:container => {:people => [{:blah => "baz", :foo => 'bar'}, {:blah => "baz", :foo => 'bar'}]}} 661 | end 662 | 663 | it "includes both the partials" do 664 | nodes = node_wrap do 665 | object :container do 666 | template "spec/fixtures/partial.json.bldr" 667 | object :sub do 668 | attribute(:blah) { "baz" } 669 | template "spec/fixtures/partial.json.bldr" 670 | end 671 | end 672 | end 673 | 674 | nodes.result.should == {:container => {:foo => "bar", :sub => {:blah => "baz", :foo => "bar"}}} 675 | end 676 | 677 | it "includes the partial with the locals" do 678 | Obj = Struct.new(:foo) 679 | nodes = node_wrap do 680 | template "spec/fixtures/partial_with_locals.json.bldr", :locals => {:obj => Obj.new('test')} 681 | end 682 | 683 | nodes.result.should == {:name => {:foo => 'test'}} 684 | end 685 | 686 | it "raises an error when the partial isn't found" do 687 | expect { 688 | nodes = node_wrap do 689 | template "unknown/path" 690 | end 691 | }.to raise_error(Errno::ENOENT) 692 | end 693 | 694 | it "doesn't raise an error when with a base path option specified and the right file" do 695 | nodes = node_wrap nil, :views => 'spec/fixtures/some' do 696 | object :foo do 697 | template "include" 698 | end 699 | end 700 | end 701 | end 702 | 703 | describe Node, "#locals" do 704 | let(:node) { Bldr::Node.new({:foo => 'bar'}, :locals => {:key => 'val'})} 705 | subject { node.locals } 706 | 707 | it { should == {:key => 'val'} } 708 | end 709 | 710 | describe Node, '#current_object' do 711 | it 'returns the node value' do 712 | Node.new('hey').current_object.should == 'hey' 713 | end 714 | 715 | it 'displays a deprecation warning' do 716 | Object.any_instance.should_receive(:warn).with("[DEPRECATION] `current_object` is deprecated. Please use object or collection block varibles instead.") 717 | Node.new.current_object 718 | end 719 | end 720 | end 721 | --------------------------------------------------------------------------------