jbuilder 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 David Heinemeier Hansson, 37signals
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.
@@ -0,0 +1,73 @@
1
+ Jbuilder
2
+ ========
3
+
4
+ Jbuilder gives you a simple DSL for declaring JSON structures that beats massaging giant hash structures. This is particularly helpful when the generation process is fraught with conditionals and loops. Here's a simple example:
5
+
6
+ Jbuilder.encode do |json|
7
+ json.content format_content(@message.content)
8
+ json.(@message, :created_at, :updated_at)
9
+
10
+ json.author do |json|
11
+ json.name @message.creator.name.familiar
12
+ json.email_address @message.creator.email_address_with_name
13
+ json.url url_for(@message.creator, format: :json)
14
+ end
15
+
16
+ if current_user.admin?
17
+ json.visitors calculate_visitors(@message)
18
+ end
19
+
20
+ json.comments @message.comments, :content, :created_at
21
+
22
+ json.attachments @message.attachments do |json, attachment|
23
+ json.filename attachment.filename
24
+ json.url url_for(attachment)
25
+ end
26
+ end
27
+
28
+ This will build the following structure:
29
+
30
+ {
31
+ "content": "<p>This is <i>serious</i> monkey business",
32
+ "created_at": "2011-10-29T20:45:28-05:00",
33
+ "updated_at": "2011-10-29T20:45:28-05:00",
34
+
35
+ "author": {
36
+ "name": "David H.",
37
+ "email_address": "'David Heinemeier Hansson' <david@heinemeierhansson.com>",
38
+ "url": "http://example.com/users/1-david.json"
39
+ },
40
+
41
+ "visitors": 15,
42
+
43
+ "comments": [
44
+ { "content": "Hello everyone!", "created_at": "2011-10-29T20:45:28-05:00" },
45
+ { "content": "To you my good sir!", "created_at": "2011-10-29T20:47:28-05:00" }
46
+ ],
47
+
48
+ "attachment": [
49
+ { "filename": "forecast.xls", "url": "http://example.com/downloads/forecast.xls" },
50
+ { "filename": "presentation.pdf", "url": "http://example.com/downloads/presentation.pdf" }
51
+ ]
52
+ }
53
+
54
+ You can either use Jbuilder stand-alone or directly as an ActionView template language. When required in Rails, you can create views ala show.json.jbuilder (the json is already yielded):
55
+
56
+ # Any helpers available to views are available to the builder
57
+ json.content format_content(@message.content)
58
+ json.(@message, :created_at, :updated_at)
59
+
60
+ json.author do |json|
61
+ json.name @message.creator.name.familiar
62
+ json.email_address @message.creator.email_address_with_name
63
+ json.url url_for(@message.creator, format: :json)
64
+ end
65
+
66
+ if current_user.admin?
67
+ json.visitors calculate_visitors(@message)
68
+ end
69
+
70
+ # You can use partials as well, just remember to pass in the json instance
71
+ json.partial! "api/comments/comments" @message.comments
72
+
73
+ Note: Jbuilder is similar to Garrett Bjerkhoel's json_builder, which I discovered after making this, but the DSL has taken a different turn and will retain the explicit yield style (vs json_builder's 3.0's move to instance_eval).
@@ -0,0 +1,12 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'jbuilder'
3
+ s.version = '0.3'
4
+ s.author = 'David Heinemeier Hansson'
5
+ s.email = 'david@37signals.com'
6
+ s.summary = 'Create JSON structures via a Builder-style DSL'
7
+
8
+ s.add_dependency 'activesupport', '>= 3.0.0'
9
+ s.add_dependency 'blankslate', '>= 2.1.2.4'
10
+
11
+ s.files = Dir["#{File.dirname(__FILE__)}/**/*"]
12
+ end
@@ -0,0 +1,165 @@
1
+ require 'blankslate'
2
+ require 'active_support/ordered_hash'
3
+ require 'active_support/core_ext/array/access'
4
+ require 'active_support/core_ext/enumerable'
5
+ require 'active_support/json'
6
+
7
+ class Jbuilder < BlankSlate
8
+ # Yields a builder and automatically turns the result into a JSON string
9
+ def self.encode
10
+ new._tap { |jbuilder| yield jbuilder }.target!
11
+ end
12
+
13
+ define_method(:__class__, find_hidden_method(:class))
14
+ define_method(:_tap, find_hidden_method(:tap))
15
+
16
+ def initialize
17
+ @attributes = ActiveSupport::OrderedHash.new
18
+ end
19
+
20
+ # Turns the current element into an array and yields a builder to add a hash.
21
+ #
22
+ # Example:
23
+ #
24
+ # json.comments do |json|
25
+ # json.child! { |json| json.content "hello" }
26
+ # json.child! { |json| json.content "world" }
27
+ # end
28
+ #
29
+ # { "comments": [ { "content": "hello" }, { "content": "world" } ]}
30
+ #
31
+ # More commonly, you'd use the combined iterator, though:
32
+ #
33
+ # json.comments(@post.comments) do |json, comment|
34
+ # json.content comment.formatted_content
35
+ # end
36
+ def child!
37
+ @attributes = [] unless @attributes.is_a? Array
38
+ @attributes << _new_instance._tap { |jbuilder| yield jbuilder }.attributes!
39
+ end
40
+
41
+ # Iterates over the passed collection and adds each iteration as an element of the resulting array.
42
+ #
43
+ # Example:
44
+ #
45
+ # json.array!(@people) do |json, person|
46
+ # json.name person.name
47
+ # json.age calculate_age(person.birthday)
48
+ # end
49
+ #
50
+ # [ { "David", 32 }, { "Jamie", 31 } ]
51
+ #
52
+ # If you are using Ruby 1.9+, you can use the call syntax instead of an explicit extract! call:
53
+ #
54
+ # json.(@people) { |json, person| ... }
55
+ #
56
+ # It's generally only needed to use this method for top-level arrays. If you have named arrays, you can do:
57
+ #
58
+ # json.people(@people) do |json, person|
59
+ # json.name person.name
60
+ # json.age calculate_age(person.birthday)
61
+ # end
62
+ #
63
+ # { "people": [ { "David", 32 }, { "Jamie", 31 } ] }
64
+ def array!(collection)
65
+ collection.each do |element|
66
+ child! do |child|
67
+ yield child, element
68
+ end
69
+ end
70
+ end
71
+
72
+ # Extracts the mentioned attributes from the passed object and turns them into attributes of the JSON.
73
+ #
74
+ # Example:
75
+ #
76
+ # json.extract! @person, :name, :age
77
+ #
78
+ # { "David", 32 }, { "Jamie", 31 }
79
+ #
80
+ # If you are using Ruby 1.9+, you can use the call syntax instead of an explicit extract! call:
81
+ #
82
+ # json.(@person, :name, :age)
83
+ def extract!(object, *attributes)
84
+ attributes.each do |attribute|
85
+ __send__ attribute, object.send(attribute)
86
+ end
87
+ end
88
+
89
+ if RUBY_VERSION > '1.9'
90
+ def call(*args)
91
+ case
92
+ when args.one?
93
+ array!(args.first) { |json, element| yield json, element }
94
+ when args.many?
95
+ extract!(*args)
96
+ end
97
+ end
98
+ end
99
+
100
+ # Returns the attributes of the current builder.
101
+ def attributes!
102
+ @attributes
103
+ end
104
+
105
+ # Encodes the current builder as JSON.
106
+ def target!
107
+ ActiveSupport::JSON.encode @attributes
108
+ end
109
+
110
+
111
+ private
112
+ def method_missing(method, *args)
113
+ case
114
+ when args.one? && block_given?
115
+ _yield_iteration(method, args.first) { |child, element| yield child, element }
116
+ when args.one?
117
+ _assign method, args.first
118
+ when args.empty? && block_given?
119
+ _yield_nesting(method) { |jbuilder| yield jbuilder }
120
+ when args.many? && args.first.is_a?(Enumerable)
121
+ _inline_nesting method, args.first, args.from(1)
122
+ when args.many?
123
+ _inline_extract method, args.first, args.from(1)
124
+ end
125
+ end
126
+
127
+ def _assign(key, value)
128
+ @attributes[key] = value
129
+ end
130
+
131
+ # Overwrite in subclasses if you need to add initialization values
132
+ def _new_instance
133
+ __class__.new
134
+ end
135
+
136
+ def _yield_nesting(container)
137
+ @attributes[container] = _new_instance._tap { |jbuilder| yield jbuilder }.attributes!
138
+ end
139
+
140
+ def _inline_nesting(container, collection, attributes)
141
+ __send__(container) do |parent|
142
+ collection.each do |element|
143
+ parent.child! do |child|
144
+ attributes.each do |attribute|
145
+ child.__send__ attribute, element.send(attribute)
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ def _yield_iteration(container, collection)
153
+ __send__(container) do |parent|
154
+ parent.array!(collection) do |child, element|
155
+ yield child, element
156
+ end
157
+ end
158
+ end
159
+
160
+ def _inline_extract(container, record, attributes)
161
+ __send__(container) { |parent| parent.extract! record, *attributes }
162
+ end
163
+ end
164
+
165
+ require "jbuilder_template" if defined?(ActionView::Template)
@@ -0,0 +1,23 @@
1
+ class JbuilderTemplate < Jbuilder
2
+ def self.encode(context)
3
+ new(context)._tap { |jbuilder| yield jbuilder }.target!
4
+ end
5
+
6
+ def initialize(context)
7
+ @context = context
8
+ super()
9
+ end
10
+
11
+ def partial!(partial_name, options = {})
12
+ @context.render(partial_name, options.merge(json: self))
13
+ end
14
+
15
+ private
16
+ def _new_instance
17
+ __class__.new(@context)
18
+ end
19
+ end
20
+
21
+ ActionView::Template.register_template_handler :jbuilder, Proc.new { |template|
22
+ "if defined?(json); #{template.source}; else; JbuilderTemplate.encode(self) do |json|;#{template.source};end; end;"
23
+ }
@@ -0,0 +1,195 @@
1
+ require 'test/unit'
2
+ require 'active_support/test_case'
3
+
4
+ require 'jbuilder'
5
+
6
+ class JbuilderTest < ActiveSupport::TestCase
7
+ test "single key" do
8
+ json = Jbuilder.encode do |json|
9
+ json.content "hello"
10
+ end
11
+
12
+ assert_equal "hello", JSON.parse(json)["content"]
13
+ end
14
+
15
+ test "multiple keys" do
16
+ json = Jbuilder.encode do |json|
17
+ json.title "hello"
18
+ json.content "world"
19
+ end
20
+
21
+ JSON.parse(json).tap do |parsed|
22
+ assert_equal "hello", parsed["title"]
23
+ assert_equal "world", parsed["content"]
24
+ end
25
+ end
26
+
27
+ test "extracting from object" do
28
+ person = Struct.new(:name, :age).new("David", 32)
29
+
30
+ json = Jbuilder.encode do |json|
31
+ json.extract! person, :name, :age
32
+ end
33
+
34
+ JSON.parse(json).tap do |parsed|
35
+ assert_equal "David", parsed["name"]
36
+ assert_equal 32, parsed["age"]
37
+ end
38
+ end
39
+
40
+ test "extracting from object using call style for 1.9" do
41
+ person = Struct.new(:name, :age).new("David", 32)
42
+
43
+ json = Jbuilder.encode do |json|
44
+ json.(person, :name, :age)
45
+ end
46
+
47
+ JSON.parse(json).tap do |parsed|
48
+ assert_equal "David", parsed["name"]
49
+ assert_equal 32, parsed["age"]
50
+ end
51
+ end
52
+
53
+ test "nesting single child with block" do
54
+ json = Jbuilder.encode do |json|
55
+ json.author do |json|
56
+ json.name "David"
57
+ json.age 32
58
+ end
59
+ end
60
+
61
+ JSON.parse(json).tap do |parsed|
62
+ assert_equal "David", parsed["author"]["name"]
63
+ assert_equal 32, parsed["author"]["age"]
64
+ end
65
+ end
66
+
67
+ test "nesting multiple children with block" do
68
+ json = Jbuilder.encode do |json|
69
+ json.comments do |json|
70
+ json.child! { |json| json.content "hello" }
71
+ json.child! { |json| json.content "world" }
72
+ end
73
+ end
74
+
75
+ JSON.parse(json).tap do |parsed|
76
+ assert_equal "hello", parsed["comments"].first["content"]
77
+ assert_equal "world", parsed["comments"].second["content"]
78
+ end
79
+ end
80
+
81
+ test "nesting single child with inline extract" do
82
+ person = Class.new do
83
+ attr_reader :name, :age
84
+
85
+ def initialize(name, age)
86
+ @name, @age = name, age
87
+ end
88
+ end.new("David", 32)
89
+
90
+ json = Jbuilder.encode do |json|
91
+ json.author person, :name, :age
92
+ end
93
+
94
+ JSON.parse(json).tap do |parsed|
95
+ assert_equal "David", parsed["author"]["name"]
96
+ assert_equal 32, parsed["author"]["age"]
97
+ end
98
+ end
99
+
100
+ test "nesting multiple children from array" do
101
+ comments = [ Struct.new(:content, :id).new("hello", 1), Struct.new(:content, :id).new("world", 2) ]
102
+
103
+ json = Jbuilder.encode do |json|
104
+ json.comments comments, :content
105
+ end
106
+
107
+ JSON.parse(json).tap do |parsed|
108
+ assert_equal ["content"], parsed["comments"].first.keys
109
+ assert_equal "hello", parsed["comments"].first["content"]
110
+ assert_equal "world", parsed["comments"].second["content"]
111
+ end
112
+ end
113
+
114
+
115
+ test "nesting multiple children from array with inline loop" do
116
+ comments = [ Struct.new(:content, :id).new("hello", 1), Struct.new(:content, :id).new("world", 2) ]
117
+
118
+ json = Jbuilder.encode do |json|
119
+ json.comments comments do |json, comment|
120
+ json.content comment.content
121
+ end
122
+ end
123
+
124
+ JSON.parse(json).tap do |parsed|
125
+ assert_equal ["content"], parsed["comments"].first.keys
126
+ assert_equal "hello", parsed["comments"].first["content"]
127
+ assert_equal "world", parsed["comments"].second["content"]
128
+ end
129
+ end
130
+
131
+ test "nesting multiple children from array with inline loop on root" do
132
+ comments = [ Struct.new(:content, :id).new("hello", 1), Struct.new(:content, :id).new("world", 2) ]
133
+
134
+ json = Jbuilder.encode do |json|
135
+ json.(comments) do |json, comment|
136
+ json.content comment.content
137
+ end
138
+ end
139
+
140
+ JSON.parse(json).tap do |parsed|
141
+ assert_equal "hello", parsed.first["content"]
142
+ assert_equal "world", parsed.second["content"]
143
+ end
144
+ end
145
+
146
+ test "array nested inside nested hash" do
147
+ json = Jbuilder.encode do |json|
148
+ json.author do |json|
149
+ json.name "David"
150
+ json.age 32
151
+
152
+ json.comments do |json|
153
+ json.child! { |json| json.content "hello" }
154
+ json.child! { |json| json.content "world" }
155
+ end
156
+ end
157
+ end
158
+
159
+ JSON.parse(json).tap do |parsed|
160
+ assert_equal "hello", parsed["author"]["comments"].first["content"]
161
+ assert_equal "world", parsed["author"]["comments"].second["content"]
162
+ end
163
+ end
164
+
165
+ test "array nested inside array" do
166
+ json = Jbuilder.encode do |json|
167
+ json.comments do |json|
168
+ json.child! do |json|
169
+ json.authors do |json|
170
+ json.child! do |json|
171
+ json.name "david"
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+ assert_equal "david", JSON.parse(json)["comments"].first["authors"].first["name"]
179
+ end
180
+
181
+ test "top-level array" do
182
+ comments = [ Struct.new(:content, :id).new("hello", 1), Struct.new(:content, :id).new("world", 2) ]
183
+
184
+ json = Jbuilder.encode do |json|
185
+ json.array!(comments) do |json, comment|
186
+ json.content comment.content
187
+ end
188
+ end
189
+
190
+ JSON.parse(json).tap do |parsed|
191
+ assert_equal "hello", parsed.first["content"]
192
+ assert_equal "world", parsed.second["content"]
193
+ end
194
+ end
195
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jbuilder
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.3'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - David Heinemeier Hansson
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-11-30 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: &70104549490040 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70104549490040
25
+ - !ruby/object:Gem::Dependency
26
+ name: blankslate
27
+ requirement: &70104549489160 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 2.1.2.4
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70104549489160
36
+ description:
37
+ email: david@37signals.com
38
+ executables: []
39
+ extensions: []
40
+ extra_rdoc_files: []
41
+ files:
42
+ - ./jbuilder.gemspec
43
+ - ./lib/jbuilder.rb
44
+ - ./lib/jbuilder_template.rb
45
+ - ./MIT-LICENSE
46
+ - ./README.md
47
+ - ./test/jbuilder_test.rb
48
+ homepage:
49
+ licenses: []
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ! '>='
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubyforge_project:
68
+ rubygems_version: 1.8.11
69
+ signing_key:
70
+ specification_version: 3
71
+ summary: Create JSON structures via a Builder-style DSL
72
+ test_files: []