Skip to content

Commit

Permalink
Make it easier to build JSON arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
rubys committed Apr 1, 2012
1 parent c44df02 commit 5d1146b
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 97 deletions.
73 changes: 55 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,37 @@ the element id and class id syntax and based on the implementation from
Wunderbar's JSON support is inspired by David Heinemeier Hansson's
[jbuilder](https://github.com/rails/jbuilder).

Overview
---

The premise of Wunderbar is that output of various types are typically formed
by appending either a series of key/value pairs or simple values, and those
operations should be optimized for. Appending a key/value pair is done via:

_key value

... and appending a simple value is done thus:

_ value

For HTML, key/value is used for element nodes, and simple values are used for
text nodes. For JSON, key/value is used for Hashes and simple values are used
for arrays. For text, simple values are output via puts, and key/value pairs
are not used.

Nesting is performed using blocks.

The underscore method, when passed no arguments, returns an object that can be
used to perform a number of special functions. Some of those functions are
unique to the output method. Others like logging methods are common.

The underscore method when passed multiple arguments or a combination of
arguments and a block may do other common functions, depending on the types of
the arguments passed.

Question mark, exclamation mark, and underscore suffixes to the method name
may modify the results.

Quick Start (HTML)
---

Expand Down Expand Up @@ -157,13 +188,18 @@ Access to all of the builder _defined_ methods (typically these end in an esclam
* `_.tag! :foo`
* `_.error 'Log message'`

XHTML differs from HTML in the escaping of inline style and script elements.
XHTML will also fall back to HTML output unless the user agent indicates it
supports XHTML via the HTTP Accept header.

Methods provided to Wunderbar.json
---

The default is to return a JSON encoded Hash.
Common operations are to return a Hash or an Array of values. Hashes are
a series of name/value pairs, and Arrays are a series of values.

``` ruby
W_.json do
Wunderbar.json do
_content format_content(@message.content)
_ @message, :created_at, :updated_at

Expand Down Expand Up @@ -193,33 +229,34 @@ also be nested. Logic can be freely intermixed.

The "`_`" method serves a number of purposes.

* calling it with a single Hash argument will merge the values into the result
* Example: `_ status: 200, body: "Hello World!"`

* calling it with multiple arguments will cause the first argument to be
treated as the object, and the remainder as the attributes to be extracted
* Example: `_ File.stat('foo'), :mtime, :size, :mode`

* calling it with a single non-Hash argument will establish that value as the
result to be returned. Useful for returning arrays. Can't preceed or follow
any setting of Hash values.
* Example: `_ [1,2,3]`

* calling it with a single Enumerable object and a block will cause an array
to be returned based on mapping each objection from the enumeration against
the block
* Example: `_([1,2,3]) {|n| n*n}`

Special methods for arrays:
* arrays can be also be built using the `_` method:
_ 1
_ 2

The `_` method returns a proxy to the object being constructed. This is often
handy when called with no arguments. Examples:

_.sort!
_['foo'] = 'bar'

Methods provided to Wunderbar.text
---

* Building simple arrays can be done with `<<` methods:
_ << 1
_ << 2
Appending to the output stream is done using the `_` method, which is
equivalent to `puts`. The `_` method returns an object which proxies the
output stream, which provides access to other useful methods, for example:

* The precedence rules make this difficult for more complex structures, so a
`push!` method is provided:
_.push! { _name 'foo' }
_.push! { _name 'bar' }
_.print 'foo'
_.printf "Hello %s!\n", 'world'

Globals provided
---
Expand Down
6 changes: 4 additions & 2 deletions demo/wiki.rb
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,13 @@
W_.json do
hash = Digest::MD5.hexdigest(@markup)
if File.exist?(file) and Digest::MD5.hexdigest(File.read(file)) != @hash
_ error: "Write conflict", markup: File.read(file), hash: hash
_error "Write conflict"
_markup File.read(file)
else
File.open(file, 'w') {|fh| fh.write @markup} unless @hash == hash
_ time: Time.now.to_i*1000, hash: hash
_time Time.now.to_i*1000
end
_hash hash
end

__END__
Expand Down
40 changes: 13 additions & 27 deletions lib/wunderbar/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -244,13 +244,12 @@ def method_missing(method, *args, &block)
if Symbol === result or String === result
result = {result.to_s => JsonBuilder.new.encode(&block)}
else
target = @_target
result = result.map {|n| @_target = {}; block.call(n); @_target}
@_target = target
end
end
elsif block
::Kernel::raise ::ArgumentError, "can't mix arguments with a block"
::Kernel::raise ::ArgumentError,
"can't mix multiple arguments with a block"
else
object = args.shift

Expand All @@ -266,40 +265,27 @@ def method_missing(method, *args, &block)

if name != ''
unless Hash === @_target or @_target.empty?
::Kernel::raise ::ArgumentError, "named values after literal" +
' ' + @_target.inspect
::Kernel::raise ::ArgumentError, "mixed array and hash calls"
end

@_target[name] = result
else
unless Hash === @_target and @_target.empty?
if Hash === result
result = @_target.merge(result)
else
::Kernel::raise ::ArgumentError, "literal after named values"
end
@_target[name.to_s] = result
elsif args.length == 0 or (args.length == 1 and not block)
@_target = [] if @_target == {}

if Hash === @_target
::Kernel::raise ::ArgumentError, "mixed hash and array calls"
end

@_target << result
else
@_target = result
end

self
end

def <<(object)
@_target = [] if @_target == {}
@_target << object
end

def push!(*args, &block)
if block
if args.length > 0
::Kernel::raise ::ArgumentError, "can't mix arguments with a block"
end
self << JsonBuilder.new.encode(&block)
else
args.each {|arg| self << arg}
end
def _!(object)
@_target = object
end

def target!
Expand Down
53 changes: 25 additions & 28 deletions test/test_jbuilder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@
# functional parity:
# https://github.com/rails/jbuilder/blob/master/test/jbuilder_test.rb

class Array
def second
self[1]
end
end

class JbuilderTest < Test::Unit::TestCase
def setup
@j = Wunderbar::JsonBuilder.new
Expand Down Expand Up @@ -78,13 +72,13 @@ def test_nesting_single_child_with_block
def test_nesting_multiple_children_with_block
parsed = @j.encode do
_comments do
_.push! { _content "hello" }
_.push! { _content "world" }
_ { _content "hello" }
_ { _content "world" }
end
end

assert_equal "hello", parsed["comments"].first["content"]
assert_equal "world", parsed["comments"].second["content"]
assert_equal "hello", parsed["comments"][0]["content"]
assert_equal "world", parsed["comments"][1]["content"]
end

def test_nesting_single_child_with_inline_extract
Expand All @@ -105,15 +99,16 @@ def initialize(name, age)
end

def test_nesting_multiple_children_from_array
comments = [ Struct.new(:content, :id).new("hello", 1), Struct.new(:content, :id).new("world", 2) ]
comments = [ Struct.new(:content, :id).new("hello", 1),
Struct.new(:content, :id).new("world", 2) ]

parsed = @j.encode do
_comments comments, :content
end

assert_equal ["content"], parsed["comments"].first.keys
assert_equal "hello", parsed["comments"].first["content"]
assert_equal "world", parsed["comments"].second["content"]
assert_equal "hello", parsed["comments"][0]["content"]
assert_equal "world", parsed["comments"][1]["content"]
end

def test_nesting_multiple_children_from_array_when_child_array_is_empty
Expand All @@ -129,7 +124,8 @@ def test_nesting_multiple_children_from_array_when_child_array_is_empty
end

def test_nesting_multiple_children_from_array_with_inline_loop
comments = [ Struct.new(:content, :id).new("hello", 1), Struct.new(:content, :id).new("world", 2) ]
comments = [ Struct.new(:content, :id).new("hello", 1),
Struct.new(:content, :id).new("world", 2) ]

parsed = @j.encode do
_comments comments do |comment|
Expand All @@ -138,8 +134,8 @@ def test_nesting_multiple_children_from_array_with_inline_loop
end

assert_equal ["content"], parsed["comments"].first.keys
assert_equal "hello", parsed["comments"].first["content"]
assert_equal "world", parsed["comments"].second["content"]
assert_equal "hello", parsed["comments"][0]["content"]
assert_equal "world", parsed["comments"][1]["content"]
end

def test_nesting_multiple_children_from_array_with_inline_loop_on_root
Expand All @@ -151,8 +147,8 @@ def test_nesting_multiple_children_from_array_with_inline_loop_on_root
end
end

assert_equal "hello", parsed.first["content"]
assert_equal "world", parsed.second["content"]
assert_equal "hello", parsed[0]["content"]
assert_equal "world", parsed[1]["content"]
end

def test_array_nested_inside_nested_hash
Expand All @@ -162,22 +158,22 @@ def test_array_nested_inside_nested_hash
_age 32

_comments do
_.push! { _content "hello" }
_.push! { _content "world" }
_ { _content "hello" }
_ { _content "world" }
end
end
end

assert_equal "hello", parsed["author"]["comments"].first["content"]
assert_equal "world", parsed["author"]["comments"].second["content"]
assert_equal "hello", parsed["author"]["comments"][0]["content"]
assert_equal "world", parsed["author"]["comments"][1]["content"]
end

def test_array_nested_inside_array
parsed = @j.encode do
_comments do
_.push! do
_ do
_authors do
_.push! do
_ do
_name "david"
end
end
Expand All @@ -189,16 +185,17 @@ def test_array_nested_inside_array
end

def test_top_level_array
comments = [ Struct.new(:content, :id).new("hello", 1), Struct.new(:content, :id).new("world", 2) ]
comments = [ Struct.new(:content, :id).new("hello", 1),
Struct.new(:content, :id).new("world", 2) ]

parsed = @j.encode do
_ comments do |comment|
_content comment.content
end
end

assert_equal "hello", parsed.first["content"]
assert_equal "world", parsed.second["content"]
assert_equal "hello", parsed[0]["content"]
assert_equal "world", parsed[1]["content"]
end

def test_empty_top_level_array
Expand All @@ -215,7 +212,7 @@ def test_empty_top_level_array

def test_dynamically_set_a_key_value
parsed = @j.encode do
_ "each" => "stuff"
_["each"] = "stuff"
end

assert_equal "stuff", parsed["each"]
Expand Down
Loading

0 comments on commit 5d1146b

Please sign in to comment.