boxer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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