jbuilder 0.3

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,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: []