Permalink
Browse files

initial import

People who become ill from inhaling asbestos are often those who are
exposed on a day-to-day basis in a job where they worked directly
with the material.
  • Loading branch information...
0 parents commit b7a03728278675413bfdd7078ec4a75d76a36b31 @mislav committed Jun 23, 2009
Showing with 295 additions and 0 deletions.
  1. +86 −0 README.markdown
  2. +3 −0 init.rb
  3. +131 −0 lib/asbestos.rb
  4. +75 −0 spec/asbestos_spec.rb
@@ -0,0 +1,86 @@
+Asbestos
+========
+
+Template handler for Rails that allows you to use
+a subset of XML Builder markup to produce JSON.
+
+Take, for instance, a common "show.xml.builder" template:
+
+ xml.instruct!
+ xml.category(:private => false) do
+ xml.name @category.name
+ xml.parent do
+ xml.id @category.parent_id
+ xml.name @category.parent.name
+ end
+ end
+
+If you copied that to "show.json.asbestos", you would get:
+
+ {"category": {
+ "private": "false",
+ "name": "Science & Technology",
+ "parent": {
+ "id": 1,
+ "name": "Religion"
+ }
+ }}
+
+But of course, you don't want to duplicate your builder template
+in another file, so we'll handle this in the controller:
+
+ def show
+ respond_to do |wants|
+ wants.xml
+ wants.json {
+ # takes the "show.xml.builder" template and renders it to JSON
+ render_json_from_xml
+ }
+ end
+ end
+
+With this method there's no need for a special template file for JSON.
+Asbestos is designed to use existing XML Builder templates.
+
+How does it work?
+-----------------
+
+The `xml` variable in your normal builder templates is the XML Builder object.
+When you call methods on this object, it turns that into XML nodes and appends
+everything to a string, which is later returned as the result of rendering.
+
+This plugin provides Asbestos::Builder, which tries to mimic the behavior
+of the XML Builder while saving all the data to a big nested ruby hash.
+In the end, `to_json` is called on it.
+
+Aggregates and ignores (important!)
+-----------------------------------
+
+Problems start when you have Builder templates that render *collections*,
+like in index actions:
+
+ xml.instruct!
+ for category in @categories
+ xml.category do
+ # ...
+ end
+ end
+
+There is a ruby loop in there, so if there are multiple categories the resulting
+JSON would have just one. This is because the same "category" field in the JSON
+hash would keep getting re-written by the next iteration. Asbestos::Builder is,
+unfortunately, not aware about any loops in your template code.
+
+The solution is to indicate explicitly which keys you want aggregated:
+
+ render_json_from_xml :aggregate => ['category']
+
+Now, what would previously create a "category" key will get aggregated
+under a "categories" array, instead:
+
+ { "categories": [ ... ] }
+
+Sometimes, most often with root elements, you want a key ignored.
+You can specify which occurrences to ignore:
+
+ render_json_from_xml :ignore => ['category']
@@ -0,0 +1,3 @@
+require 'asbestos'
+ActionView::Template.register_template_handler(:asbestos, Asbestos::TemplateHandler)
+ActionController::Base.__send__(:include, Asbestos::ControllerMethods)
@@ -0,0 +1,131 @@
+module Asbestos
+ class TemplateHandler < ActionView::TemplateHandler
+ include ActionView::TemplateHandlers::Compilable
+
+ def compile(template, options = {})
+ "_set_controller_content_type(Mime::JSON);" +
+ "xml = ::Asbestos::Builder.new(#{options.inspect});" +
+ # "self.output_buffer = xml.target!;" +
+ template.source +
+ ";xml.target!.to_json;"
+ end
+ end
+
+ module ControllerMethods
+ private
+ def render_json_from_xml(options)
+ template = @template.view_paths.find_template("#{controller_name}/#{action_name}", :xml)
+ compiled_name = (template.method_name_without_locals + '_asbestos').to_sym
+
+ if !ActionView::Base::CompiledTemplates.method_defined?(compiled_name)
+ compiled_source = ::Asbestos::TemplateHandler.new.compile(template, options)
+
+ ActionView::Base::CompiledTemplates.module_eval(<<-SRC, template.filename, 0)
+ def #{compiled_name}
+ old_output_buffer = output_buffer;#{compiled_source}
+ ensure
+ self.output_buffer = old_output_buffer
+ end
+ SRC
+ end
+
+ render :text => @template.with_template(template) {
+ @template.send(:_evaluate_assigns_and_ivars)
+ @template.send(compiled_name)
+ }
+ end
+ end
+
+ class Builder
+ def initialize(options = {})
+ @target = _new_hash
+ @options = options
+ end
+
+ def target!
+ @target
+ end
+
+ def instruct!
+ end
+
+ def tag!(sym, *args, &block)
+ method_missing(sym.to_sym, *args, &block)
+ end
+
+ protected
+
+ def method_missing(method, *args)
+ method = method.to_s
+
+ if method.ends_with?('!')
+ super
+ else
+ value, attrs = _extract_value_and_attributes(args)
+
+ if block_given?
+ raise ArgumentError, "can't have mix values with a block" if value
+ old_target = @target
+ begin
+ if _aggregates.include?(method)
+ collection = old_target[method.pluralize] ||= []
+ collection << (@target = _new_hash)
+ elsif !_ignores.include?(method)
+ @target = old_target[method] = _new_hash
+ end
+ attrs.each { |name, value| _write_pair(name, value) } if attrs
+ yield
+ ensure
+ @target = old_target
+ end
+ else
+ raise ArgumentError, "don't know what to do with attributes" if attrs
+ _write_pair(method, value)
+ end
+ end
+ end
+
+ private
+
+ def _ignores
+ @options[:ignore] ||= []
+ end
+
+ def _aggregates
+ @options[:aggregate] ||= []
+ end
+
+ def _write_pair(key, value)
+ @target[key.to_s.gsub('-', '_')] = value
+ end
+
+ def _new_hash
+ ActiveSupport::OrderedHash.new
+ end
+
+ def _extract_value_and_attributes(args)
+ if args.first.kind_of?(Symbol)
+ raise ArgumentError, "don't know how to do XML namespaces in JSON"
+ end
+
+ value = nil
+ attrs = nil
+
+ args.each do |arg|
+ case arg
+ when Hash
+ attrs ||= {}
+ attrs.update(arg)
+ else
+ if value
+ value = value.to_s << arg.to_s
+ else
+ value = arg
+ end
+ end
+ end
+
+ [value, attrs]
+ end
+ end
+end
@@ -0,0 +1,75 @@
+require 'action_view'
+require 'asbestos'
+
+describe Asbestos::Builder do
+ before do
+ @json = described_class.new
+ end
+
+ def to_json
+ @json.target!.to_json
+ end
+
+ it "should be empty hash" do
+ to_json.should == '{}'
+ end
+
+ it "should add a key-value pair" do
+ @json.foo('bar')
+ to_json.should == '{"foo": "bar"}'
+ end
+
+ it "should add a key-value pair with `tag!`" do
+ @json.tag!(:foo, 'bar')
+ to_json.should == '{"foo": "bar"}'
+ end
+
+ it "should not cast numbers to strings" do
+ @json.num(2)
+ to_json.should == '{"num": 2}'
+ end
+
+ it "should case values to strings if there are more than one" do
+ @json.num(2, 3)
+ to_json.should == '{"num": "23"}'
+ end
+
+ it "should do nested hashes with block form" do
+ @json.foo do
+ @json.bar('baz')
+ @json.qoo('qux')
+ end
+ to_json.should == '{"foo": {"bar": "baz", "qoo": "qux"}}'
+ end
+
+ it "should do nested hashes with block form and attributes" do
+ @json.foo(:qoo => 'qux') do
+ @json.bar('baz')
+ end
+ to_json.should == '{"foo": {"qoo": "qux", "bar": "baz"}}'
+ end
+
+ it "should ignore instruct" do
+ @json.instruct!
+ to_json.should == '{}'
+ end
+
+ it "should support ignores" do
+ @json = described_class.new(:ignore => ['foo'])
+ @json.foo do
+ @json.bar('baz')
+ end
+ to_json.should == '{"bar": "baz"}'
+ end
+
+ it "should support aggregates" do
+ @json = described_class.new(:aggregate => ['foo'])
+ @json.foo do
+ @json.bar('baz')
+ end
+ @json.foo do
+ @json.bar('qux')
+ end
+ to_json.should == '{"foos": [{"bar": "baz"}, {"bar": "qux"}]}'
+ end
+end

0 comments on commit b7a0372

Please sign in to comment.