boxer 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2011 Brad Fults, Gowalla Incorporated
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.
@@ -0,0 +1,106 @@
1
+ # Boxer
2
+
3
+ Boxer is a template engine for creating nested and multi-view JSON objects
4
+ from Ruby hashes.
5
+
6
+ ## The Problem
7
+
8
+ Say you have a couple ActiveRecord models in your Rails app and you want to
9
+ render an API response in JSON, but the view of each of those model objects
10
+ may change based on the API action that's being requested.
11
+
12
+ * User
13
+ * Place
14
+
15
+ For instance, the API for `GET /users/:id` should render a full representation
16
+ of the User object in question, including all relevant attributes.
17
+
18
+ But in your `GET /places/:id/users` API call, you only need short-form
19
+ representations of the users at that place, without every single attribute
20
+ being included in the response.
21
+
22
+ ## The Solution
23
+
24
+ Boxer allows you to define a box for each type of object you'd like to display
25
+ (or for each amalgamation of objects you want to display—it's up to you).
26
+
27
+ Boxer.box(:user) do |box, user|
28
+ {
29
+ :name => user.name,
30
+ :age => user.age,
31
+ }
32
+ end
33
+
34
+ To display different views on the same object, you can use Boxer's views:
35
+
36
+ Boxer.box(:user) do |box, user|
37
+ box.view(:base) do
38
+ {
39
+ :name => user.name,
40
+ :age => user.age,
41
+ }
42
+ end
43
+
44
+ box.view(:full, :extends => :base) do
45
+ {
46
+ :email => user.email,
47
+ :is_private => user.private?,
48
+ }
49
+ end
50
+ end
51
+
52
+ As you might guess, the `:full` view includes all attributes in the `:base`
53
+ view by virtue of the `:extends` option.
54
+
55
+ Now, in order to render a User with the `:base` view, simple call `Boxer.ship`:
56
+
57
+ Boxer.ship(:user, User.first)
58
+
59
+ Boxer assumes that you want the `:base` view if no view is specified to
60
+ `ship`—it's the only specially-named view.
61
+
62
+ To render the full view for the same user:
63
+
64
+ Boxer.ship(:user, User.first, :view => :full)
65
+
66
+ Which will give you back a Ruby hash on which you can call `#to_json`, to render
67
+ your JSON response<sup>1</sup>:
68
+
69
+ >> Boxer.ship(:user, User.first, :view => :full).to_json
70
+ => "{"name": "Bo Jim", "age": 17, "email": "b@a.com", "is_private": false}"
71
+
72
+ Composing different boxes together is as simple as calling `Boxer.ship` from
73
+ within a box&mdash;it's just Ruby:
74
+
75
+ Boxer.box(:place) do |box, place|
76
+ {
77
+ :name => place.name,
78
+ :address => place.address,
79
+ :top_user => Boxer.ship(:user, place.users.order(:visits).first),
80
+ }
81
+ end
82
+
83
+ 1. `Hash#to_json` requires the [`json` library](http://rubygems.org/gems/json)
84
+
85
+ ## More Features
86
+
87
+ See [the wiki](/h3h/boxer/wiki) for more features of Boxer, including:
88
+
89
+ * [Extra Arguments](/h3h/boxer/wiki/Extra-Arguments)
90
+ * [Preconditions](/h3h/boxer/wiki/Preconditions)
91
+ * [Helper Methods in Boxes](/h3h/boxer/wiki/Helper-Methods-in-Boxes)
92
+ * [Box Includes](/h3h/boxer/wiki/Box-Includes)
93
+ * [Multiple Inheritance](/h3h/boxer/wiki/Multiple-Inheritance)
94
+
95
+ ## Installation
96
+
97
+ Install the [boxer gem](http://rubygems.org/gems/boxer).
98
+
99
+ ## Original Author
100
+
101
+ * [Brad Fults](http://h3h.net/), Gowalla Incorporated
102
+
103
+ ## Inspiration
104
+
105
+ Boxer was inspired by [rabl](https://github.com/nesquena/rabl),
106
+ by Nathan Esquenazi.
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "boxer/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'boxer'
7
+ s.version = Boxer::VERSION
8
+ s.authors = ['Brad Fults']
9
+ s.email = ['bfults@gmail.com']
10
+ s.homepage = 'http://github.com/h3h/boxer'
11
+ s.license = 'MIT'
12
+ s.summary = %q{
13
+ Easy custom-defined templates for JSON generation of objects in Ruby.
14
+ }
15
+ s.description = %q{
16
+ A composable templating system for generating JSON via Ruby hashes, with
17
+ different possible views on each object and runtime data passing.
18
+ }
19
+
20
+ s.files = `git ls-files`.split("\n")
21
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
22
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
23
+ s.require_paths = ['lib']
24
+
25
+ s.add_development_dependency 'rspec', '>= 2.0.0'
26
+ s.add_runtime_dependency 'activesupport', '>= 3.0.0'
27
+ end
@@ -0,0 +1,101 @@
1
+ require 'boxer/version'
2
+ require 'active_support/core_ext/class/attribute_accessors'
3
+ require 'active_support/core_ext/array/extract_options'
4
+ require 'active_support/core_ext/hash/deep_merge'
5
+ require 'ostruct'
6
+
7
+ class Boxer
8
+ cattr_accessor :config
9
+
10
+ self.config = OpenStruct.new({
11
+ :box_includes => [],
12
+ })
13
+
14
+ class ViewMissingError < StandardError; end
15
+
16
+ def initialize(name, options={}, &block)
17
+ @name = name
18
+ @block = block
19
+ @fallback = []
20
+ @views = {}
21
+ @views_chain = {}
22
+ @options = options
23
+ end
24
+
25
+ ## class methods
26
+
27
+ def self.box(name, options={}, &block)
28
+ (@boxes ||= {})[name] = self.new(name, options, &block)
29
+ end
30
+
31
+ def self.boxes
32
+ @boxes
33
+ end
34
+
35
+ def self.clear!
36
+ @boxes = {}
37
+ end
38
+
39
+ def self.configure
40
+ yield config
41
+ end
42
+
43
+ def self.ship(name, *args)
44
+ fail "Unknown box: #{name.inspect}" unless @boxes.has_key?(name)
45
+ @boxes[name].ship(*args)
46
+ end
47
+
48
+ ## instance methods
49
+
50
+ def emit(val)
51
+ @fallback = [val]
52
+ end
53
+
54
+ def ship(*args)
55
+ args = args.dup
56
+ if args.last.is_a?(Hash)
57
+ args[-1] = args.last.dup
58
+ view = args.last.delete(:view)
59
+ args.slice!(-1) if args.last.empty?
60
+ end
61
+ view ||= :base
62
+
63
+ modules = self.class.config.box_includes
64
+ black_box = Class.new do
65
+ modules.each do |mod|
66
+ include mod
67
+ end
68
+ end
69
+ block_result = black_box.new.instance_exec(self, *args, &@block)
70
+
71
+ if @fallback.length > 0
72
+ return @fallback.pop
73
+ elsif @views_chain.any?
74
+ unless @views_chain.has_key?(view)
75
+ fail ViewMissingError.new([@name, view].map(&:inspect).join('/'))
76
+ end
77
+ return @views_chain[view].inject({}) do |res, view_name|
78
+ res.deep_merge(@views[view_name].call(*args))
79
+ end
80
+ else
81
+ return block_result
82
+ end
83
+ end
84
+
85
+ def precondition
86
+ yield self
87
+ end
88
+
89
+ def view(name, opts={}, &block)
90
+ @views_chain[name] = []
91
+ if opts.has_key?(:extends)
92
+ ancestors = Array(opts[:extends]).map do |parent|
93
+ (@views_chain[parent] || []) + [parent]
94
+ end.flatten.uniq
95
+ @views_chain[name] += ancestors
96
+ end
97
+ @views_chain[name] << name
98
+
99
+ @views[name] = block
100
+ end
101
+ end
@@ -0,0 +1,3 @@
1
+ class Boxer
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,206 @@
1
+ require 'spec_helper'
2
+ require 'boxer'
3
+
4
+ module MyTestModule
5
+ def my_test_method; 42 end
6
+ end
7
+
8
+ module MySecondTestModule
9
+ def my_second_test_method; 43 end
10
+ end
11
+
12
+ describe Boxer do
13
+
14
+ describe ".box" do
15
+ it "can create a box based on a simple hash" do
16
+ Boxer.box(:foo) do
17
+ {:working => true}
18
+ end
19
+
20
+ Boxer.ship(:foo).should eq({:working => true})
21
+ end
22
+
23
+ it "defaults to shipping the base view, when it exists" do
24
+ Boxer.box(:foo) do |box|
25
+ box.view(:base) { {:working => true} }
26
+ end
27
+
28
+ Boxer.ship(:foo).should eq({:working => true})
29
+ end
30
+
31
+ it "fails if views are specified, but :base is missing" do
32
+ Boxer.box(:foo) do |box|
33
+ box.view(:face) { {:working => true} }
34
+ end
35
+
36
+ expect {
37
+ Boxer.ship(:foo).should eq({:working => true})
38
+ }.to raise_exception(Boxer::ViewMissingError)
39
+ end
40
+
41
+ it "executes its block in a sandbox context, not a global one" do
42
+ Boxer.box(:foo) do |box|
43
+ self
44
+ end
45
+
46
+ context_obj = Boxer.ship(:foo)
47
+ context_obj.inspect.should_not include('RSpec')
48
+ context_obj.inspect.should include('Class')
49
+ end
50
+
51
+ it "raises a ViewMissing error if given a non-existent view" do
52
+ Boxer.box(:foo) do |box|
53
+ box.view(:face) { {:working => true} }
54
+ end
55
+
56
+ expect {
57
+ Boxer.ship(:foo).should eq({:working => true})
58
+ }.to raise_exception(Boxer::ViewMissingError)
59
+ end
60
+ end
61
+
62
+ describe ".clear!" do
63
+ it "clears all boxes" do
64
+ Boxer.box(:foo) { {:working => true} }
65
+ Boxer.clear!
66
+ Boxer.boxes.should be_empty
67
+ end
68
+ end
69
+
70
+ describe ".configure" do
71
+ it "sets config.box_includes via its supplied block" do
72
+ Boxer.config.box_includes = []
73
+ Boxer.configure {|config| config.box_includes = [MyTestModule] }
74
+ Boxer.config.box_includes.should include(MyTestModule)
75
+ end
76
+ end
77
+
78
+ describe ".ship" do
79
+ it "accepts arguments and passes them to a box for shipping" do
80
+ Boxer.box(:bar) do |box, x, y, z|
81
+ box.view(:base) { {:working => true, :stuff => [x, y, z]} }
82
+ end
83
+
84
+ Boxer.ship(:bar, 1, 2, 3).should eq(
85
+ {:working => true, :stuff => [1, 2, 3]}
86
+ )
87
+ end
88
+
89
+ it "allows a hash as the final argument" do
90
+ Boxer.box(:bar) do |box, x, y, z|
91
+ box.view(:base) { {:working => true, :stuff => [x, y, z]} }
92
+ end
93
+
94
+ Boxer.ship(:bar, 1, 2, :banana => true).should eq(
95
+ {:working => true, :stuff => [1, 2, {:banana => true}]}
96
+ )
97
+ end
98
+
99
+ it "includes modules from config.box_includes in shipping boxes" do
100
+ Boxer.config.box_includes = [MyTestModule, MySecondTestModule]
101
+ Boxer.box(:bar) do |box|
102
+ box.view(:base) { {:a => my_test_method, :b => my_second_test_method} }
103
+ end
104
+
105
+ Boxer.ship(:bar).should eq({:a => 42, :b => 43})
106
+ end
107
+
108
+ it "does not mutate passed in arguments" do
109
+ Boxer.box(:bar) do |box, num, opts|
110
+ box.view(:base) { {} }
111
+ end
112
+
113
+ hash = {:view => :base}.freeze
114
+ orig_hash = hash.dup
115
+ Boxer.ship(:bar, 1, hash)
116
+ hash.should eq(orig_hash)
117
+ end
118
+ end
119
+
120
+ describe "#precondition" do
121
+ it "ships the value emitted in the precondition" do
122
+ Boxer.box(:foo) do |box, obj|
123
+ box.precondition {|resp| resp.emit({}) if obj.nil? }
124
+ box.view(:base) { {:working => true} }
125
+ end
126
+
127
+ Boxer.ship(:foo, nil).should eq({})
128
+ end
129
+
130
+ it "disregards the precondition if no value is emitted" do
131
+ Boxer.box(:foo) do |box, obj|
132
+ box.precondition {|resp| resp.emit({}) if obj.nil? }
133
+ box.view(:base) { {:working => true} }
134
+ end
135
+
136
+ Boxer.ship(:foo, Object.new).should eq({:working => true})
137
+ end
138
+
139
+ it "can handle nil as an emitted precondition value" do
140
+ Boxer.box(:foo) do |box, obj|
141
+ box.precondition {|resp| resp.emit(nil) if obj.nil? }
142
+ box.view(:base) { {:working => true} }
143
+ end
144
+
145
+ Boxer.ship(:foo, nil).should eq(nil)
146
+ end
147
+
148
+ it "doesn't remember a fallback value from a previous shipping" do
149
+ Boxer.box(:foo) do |box, obj|
150
+ box.precondition {|resp| resp.emit({}) if obj.nil? }
151
+ box.view(:base) { {:working => true} }
152
+ end
153
+
154
+ Boxer.ship(:foo, nil).should eq({})
155
+ Boxer.ship(:foo, Object.new).should eq({:working => true})
156
+ end
157
+ end
158
+
159
+ describe "#view" do
160
+ it "allow extending of other views" do
161
+ Boxer.box(:foo) do |box|
162
+ box.view(:base) { {:working => true} }
163
+ box.view(:face, :extends => :base) { {:awesome => true} }
164
+ end
165
+
166
+ Boxer.ship(:foo, :view => :face).should eq(
167
+ {:working => true, :awesome => true}
168
+ )
169
+ end
170
+
171
+ it "extends by smashing lesser (extended) views" do
172
+ Boxer.box(:foo) do |box|
173
+ box.view(:base) { {:working => true} }
174
+ box.view(:face, :extends => :base) { {:working => :awesome} }
175
+ end
176
+
177
+ Boxer.ship(:foo, :view => :face).should eq(
178
+ {:working => :awesome}
179
+ )
180
+ end
181
+
182
+ it "extends by merging nested keys without overriding" do
183
+ Boxer.box(:foo) do |box|
184
+ box.view(:base) { {:working => {:a => 1}} }
185
+ box.view(:face, :extends => :base) { {:working => {:b => 2}} }
186
+ end
187
+
188
+ Boxer.ship(:foo, :view => :face).should eq(
189
+ {:working => {:a => 1, :b => 2}}
190
+ )
191
+ end
192
+
193
+ it "allows extending in a chain of more than two views" do
194
+ Boxer.box(:foo) do |box|
195
+ box.view(:base) { {:working => {:a => 1}} }
196
+ box.view(:face, :extends => :base) { {:working => {:b => 2}} }
197
+ box.view(:race, :extends => :face) { {:working => {:c => 3}} }
198
+ end
199
+
200
+ Boxer.ship(:foo, :view => :race).should eq(
201
+ {:working => {:a => 1, :b => 2, :c => 3}}
202
+ )
203
+ end
204
+ end
205
+
206
+ end
@@ -0,0 +1,5 @@
1
+ $:.push File.expand_path("../../lib", __FILE__)
2
+
3
+ RSpec.configure do |config|
4
+ # ...
5
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: boxer
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Brad Fults
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-10-18 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: &70290064287800 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 2.0.0
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70290064287800
25
+ - !ruby/object:Gem::Dependency
26
+ name: activesupport
27
+ requirement: &70290064287300 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 3.0.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70290064287300
36
+ description: ! "\n A composable templating system for generating JSON via Ruby
37
+ hashes, with\n different possible views on each object and runtime data passing.\n
38
+ \ "
39
+ email:
40
+ - bfults@gmail.com
41
+ executables: []
42
+ extensions: []
43
+ extra_rdoc_files: []
44
+ files:
45
+ - .gitignore
46
+ - Gemfile
47
+ - LICENSE
48
+ - README.md
49
+ - Rakefile
50
+ - boxer.gemspec
51
+ - lib/boxer.rb
52
+ - lib/boxer/version.rb
53
+ - spec/boxer_spec.rb
54
+ - spec/spec_helper.rb
55
+ homepage: http://github.com/h3h/boxer
56
+ licenses:
57
+ - MIT
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ! '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ! '>='
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubyforge_project:
76
+ rubygems_version: 1.8.10
77
+ signing_key:
78
+ specification_version: 3
79
+ summary: Easy custom-defined templates for JSON generation of objects in Ruby.
80
+ test_files:
81
+ - spec/boxer_spec.rb
82
+ - spec/spec_helper.rb