bldr 0.1.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.
- data/.gitignore +2 -0
- data/.rvmrc +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +10 -0
- data/MIT-LICENSE +21 -0
- data/README.md +281 -0
- data/Rakefile +7 -0
- data/bldr.gemspec +28 -0
- data/lib/bldr/engine.rb +14 -0
- data/lib/bldr/node.rb +171 -0
- data/lib/bldr/template.rb +28 -0
- data/lib/bldr/version.rb +4 -0
- data/lib/bldr.rb +15 -0
- data/lib/sinatra/bldr.rb +29 -0
- data/spec/fixtures/nested_objects.json.bldr +11 -0
- data/spec/fixtures/root_template.json.bldr +5 -0
- data/spec/functional/tilt_template_spec.rb +33 -0
- data/spec/integration/sinatra_spec.rb +88 -0
- data/spec/models/comment.rb +8 -0
- data/spec/models/person.rb +9 -0
- data/spec/models/post.rb +10 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/unit/bldr_spec.rb +17 -0
- data/spec/unit/node_spec.rb +275 -0
- metadata +143 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm 1.9.2
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Copyright (c) 2011 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
|
+
|
data/README.md
ADDED
@@ -0,0 +1,281 @@
|
|
1
|
+
[](http://travis-ci.org/ajsharp/bldr)
|
2
|
+
|
3
|
+
|
4
|
+
# Bldr
|
5
|
+
|
6
|
+
Bldr is a minimalist templating library that provides a simple DSL for generating
|
7
|
+
json documents from ruby objects. It currently supports Sinatra out of
|
8
|
+
the box -- Rails 3 support is planned for the near future.
|
9
|
+
|
10
|
+
If you would like to contribute, pull requests with specs are warmly accepted :)
|
11
|
+
|
12
|
+
## Features
|
13
|
+
|
14
|
+
* Simple json templating DSL
|
15
|
+
* Uses Tilt's built-in rendering and template caching for better
|
16
|
+
performance
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
There are two ways to use Bldr in your Sinatra app, depending on whether
|
21
|
+
you are using Sinatra's classic or module application style:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
|
25
|
+
# Method 1: Classic style
|
26
|
+
|
27
|
+
require 'sinatra/bldr'
|
28
|
+
|
29
|
+
get '/hello' do
|
30
|
+
bldr :hello
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
# Method 2: Modular style
|
35
|
+
|
36
|
+
require 'sinatra/bldr'
|
37
|
+
|
38
|
+
class MyApp < Sinatra::Base
|
39
|
+
register Sinatra::Bldr
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
## Usage
|
44
|
+
|
45
|
+
In your sinatra endpoints/actions, use the `bldr` helper method to
|
46
|
+
render templates.
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
get "/posts" do
|
50
|
+
# ...
|
51
|
+
posts = Post.all
|
52
|
+
bldr :'template.json', {}, {:posts => posts}
|
53
|
+
end
|
54
|
+
|
55
|
+
# views/template.json.bldr
|
56
|
+
collection :posts => posts do
|
57
|
+
attributes :title
|
58
|
+
attribute :comment_count { |post| post.comments.count }
|
59
|
+
|
60
|
+
collection :comments => current_object.comments do
|
61
|
+
attributes :body, :author, :email
|
62
|
+
end
|
63
|
+
end
|
64
|
+
```
|
65
|
+
|
66
|
+
## Examples
|
67
|
+
|
68
|
+
### Rendering a simple list of attributes
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
object :post => post do
|
72
|
+
attributes :title, :body
|
73
|
+
end
|
74
|
+
```
|
75
|
+
|
76
|
+
Output:
|
77
|
+
|
78
|
+
```javascript
|
79
|
+
{
|
80
|
+
"post": {
|
81
|
+
"title": "my title",
|
82
|
+
"body": "..."
|
83
|
+
}
|
84
|
+
}
|
85
|
+
```
|
86
|
+
|
87
|
+
### Dynamic attributes
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
object :post => post do
|
91
|
+
attribute :comment_count do |post|
|
92
|
+
post.comments.count
|
93
|
+
end
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
Output:
|
98
|
+
|
99
|
+
```javascript
|
100
|
+
{
|
101
|
+
"post": {
|
102
|
+
"comment_count": 1
|
103
|
+
}
|
104
|
+
}
|
105
|
+
|
106
|
+
```
|
107
|
+
|
108
|
+
### Attribute aliases
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
object :post => post do
|
112
|
+
attributes :title, :body
|
113
|
+
|
114
|
+
object :author => post.author do
|
115
|
+
attribute :surname => :last_name
|
116
|
+
end
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
Output:
|
121
|
+
|
122
|
+
```javascript
|
123
|
+
{
|
124
|
+
"post": {
|
125
|
+
"title": "my title",
|
126
|
+
"body": "...",
|
127
|
+
"author": {
|
128
|
+
"surname": "Doe"
|
129
|
+
}
|
130
|
+
}
|
131
|
+
}
|
132
|
+
```
|
133
|
+
|
134
|
+
### Nested objects
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
object :post => post do
|
138
|
+
attributes :title, :body
|
139
|
+
|
140
|
+
object :author => post.author do
|
141
|
+
attributes :first_name, :last_name, :email
|
142
|
+
|
143
|
+
attribute(:full_name) { |author| "#{author.first_name} #{author.last_name}" }
|
144
|
+
end
|
145
|
+
end
|
146
|
+
```
|
147
|
+
|
148
|
+
Output:
|
149
|
+
|
150
|
+
```javascript
|
151
|
+
{
|
152
|
+
"post": {
|
153
|
+
"title": "my title",
|
154
|
+
"body": "...",
|
155
|
+
"author": {
|
156
|
+
"first_name": "John",
|
157
|
+
"last_name": "Doe",
|
158
|
+
"email": "john@doe.com",
|
159
|
+
"full_name": "John Doe"
|
160
|
+
}
|
161
|
+
}
|
162
|
+
}
|
163
|
+
```
|
164
|
+
|
165
|
+
### Root-level attributes
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
get '/redirector' do
|
169
|
+
url = params['redirect_url']
|
170
|
+
bldr :'redirect.json', :locals => {:url => url}
|
171
|
+
end
|
172
|
+
|
173
|
+
# views/redirect.json.bldr
|
174
|
+
object do
|
175
|
+
attribute(:redirect_to) { url }
|
176
|
+
end
|
177
|
+
```
|
178
|
+
|
179
|
+
Output:
|
180
|
+
|
181
|
+
```javascript
|
182
|
+
{"redirect_to": "http://example.org"}
|
183
|
+
```
|
184
|
+
|
185
|
+
### Collections
|
186
|
+
|
187
|
+
All the examples above can be used inside a collection block. Here we
|
188
|
+
assume a Post model which has many Comments. You might use the below
|
189
|
+
code to render an action which returns a collection of posts, where
|
190
|
+
each post has a collection of comments.
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
collection :posts => posts do
|
194
|
+
attributes :title
|
195
|
+
attribute :comment_count { |post| post.comments.count }
|
196
|
+
|
197
|
+
# current_object
|
198
|
+
collection :comments => current_object.comments do
|
199
|
+
attributes :body, :author_name, :author_email
|
200
|
+
end
|
201
|
+
end
|
202
|
+
```
|
203
|
+
|
204
|
+
Output:
|
205
|
+
|
206
|
+
```javascript
|
207
|
+
{
|
208
|
+
"posts": [
|
209
|
+
{
|
210
|
+
"title": "my title",
|
211
|
+
"comment_count": 2,
|
212
|
+
"comments": [
|
213
|
+
{
|
214
|
+
"body": "...",
|
215
|
+
"author_name": "Comment Troll",
|
216
|
+
"email": "troll@trolling.edu"
|
217
|
+
},
|
218
|
+
{
|
219
|
+
"body": "...",
|
220
|
+
"author_name": "Uber Troll",
|
221
|
+
"email": "uber.troll@earthlink.net"
|
222
|
+
}
|
223
|
+
]
|
224
|
+
}
|
225
|
+
]
|
226
|
+
}
|
227
|
+
```
|
228
|
+
|
229
|
+
When inside of a collection block, you can use the `current_object`
|
230
|
+
method to access the member of the collection currently being iterated
|
231
|
+
over. This allows you to do nested collections, as in the example above.
|
232
|
+
|
233
|
+
### Templates
|
234
|
+
|
235
|
+
It is recommended to name your templates with the content type extension before
|
236
|
+
the .bldr extension. For example: `my_template.json.bldr`.
|
237
|
+
|
238
|
+
The templates themselves are just plain ruby code. They are evaluated in the context of a
|
239
|
+
`Bldr::Node` instance, which provides the bldr DSL. The DSL is comprised
|
240
|
+
primarily of 3 simple methods:
|
241
|
+
|
242
|
+
+ `object` - Creates an object
|
243
|
+
+ `collection` - Iterates over a collection of objects
|
244
|
+
+ `attributes` - Add attributes to the current object.
|
245
|
+
|
246
|
+
### Local Variables
|
247
|
+
|
248
|
+
You may pass local variables from your sinatra actions to bldr templates
|
249
|
+
by passing the `bldr` method a `:locals` hash, like so:
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
get '/posts' do
|
253
|
+
posts = Post.all.recent
|
254
|
+
|
255
|
+
bldr :'posts/index.json', :locals => {:posts => posts}
|
256
|
+
end
|
257
|
+
```
|
258
|
+
|
259
|
+
## Editor Syntax Support
|
260
|
+
|
261
|
+
To get proper syntax highlighting in vim, add this line to your .vimrc:
|
262
|
+
|
263
|
+
```
|
264
|
+
au BufRead,BufNewFile *.bldr set filetype=ruby
|
265
|
+
```
|
266
|
+
|
267
|
+
## TODO
|
268
|
+
|
269
|
+
* Rails 3 support
|
270
|
+
* Replace current_object with a block param for collection methods
|
271
|
+
* XML support
|
272
|
+
|
273
|
+
## Acknowledgements
|
274
|
+
|
275
|
+
* [RABL](http://github.com/nesquena/rabl) - Inspiration
|
276
|
+
* [Tilt](https://github.com/rtomayko/tilt) - Mega awesome goodness
|
277
|
+
|
278
|
+
## Copyright
|
279
|
+
|
280
|
+
Copyright (c) 2011 Alex Sharp. See the MIT-LICENSE file for full
|
281
|
+
copyright information.
|
data/Rakefile
ADDED
data/bldr.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
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', '~> 1.0.3'
|
23
|
+
|
24
|
+
s.add_development_dependency 'json_pure'
|
25
|
+
s.add_development_dependency 'sinatra', '~>1.2.6'
|
26
|
+
s.add_development_dependency 'tilt', '~>1.3.2'
|
27
|
+
s.add_development_dependency 'yajl-ruby'
|
28
|
+
end
|
data/lib/bldr/engine.rb
ADDED
data/lib/bldr/node.rb
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
|
2
|
+
module Bldr
|
3
|
+
|
4
|
+
class Node
|
5
|
+
|
6
|
+
attr_reader :current_object, :result, :parent
|
7
|
+
|
8
|
+
# Initialize a new Node instance.
|
9
|
+
#
|
10
|
+
# @example Building a simple node
|
11
|
+
# node = Node.new do
|
12
|
+
# Node.new(:person => Person.new("alex")) do
|
13
|
+
# attributes(:name)
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
# node.to_json # => {"person": {"name": "alex"}}
|
17
|
+
#
|
18
|
+
#
|
19
|
+
# @param [Object] value an object to serialize.
|
20
|
+
def initialize(value = nil, opts = {}, &block)
|
21
|
+
@current_object = value
|
22
|
+
@parent = opts[:parent]
|
23
|
+
|
24
|
+
# Storage hash for all descendant nodes
|
25
|
+
@result = {}
|
26
|
+
|
27
|
+
instance_eval(&block) if block_given?
|
28
|
+
end
|
29
|
+
|
30
|
+
# Merge the local results into the ancestor result hash.
|
31
|
+
#
|
32
|
+
# @return [Hash]
|
33
|
+
def render!
|
34
|
+
result
|
35
|
+
end
|
36
|
+
|
37
|
+
# Return the json-encoded result hash.
|
38
|
+
#
|
39
|
+
# @return [String] the json-encoded result hash
|
40
|
+
def to_json
|
41
|
+
MultiJson.encode(result)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Create and render a node.
|
45
|
+
#
|
46
|
+
# @example A keyed object
|
47
|
+
# get '/users/:id' do
|
48
|
+
# user = User.find(params['id'])
|
49
|
+
#
|
50
|
+
# bldr :'users/show.json', :locals => {:user => user}
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# # views/users/show.json.bldr
|
54
|
+
# object :user => user do
|
55
|
+
# attributes :name, :email
|
56
|
+
#
|
57
|
+
# attribute(:id) { |person| person.id.to_s }
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# @example Root-level object with no key
|
61
|
+
# get '/' do
|
62
|
+
# url = "http://google.com"
|
63
|
+
#
|
64
|
+
# bldr :'template.json', :locals => {:url => url}
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# # views/template.json.bldr
|
68
|
+
# object do
|
69
|
+
# attributes(:url) { url }
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# @param [Hash, Nil] hash a key/value pair indicating the output key name
|
73
|
+
# and the object to serialize.
|
74
|
+
# @param [Proc] block the code block to evaluate
|
75
|
+
#
|
76
|
+
# @return [String] returns a json-encoded string of itself and all
|
77
|
+
# descendant nodes.
|
78
|
+
def object(hash = nil, &block)
|
79
|
+
if hash
|
80
|
+
key = hash.keys.first
|
81
|
+
value = hash.values.first
|
82
|
+
end
|
83
|
+
|
84
|
+
node = Node.new(value, :parent => self, &block)
|
85
|
+
merge_result!(key, node.render!)
|
86
|
+
node.parent.to_json
|
87
|
+
end
|
88
|
+
|
89
|
+
def collection(items, &block)
|
90
|
+
key = items.keys.first
|
91
|
+
items = items.values.first
|
92
|
+
|
93
|
+
merge_result!(key, [])
|
94
|
+
items.each do |item|
|
95
|
+
node = Node.new(item, :parent => self, &block)
|
96
|
+
append_result!(key, node.render!)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Add attributes to the result hash in a variety of ways
|
101
|
+
#
|
102
|
+
# @example Simple list of attributes
|
103
|
+
# object :person => dude do
|
104
|
+
# attributes :name, :age
|
105
|
+
# end
|
106
|
+
#
|
107
|
+
# @example Attribute aliasing
|
108
|
+
# object :person => dude do
|
109
|
+
# attributes :surname => :last_name
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# @example Dynamic attributes (explicit object context)
|
113
|
+
# object :person => employee do
|
114
|
+
# collection :colleagues => employee.colleagues do |colleague|
|
115
|
+
# attribute :isBoss do |colleague|
|
116
|
+
# employee.works_with?(colleague) && colleague.admin?
|
117
|
+
# end
|
118
|
+
# end
|
119
|
+
# end
|
120
|
+
#
|
121
|
+
# @example Dynamic attributes (implicit object context)
|
122
|
+
# object :person => dude do
|
123
|
+
# collection :colleagues => employee.colleagues do |colleague|
|
124
|
+
# attribute :rank do
|
125
|
+
# # method called on colleague
|
126
|
+
# if admin? && superior_to?(employee)
|
127
|
+
# "High Up"
|
128
|
+
# end
|
129
|
+
# end
|
130
|
+
# end
|
131
|
+
# end
|
132
|
+
#
|
133
|
+
# @return [Nil]
|
134
|
+
def attributes(*args, &block)
|
135
|
+
if block_given?
|
136
|
+
if args.size > 1
|
137
|
+
raise(ArgumentError, "You may only pass one argument to #attribute when using the block syntax.")
|
138
|
+
end
|
139
|
+
|
140
|
+
merge_result!(args.first, (block.arity == 1) ? block.call(current_object) : current_object.instance_eval(&block))
|
141
|
+
return nil
|
142
|
+
end
|
143
|
+
|
144
|
+
args.each do |arg|
|
145
|
+
if arg.is_a?(Hash)
|
146
|
+
merge_result!(arg.keys.first, current_object.send(arg.values.first))
|
147
|
+
else
|
148
|
+
merge_result!(arg, current_object.send(arg))
|
149
|
+
end
|
150
|
+
end
|
151
|
+
nil
|
152
|
+
end
|
153
|
+
alias :attribute :attributes
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
# Merges values into the "local" result hash.
|
158
|
+
def merge_result!(key, val)
|
159
|
+
if key
|
160
|
+
result[key] = val
|
161
|
+
else
|
162
|
+
result.merge!(val)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def append_result!(key, val)
|
167
|
+
result[key] << val
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'tilt'
|
2
|
+
|
3
|
+
module Bldr
|
4
|
+
|
5
|
+
class Template < Tilt::Template
|
6
|
+
# attr_reader :engine
|
7
|
+
# self.default_mime_type = 'application/json'
|
8
|
+
|
9
|
+
def initialize_engine
|
10
|
+
require_template_library 'bldr'
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.engine_initialized?
|
14
|
+
defined? ::Bldr
|
15
|
+
end
|
16
|
+
|
17
|
+
def prepare
|
18
|
+
@engine = Bldr::Engine.new(data, options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def precompiled_template(locals)
|
22
|
+
data.to_s
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
Tilt.register 'bldr', Bldr::Template
|
28
|
+
end
|
data/lib/bldr/version.rb
ADDED
data/lib/bldr.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
|
2
|
+
$:.unshift(File.dirname(File.expand_path(__FILE__)))
|
3
|
+
|
4
|
+
require 'multi_json'
|
5
|
+
require 'bldr/engine'
|
6
|
+
require 'bldr/template'
|
7
|
+
require 'bldr/node'
|
8
|
+
|
9
|
+
module Bldr
|
10
|
+
class << self
|
11
|
+
def json_encoder=(encoder)
|
12
|
+
MultiJson.engine = encoder
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/sinatra/bldr.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
|
3
|
+
module Sinatra
|
4
|
+
|
5
|
+
module Bldr
|
6
|
+
module Helpers
|
7
|
+
|
8
|
+
# @param [String, Symbol] template the template to render
|
9
|
+
# Can be a relative file location or a string template.
|
10
|
+
# The template may also be passed in as the block argument
|
11
|
+
# to this method, in which case, template argument is nil.
|
12
|
+
#
|
13
|
+
# @param [Hash] opts a hash of options
|
14
|
+
# @option opts [Hash] :locals a hash of local variables to be used in the template
|
15
|
+
# @option
|
16
|
+
def bldr(template, opts = {}, &block)
|
17
|
+
opts[:scope] = ::Bldr::Node.new
|
18
|
+
locals = opts.delete(:locals)
|
19
|
+
render(:bldr, template, opts, locals, &block)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.registered(app)
|
24
|
+
app.helpers Helpers
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
register Bldr
|
29
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "evaluating a tilt template" do
|
4
|
+
it "registers with Tilt" do
|
5
|
+
Tilt['test.bldr'].should == Bldr::Template
|
6
|
+
end
|
7
|
+
|
8
|
+
it "renders a template" do
|
9
|
+
alex = Person.new
|
10
|
+
alex.name = 'alex'
|
11
|
+
|
12
|
+
tpl = Bldr::Template.new { "object(:person => alex) { attribute(:name) }" }
|
13
|
+
tpl.render(Bldr::Node.new, :alex => alex).should == jsonify({:person => {:name => 'alex'}})
|
14
|
+
end
|
15
|
+
|
16
|
+
it "works when render two top-level objects" do
|
17
|
+
alex = Person.new('alex')
|
18
|
+
john = Person.new('john')
|
19
|
+
|
20
|
+
tpl = Bldr::Template.new {
|
21
|
+
<<-RUBY
|
22
|
+
object(:person_1 => alex) { attribute(:name) }
|
23
|
+
object(:person_2 => john) { attribute(:name) }
|
24
|
+
RUBY
|
25
|
+
}
|
26
|
+
|
27
|
+
result = tpl.render(Bldr::Node.new, :alex => alex, :john => john)
|
28
|
+
result.should == jsonify({
|
29
|
+
:person_1 => {:name => 'alex'},
|
30
|
+
:person_2 => {:name => 'john'}
|
31
|
+
})
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,88 @@
|
|
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
|
+
|
12
|
+
get '/' do
|
13
|
+
alex = Person.new("alex", 25)
|
14
|
+
tpl = <<-RUBY
|
15
|
+
object(:dude => alex) do
|
16
|
+
attributes :name, :age
|
17
|
+
end
|
18
|
+
RUBY
|
19
|
+
|
20
|
+
status(200)
|
21
|
+
bldr tpl, :locals => {:alex => alex}
|
22
|
+
end
|
23
|
+
|
24
|
+
get '/collections' do
|
25
|
+
alex = Person.new('alex', 25)
|
26
|
+
|
27
|
+
tpl = <<-RUBY
|
28
|
+
object :person => alex do
|
29
|
+
attributes :name, :age
|
30
|
+
|
31
|
+
collection :friends => [Person.new("john", 24)] do
|
32
|
+
attributes :name, :age
|
33
|
+
end
|
34
|
+
end
|
35
|
+
RUBY
|
36
|
+
|
37
|
+
bldr tpl, :locals => {:alex => alex}
|
38
|
+
end
|
39
|
+
|
40
|
+
get '/template' do
|
41
|
+
bert = Person.new('bert', 25)
|
42
|
+
ernie = Person.new('ernie', 26)
|
43
|
+
bldr :'fixtures/nested_objects.json', :locals => {:bert => bert, :ernie => ernie}
|
44
|
+
end
|
45
|
+
|
46
|
+
get '/root_template' do
|
47
|
+
name = "john doe"
|
48
|
+
age = 26
|
49
|
+
|
50
|
+
bldr :'fixtures/root_template.json', :locals => {:name => name, :age => age}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
it "returns json for a simple single-level template" do
|
55
|
+
request = Rack::MockRequest.new(TestApp)
|
56
|
+
response = request.get '/'
|
57
|
+
response.status.should == 200
|
58
|
+
parse_json(response.body).should == {'dude' => {'name' => 'alex', 'age' => 25}}
|
59
|
+
end
|
60
|
+
|
61
|
+
it "properly serializes templates with collections" do
|
62
|
+
request = Rack::MockRequest.new(TestApp)
|
63
|
+
response = request.get '/collections'
|
64
|
+
|
65
|
+
response.status.should == 200
|
66
|
+
parse_json(response.body).should == {
|
67
|
+
'person'=> {'name' => 'alex', 'age' => 25, 'friends' => [{'name' => 'john', 'age' => 24}]}
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
it "works with template files" do
|
72
|
+
request = Rack::MockRequest.new(TestApp)
|
73
|
+
response = request.get '/template'
|
74
|
+
|
75
|
+
parse_json(response.body).should == {
|
76
|
+
'person' => {'name' => 'bert', 'age' => 25, 'name_age' => "bert 25",
|
77
|
+
'friend' => {'name' => 'ernie', 'age' => 26}
|
78
|
+
}
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
it "allows using root-level attributes" do
|
83
|
+
request = Rack::MockRequest.new(TestApp)
|
84
|
+
response = request.get '/root_template'
|
85
|
+
|
86
|
+
parse_json(response.body).should == {'name' => 'john doe', 'age' => 26}
|
87
|
+
end
|
88
|
+
end
|
data/spec/models/post.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rspec'
|
3
|
+
|
4
|
+
require 'yajl'
|
5
|
+
require 'tilt'
|
6
|
+
|
7
|
+
$:.unshift(File.dirname(File.expand_path(__FILE__)))
|
8
|
+
|
9
|
+
require File.join(File.dirname(File.expand_path(__FILE__)), "..", "lib", "bldr")
|
10
|
+
|
11
|
+
Dir['spec/models/*'].each { |f| require File.expand_path(f) }
|
12
|
+
|
13
|
+
RSpec.configure do |c|
|
14
|
+
def node_wrap(*args, &block)
|
15
|
+
Bldr::Node.new(*args, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Parse some json and return a ruby object
|
19
|
+
def parse_json(str)
|
20
|
+
Yajl::Parser.parse(str)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Jsonify a ruby object
|
24
|
+
def jsonify(hash)
|
25
|
+
Yajl::Encoder.encode(hash)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "the json encoding library" do
|
4
|
+
it "uses yajl by default" do
|
5
|
+
MultiJson.engine.should == MultiJson::Engines::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::Engines::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::Engines::JsonGem
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,275 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Node#attributes" do
|
4
|
+
def wrap(&block)
|
5
|
+
alex = Person.new('alex').tap { |p| p.age = 25; p }
|
6
|
+
Bldr::Node.new do
|
7
|
+
object(:person => alex, &block)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
it "adds attributes of the person to the result hash" do
|
12
|
+
node = wrap { attributes(:name, :age) }
|
13
|
+
node.render!.should == {:person => {:name => 'alex', :age => 25}}
|
14
|
+
end
|
15
|
+
|
16
|
+
it "supports dynamic block attributes with explicit object context" do
|
17
|
+
node = wrap do
|
18
|
+
attribute(:oldness) do |person|
|
19
|
+
"#{person.age} years"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
node.render!.should == {:person => {:oldness => "25 years"}}
|
24
|
+
end
|
25
|
+
|
26
|
+
it "supports dynamic block attributes with implicit object context" do
|
27
|
+
node = wrap do
|
28
|
+
attribute(:oldness) do
|
29
|
+
"#{age} years"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
node.render!.should == {:person => {:oldness => "25 years"}}
|
34
|
+
end
|
35
|
+
|
36
|
+
it "raises an error when you use the block syntax with more than one attribute" do
|
37
|
+
expect {
|
38
|
+
node_wrap {
|
39
|
+
attributes(:one, :two) do |person|
|
40
|
+
"..."
|
41
|
+
end
|
42
|
+
}
|
43
|
+
}.to raise_error(ArgumentError, "You may only pass one argument to #attribute when using the block syntax.")
|
44
|
+
end
|
45
|
+
|
46
|
+
it "returns nil attributes in the result" do
|
47
|
+
node = node_wrap do
|
48
|
+
object :person => Person.new('alex') do
|
49
|
+
attributes :name, :age
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
node.render!.should == {:person => {:name => 'alex', :age => nil}}
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "Node#render!" do
|
59
|
+
it "returns an empty hash when not passed an object" do
|
60
|
+
Bldr::Node.new.render!.should == {}
|
61
|
+
end
|
62
|
+
|
63
|
+
it "a document with a single node with no nesting" do
|
64
|
+
node = node_wrap do
|
65
|
+
object :person => Person.new('alex') do
|
66
|
+
attributes :name
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
node.render!.should == {:person => {:name => 'alex'}}
|
71
|
+
end
|
72
|
+
|
73
|
+
it "works for multiple top-level objects" do
|
74
|
+
alex, john = Person.new("alex"), Person.new("john")
|
75
|
+
|
76
|
+
node = node_wrap do
|
77
|
+
object(:alex => alex) do
|
78
|
+
attributes :name
|
79
|
+
end
|
80
|
+
|
81
|
+
object(:john => john) do
|
82
|
+
attributes :name
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
node.render!.should == {:alex => {:name => 'alex'}, :john => {:name => 'john'}}
|
87
|
+
end
|
88
|
+
|
89
|
+
it "recursively renders nested objects" do
|
90
|
+
node = node_wrap do
|
91
|
+
object :alex => Person.new("alex") do
|
92
|
+
attributes :name
|
93
|
+
|
94
|
+
object :friend => Person.new("john") do
|
95
|
+
attributes :name
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
node.render!.should == {
|
101
|
+
:alex => {
|
102
|
+
:name => 'alex',
|
103
|
+
:friend => {:name => 'john'}
|
104
|
+
}
|
105
|
+
}
|
106
|
+
end
|
107
|
+
|
108
|
+
describe "#attributes syntax" do
|
109
|
+
it "allows a hash to be sent where the keys are the result keys" do
|
110
|
+
alex = Person.new("alex").tap do |p|
|
111
|
+
p.age = 25
|
112
|
+
p
|
113
|
+
end
|
114
|
+
|
115
|
+
node = node_wrap do
|
116
|
+
object(:person => alex) do
|
117
|
+
attributes({:surname => :name}, :age)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
node.render!.should == {:person => {:surname => 'alex', :age => 25}}
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe "Node#object" do
|
127
|
+
it "evaluates the block and returns json" do
|
128
|
+
node = Bldr::Node.new
|
129
|
+
result = node.object(:dude => Person.new("alex")) do
|
130
|
+
attributes :name
|
131
|
+
|
132
|
+
object(:bro => Person.new("john")) do
|
133
|
+
attributes :name
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
result.should == jsonify({
|
138
|
+
:dude => {:name => 'alex', :bro => {:name => 'john'}}
|
139
|
+
})
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
describe "Node#to_json" do
|
144
|
+
it "recursively returns the result json" do
|
145
|
+
node = node_wrap do
|
146
|
+
object :person => Person.new("alex") do
|
147
|
+
attributes :name
|
148
|
+
|
149
|
+
object :friend => Person.new("pete", 30) do
|
150
|
+
attributes :name, :age
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
node.to_json.should == jsonify({
|
156
|
+
:person => {
|
157
|
+
:name => 'alex',
|
158
|
+
:friend => {:name => 'pete', :age => 30}
|
159
|
+
}
|
160
|
+
})
|
161
|
+
end
|
162
|
+
|
163
|
+
it "returns null values for nil attributes" do
|
164
|
+
node = node_wrap do
|
165
|
+
object :person => Person.new('alex') do
|
166
|
+
attributes :name, :age
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
parse_json(node.to_json)['person'].should have_key('age')
|
171
|
+
parse_json(node.to_json)['person']['age'].should be_nil
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
describe "Node#collection" do
|
176
|
+
it "iterates through the collection and renders them as nodes" do
|
177
|
+
node = node_wrap do
|
178
|
+
object :person => Person.new('alex', 26) do
|
179
|
+
attributes :name, :age
|
180
|
+
|
181
|
+
collection :friends => [Person.new('john', 24), Person.new('jeff', 25)] do
|
182
|
+
attributes :name, :age
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
node.render!.should == {
|
188
|
+
:person => {
|
189
|
+
:name => 'alex', :age => 26,
|
190
|
+
:friends => [{:name => 'john', :age => 24}, {:name => 'jeff', :age => 25}]
|
191
|
+
}
|
192
|
+
}
|
193
|
+
end
|
194
|
+
|
195
|
+
it "renders properly when a collection is the root node" do
|
196
|
+
nodes = node_wrap do
|
197
|
+
collection :people => [Person.new('bert'), Person.new('ernie')] do
|
198
|
+
attributes :name
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
nodes.render!.should == {:people => [{:name => 'bert'}, {:name => 'ernie'}]}
|
203
|
+
end
|
204
|
+
|
205
|
+
it "renders nested collections properly" do
|
206
|
+
post = Post.new("my post")
|
207
|
+
post.comments << Comment.new('my comment')
|
208
|
+
|
209
|
+
nodes = node_wrap do
|
210
|
+
collection :posts => [post] do
|
211
|
+
attributes :title
|
212
|
+
attribute(:comment_count) { |post| post.comments.count }
|
213
|
+
|
214
|
+
collection :comments => current_object.comments do
|
215
|
+
attributes :body
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
nodes.render!.should == {
|
221
|
+
:posts => [
|
222
|
+
{:title => 'my post', :comment_count => 1, :comments => [{:body => 'my comment'}]}
|
223
|
+
]
|
224
|
+
}
|
225
|
+
end
|
226
|
+
|
227
|
+
it "renders nested collections with dynamic property values correctly" do
|
228
|
+
post1 = Post.new("post 1")
|
229
|
+
post2 = Post.new("post 2")
|
230
|
+
post1.comments << Comment.new('post 1 comment')
|
231
|
+
post2.comments << Comment.new('post 2 first comment')
|
232
|
+
post2.comments << Comment.new('post 2 second comment')
|
233
|
+
|
234
|
+
nodes = node_wrap do
|
235
|
+
collection :posts => [post1, post2] do
|
236
|
+
attributes :title
|
237
|
+
attribute(:comment_count) { |post| post.comments.count }
|
238
|
+
|
239
|
+
collection :comments => current_object.comments do
|
240
|
+
attributes :body
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
nodes.render!.should == {
|
246
|
+
:posts => [
|
247
|
+
{
|
248
|
+
:title => 'post 1',
|
249
|
+
:comment_count => 1,
|
250
|
+
:comments => [{:body => 'post 1 comment'}]
|
251
|
+
},
|
252
|
+
{
|
253
|
+
:title => 'post 2',
|
254
|
+
:comment_count => 2,
|
255
|
+
:comments => [{:body => 'post 2 first comment'}, {:body => 'post 2 second comment'}]
|
256
|
+
}
|
257
|
+
]
|
258
|
+
}
|
259
|
+
end
|
260
|
+
|
261
|
+
it "allows root level attributes using local variables" do
|
262
|
+
node = node_wrap do
|
263
|
+
name = "john doe"
|
264
|
+
age = 25
|
265
|
+
|
266
|
+
object do
|
267
|
+
attribute(:name) { name }
|
268
|
+
attribute(:age) { age }
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
node.render!.should == {:name => 'john doe', :age => 25}
|
273
|
+
end
|
274
|
+
|
275
|
+
end
|
metadata
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bldr
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.1.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Alex Sharp
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-08-04 00:00:00 -07:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: multi_json
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ~>
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 1.0.3
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: json_pure
|
29
|
+
prerelease: false
|
30
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: "0"
|
36
|
+
type: :development
|
37
|
+
version_requirements: *id002
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: sinatra
|
40
|
+
prerelease: false
|
41
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ~>
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 1.2.6
|
47
|
+
type: :development
|
48
|
+
version_requirements: *id003
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: tilt
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ~>
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: 1.3.2
|
58
|
+
type: :development
|
59
|
+
version_requirements: *id004
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: yajl-ruby
|
62
|
+
prerelease: false
|
63
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: "0"
|
69
|
+
type: :development
|
70
|
+
version_requirements: *id005
|
71
|
+
description: Provides a simple and intuitive templating DSL for serializing objects to JSON.
|
72
|
+
email:
|
73
|
+
- ajsharp@gmail.com
|
74
|
+
executables: []
|
75
|
+
|
76
|
+
extensions: []
|
77
|
+
|
78
|
+
extra_rdoc_files: []
|
79
|
+
|
80
|
+
files:
|
81
|
+
- .gitignore
|
82
|
+
- .rvmrc
|
83
|
+
- .travis.yml
|
84
|
+
- Gemfile
|
85
|
+
- MIT-LICENSE
|
86
|
+
- README.md
|
87
|
+
- Rakefile
|
88
|
+
- bldr.gemspec
|
89
|
+
- lib/bldr.rb
|
90
|
+
- lib/bldr/engine.rb
|
91
|
+
- lib/bldr/node.rb
|
92
|
+
- lib/bldr/template.rb
|
93
|
+
- lib/bldr/version.rb
|
94
|
+
- lib/sinatra/bldr.rb
|
95
|
+
- spec/fixtures/nested_objects.json.bldr
|
96
|
+
- spec/fixtures/root_template.json.bldr
|
97
|
+
- spec/functional/tilt_template_spec.rb
|
98
|
+
- spec/integration/sinatra_spec.rb
|
99
|
+
- spec/models/comment.rb
|
100
|
+
- spec/models/person.rb
|
101
|
+
- spec/models/post.rb
|
102
|
+
- spec/spec_helper.rb
|
103
|
+
- spec/unit/bldr_spec.rb
|
104
|
+
- spec/unit/node_spec.rb
|
105
|
+
has_rdoc: true
|
106
|
+
homepage: https://github.com/ajsharp/bldr
|
107
|
+
licenses: []
|
108
|
+
|
109
|
+
post_install_message:
|
110
|
+
rdoc_options: []
|
111
|
+
|
112
|
+
require_paths:
|
113
|
+
- lib
|
114
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
115
|
+
none: false
|
116
|
+
requirements:
|
117
|
+
- - ">="
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: "0"
|
120
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: "0"
|
126
|
+
requirements: []
|
127
|
+
|
128
|
+
rubyforge_project: bldr
|
129
|
+
rubygems_version: 1.6.2
|
130
|
+
signing_key:
|
131
|
+
specification_version: 3
|
132
|
+
summary: Templating library with a simple, minimalist DSL.
|
133
|
+
test_files:
|
134
|
+
- spec/fixtures/nested_objects.json.bldr
|
135
|
+
- spec/fixtures/root_template.json.bldr
|
136
|
+
- spec/functional/tilt_template_spec.rb
|
137
|
+
- spec/integration/sinatra_spec.rb
|
138
|
+
- spec/models/comment.rb
|
139
|
+
- spec/models/person.rb
|
140
|
+
- spec/models/post.rb
|
141
|
+
- spec/spec_helper.rb
|
142
|
+
- spec/unit/bldr_spec.rb
|
143
|
+
- spec/unit/node_spec.rb
|