hypertemplate 1.2.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.
Files changed (46) hide show
  1. data/LICENSE +29 -0
  2. data/README.md +134 -0
  3. data/lib/hypertemplate.rb +14 -0
  4. data/lib/hypertemplate/builder.rb +33 -0
  5. data/lib/hypertemplate/builder/base.rb +111 -0
  6. data/lib/hypertemplate/builder/json.rb +132 -0
  7. data/lib/hypertemplate/builder/values.rb +33 -0
  8. data/lib/hypertemplate/builder/xml.rb +119 -0
  9. data/lib/hypertemplate/errors.rb +3 -0
  10. data/lib/hypertemplate/hook.rb +6 -0
  11. data/lib/hypertemplate/hook/rails.rb +112 -0
  12. data/lib/hypertemplate/hook/sinatra.rb +44 -0
  13. data/lib/hypertemplate/hook/tilt.rb +68 -0
  14. data/lib/hypertemplate/recipes.rb +25 -0
  15. data/lib/hypertemplate/registry.rb +24 -0
  16. data/lib/hypertemplate/version.rb +13 -0
  17. data/script/console +7 -0
  18. data/test/hypertemplate/builder/base_test.rb +45 -0
  19. data/test/hypertemplate/builder/json_test.rb +506 -0
  20. data/test/hypertemplate/builder/values_test.rb +12 -0
  21. data/test/hypertemplate/builder/xml_test.rb +479 -0
  22. data/test/hypertemplate/helper_test.rb +116 -0
  23. data/test/hypertemplate/hook/rails_test.rb +77 -0
  24. data/test/hypertemplate/hook/sinatra_test.rb +89 -0
  25. data/test/hypertemplate/hook/tilt_test.rb +48 -0
  26. data/test/hypertemplate/recipes_test.rb +94 -0
  27. data/test/rails2_skel/Rakefile +16 -0
  28. data/test/rails2_skel/app/controllers/application_controller.rb +1 -0
  29. data/test/rails2_skel/app/controllers/test_controller.rb +18 -0
  30. data/test/rails2_skel/app/views/test/_feed_member.tokamak +9 -0
  31. data/test/rails2_skel/app/views/test/feed.tokamak +24 -0
  32. data/test/rails2_skel/app/views/test/show.hyper +31 -0
  33. data/test/rails2_skel/config/boot.rb +110 -0
  34. data/test/rails2_skel/config/environment.rb +20 -0
  35. data/test/rails2_skel/config/environments/development.rb +17 -0
  36. data/test/rails2_skel/config/environments/production.rb +28 -0
  37. data/test/rails2_skel/config/environments/test.rb +28 -0
  38. data/test/rails2_skel/config/initializers/cookie_verification_secret.rb +2 -0
  39. data/test/rails2_skel/config/initializers/mime_types.rb +3 -0
  40. data/test/rails2_skel/config/initializers/new_rails_defaults.rb +10 -0
  41. data/test/rails2_skel/config/initializers/session_store.rb +5 -0
  42. data/test/rails2_skel/config/routes.rb +43 -0
  43. data/test/rails2_skel/log/development.log +10190 -0
  44. data/test/rails2_skel/script/console +3 -0
  45. data/test/test_helper.rb +9 -0
  46. metadata +207 -0
data/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ Copyright (c) 2010 Caelum
2
+ All rights reserved.
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ Copyright applicable to Tokamak's original code:
16
+ Copyright (c) 2010 Abril Midia
17
+ All rights reserved.
18
+
19
+ Licensed under the Apache License, Version 2.0 (the "License");
20
+ you may not use this file except in compliance with the License.
21
+ You may obtain a copy of the License at
22
+
23
+ http://www.apache.org/licenses/LICENSE-2.0
24
+
25
+ Unless required by applicable law or agreed to in writing, software
26
+ distributed under the License is distributed on an "AS IS" BASIS,
27
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
28
+ See the License for the specific language governing permissions and
29
+ limitations under the License.
@@ -0,0 +1,134 @@
1
+ # Hypertemplate
2
+
3
+ *Hypertemplate* is a template engine for hypermedia resources.
4
+ It provides a single DSL to generate different representations.
5
+ This version supports json and xml generation (you can add other media types
6
+ easily).
7
+
8
+ ## Configuring
9
+
10
+ If you are using Restfulie, there is no need to configure it, simply check the DSL.
11
+
12
+ The lib provide hooks for:
13
+
14
+ * Rails
15
+ * Sinatra
16
+ * Tilt ([https://github.com/rtomayko/tilt](https://github.com/rtomayko/tilt))
17
+
18
+ Just put `require "hypertemplate/hook/[sinatra|rails|tilt]"` on your app. See
19
+ the integration tests for hook samples.
20
+
21
+ ## Hypertemplate DSL
22
+
23
+ products {
24
+ link "order", orders_url, "type" => "application/xml"
25
+ @products.each do |prod|
26
+ product {
27
+ link :self, product_url(prod)
28
+ id prod.id
29
+ name prod.name
30
+ price prod.price
31
+ }
32
+ end
33
+ }
34
+
35
+
36
+ Generates the following representations:
37
+
38
+ ### JSON
39
+
40
+ {"products":
41
+ {"link":[
42
+ {"type":"application/xml",
43
+ "rel":"order",
44
+ "href":"http://localhost:3000/orders"}],
45
+ "product":[
46
+ {"link":
47
+ [{"rel":"self",
48
+ "href":"http://localhost:3000/products/2",
49
+ "type":"application/json"}],
50
+ "id":2,
51
+ "name":"Rest Training (20h)",
52
+ "price":"800.0"},
53
+ {"link":
54
+ [{"rel":"self",
55
+ "href":"http://localhost:3000/products/3",
56
+ "type":"application/json"}],
57
+ "id":3,
58
+ "name":"Modern Software architecture and Design (20h)",
59
+ "price":"800.0"}
60
+ ]
61
+ }
62
+ }
63
+
64
+ ### XML
65
+
66
+ <?xml version="1.0"?>
67
+ <products>
68
+ <link type="application/xml" rel="order" href="http://localhost:3000/orders"/>
69
+ <product>
70
+ <link rel="self" href="http://localhost:3000/products/2" type="application/xml"/>
71
+ <id>2</id>
72
+ <name>Rest Training (20h)</name>
73
+ <price>800.0</price>
74
+ </product>
75
+ <product>
76
+ <link rel="self" href="http://localhost:3000/products/3" type="application/xml"/>
77
+ <id>3</id>
78
+ <name>Modern Software architecture and Design (20h)</name>
79
+ <price>800.0</price>
80
+ </product>
81
+ </products>
82
+
83
+
84
+
85
+ ## Other features
86
+
87
+ * You can declare recipes once and reuse it later (see `Hypertemplate::Recipes`)
88
+ * You can extend `Hypertemplate::Builder::Base` to support a custom media type.
89
+ * You can customize the DSL entrypoint helpers, used by the hooks (see `Hypertemplate::Builder::HelperTest`)
90
+
91
+ ## Want to know more?
92
+
93
+ Please check the unit tests, you can see a lot of richer samples, including tests for the hooks.
94
+
95
+ *This library was extracted from [Restfulie](https://github.com/caelum/restfulie),
96
+ then forked from [Tokamak](http://github.com/abril/tokamak). See LICENSE.txt*
97
+
98
+ ## A more complex example
99
+
100
+ order {
101
+ link "self", order_url(@order)
102
+ link "payment", order_payments_url(@order), "type" => "application/xml"
103
+ link "calendar", order_calendar_url(@order), "type" => "text/calendar"
104
+ address @order.address
105
+ price @order.price
106
+ state @order.state
107
+ payments {
108
+ @order.payments.each do |p|
109
+ payment do
110
+ value p.value
111
+ state p.state
112
+ end
113
+ end
114
+ }
115
+ items {
116
+ @order.items.each do |i|
117
+ item do
118
+ id i.product.id
119
+ name i.product.name
120
+ price i.product.price
121
+ quantity i.quantity
122
+ end
123
+ end
124
+ }
125
+ }
126
+
127
+ ## Questions? Help?
128
+
129
+ * Join the mailing list restfulie@googlegroups.com
130
+ * Join #restfulie at freenode IRC
131
+
132
+ ## Compatibility
133
+
134
+ Hypertemplate can be used within Restfulie and its DSL is backward compatible with Tokamak 1.1.0 ~ 1.1.5.
@@ -0,0 +1,14 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__))
2
+
3
+ # Dependencies
4
+ require "rubygems"
5
+ require "bundler/setup"
6
+
7
+ module Hypertemplate
8
+ end
9
+
10
+ require "hypertemplate/errors"
11
+ require "hypertemplate/recipes"
12
+ require "hypertemplate/builder"
13
+ require "hypertemplate/registry"
14
+ require "hypertemplate/hook"
@@ -0,0 +1,33 @@
1
+ module Hypertemplate
2
+ module Builder
3
+ require "hypertemplate/builder/base"
4
+ require "hypertemplate/builder/values"
5
+ require "hypertemplate/builder/json"
6
+ require "hypertemplate/builder/xml"
7
+
8
+ def self.helper_module_for(const)
9
+ mod = Module.new
10
+ mod.module_eval <<-EOS
11
+ def collection(obj, *args, &block)
12
+ #{const.name}.build(obj, *args, &block)
13
+ end
14
+
15
+ alias_method :member, :collection
16
+
17
+ def method_missing(sym, *args, &block)
18
+
19
+ # you thought meta-(meta-programming) was nasty?
20
+ # this is nasty... help me! i am an ActionView
21
+ # so coupled to the entire Rails stack that I need
22
+ # this hack.
23
+ super if @first_invocation
24
+ @first_invocation = true
25
+
26
+ #{const.name}.build_dsl(self, :root => sym.to_s, &block)
27
+ end
28
+
29
+ EOS
30
+ mod
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,111 @@
1
+ module Hypertemplate
2
+ module Builder
3
+ class Base
4
+
5
+ # field id is quite common
6
+ undef_method :id if respond_to?(:id)
7
+
8
+ class << self
9
+
10
+ def build_dsl(obj, options = {}, &block)
11
+ recipe = block_given? ? block : options.delete(:recipe)
12
+ raise Hypertemplate::BuilderError.new("Recipe required to build representation.") unless recipe.respond_to?(:call)
13
+
14
+ builder = self.new(nil, options)
15
+ copy_internal_variables(builder, obj)
16
+ builder.instance_variable_set :@view, obj
17
+ def builder.method_missing(name, *args, &block)
18
+ begin
19
+ @view.send name, *args, &block
20
+ rescue
21
+ super
22
+ end
23
+ end
24
+ builder.instance_exec(obj, &recipe)
25
+
26
+ builder.representation
27
+ end
28
+
29
+ def copy_internal_variables(builder, obj)
30
+ # TODO this is nasty. i am sorry.
31
+ obj.instance_variables.each do |name|
32
+ builder.instance_variable_set name, obj.instance_variable_get(name)
33
+ end
34
+ end
35
+
36
+ def build(obj, options = {}, &block)
37
+ recipe = block_given? ? block : options.delete(:recipe)
38
+ raise Hypertemplate::BuilderError.new("Recipe required to build representation.") unless recipe.respond_to?(:call)
39
+
40
+ builder = self.new(obj, options)
41
+
42
+ if recipe.arity==-1
43
+ builder.instance_exec(&recipe)
44
+ else
45
+ recipe.call(*[builder, obj, options][0, recipe.arity])
46
+ end
47
+
48
+ builder.representation
49
+ end
50
+
51
+ def helper
52
+ @helper_module ||= Hypertemplate::Builder.helper_module_for(self)
53
+ end
54
+
55
+ def collection_helper_default_options(options = {}, &block)
56
+ generic_helper(:collection, options, &block)
57
+ end
58
+
59
+ def member_helper_default_options(type, options = {}, &block)
60
+ generic_helper(:member, options, &block)
61
+ end
62
+
63
+ def generic_helper(section, options = {}, &block)
64
+ helper.send(:remove_method, section)
65
+ var_name = "@@more_options_#{section.to_s}".to_sym
66
+ helper.send(:class_variable_set, var_name, options)
67
+ helper.module_eval <<-EOS
68
+ def #{section.to_s}(obj, *args, &block)
69
+ #{var_name}.merge!(args.shift)
70
+ args.unshift(#{var_name})
71
+ #{self.name}.build(obj, *args, &block)
72
+ end
73
+ EOS
74
+ end
75
+ end
76
+
77
+ # adds a key and value pair to the representation
78
+ # example:
79
+ #
80
+ # name 'guilherme'
81
+ def method_missing(sym, *args, &block)
82
+ values do |v|
83
+ v.send sym, *args, &block
84
+ end
85
+ end
86
+
87
+ def ns(*args, &block)
88
+ values do |v|
89
+ v.send(:[], *args, &block)
90
+ end
91
+ end
92
+
93
+ # writes a key and value pair to the representation
94
+ # example:
95
+ #
96
+ # write :name, "guilherme"
97
+ def write(sym, val)
98
+ values do |v|
99
+ v.send sym, val
100
+ end
101
+ end
102
+
103
+
104
+ def each(collection, options = {}, &block)
105
+ options[:collection] = collection
106
+ members(options, &block)
107
+ end
108
+
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,132 @@
1
+ module Hypertemplate
2
+ module Builder
3
+ class Json < Hypertemplate::Builder::Base
4
+
5
+ def self.media_types
6
+ ["application/json"]
7
+ end
8
+
9
+ attr_reader :raw
10
+
11
+ def initialize(obj, options = {})
12
+ initialize_library
13
+ if options[:root]
14
+ @raw = { options[:root] => {} }
15
+ @current = @raw[options[:root]]
16
+ else
17
+ @current = @raw = {}
18
+ end
19
+ @obj = obj
20
+ end
21
+
22
+ def initialize_library
23
+ return if defined?(::JSON)
24
+ require "json/pure"
25
+ end
26
+
27
+ def members(options = {}, &block)
28
+ collection = options[:collection] || @obj
29
+ raise Hypertemplate::BuilderError.new("Members method require a collection to execute") unless collection.respond_to?(:each)
30
+ root = options[:root] || "members"
31
+
32
+ add_to(@current, root, [])
33
+ collection.each do |member|
34
+
35
+ parent = @current
36
+ @current = {}
37
+ if block.arity==1
38
+ # new dsl
39
+ block.call(member)
40
+ else
41
+ # old dsl (deprecate at 1.3?)
42
+ block.call(self, member)
43
+ end
44
+ add_to(parent, root, @current)
45
+ @current = parent
46
+
47
+ end
48
+ end
49
+
50
+ def values(options = {}, &block)
51
+ yield Values.new(self)
52
+ end
53
+
54
+ def link(relationship, uri, options = {})
55
+ # Start link array
56
+ @current["link"] ||= []
57
+ stringify_keys(options)
58
+
59
+ options["rel"] = relationship.to_s
60
+ options["href"] = uri
61
+ options["type"] ||= "application/json"
62
+ insert_value("link", nil, options)
63
+ end
64
+
65
+ def insert_value(name, prefix, *args, &block)
66
+ node = create_element(block_given?, *args)
67
+
68
+ if block_given?
69
+ parent = @current
70
+ @current = node
71
+ block.call
72
+ @current = parent
73
+ end
74
+
75
+ # TODO change to name.to_sym in 1.3?
76
+ add_to(@current, name, node)
77
+ end
78
+
79
+ def representation
80
+ @raw.to_json
81
+ end
82
+
83
+ private
84
+
85
+ def create_element(has_block, *args)
86
+ vals = []
87
+ hashes = []
88
+
89
+ args.each do |arg|
90
+ arg.kind_of?(Hash) ? hashes << arg : vals << arg
91
+ end
92
+
93
+ if hashes.empty?
94
+ if has_block
95
+ {}
96
+ elsif vals.size<=1
97
+ vals.first
98
+ else
99
+ vals
100
+ end
101
+ else
102
+ # yes we have hashes
103
+ node = {}
104
+ hashes.each { |hash| node.merge!(hash) }
105
+ unless vals.empty?
106
+ vals = vals.first if vals.size == 1
107
+ node = has_block ? {} : [node, vals]
108
+ end
109
+ node
110
+ end
111
+ end
112
+
113
+ def add_to(node, name, value)
114
+ if node[name]
115
+ if node[name].kind_of?(Array)
116
+ node[name] << value
117
+ else
118
+ node[name] = [node[name], value]
119
+ end
120
+ else
121
+ node[name] = value
122
+ end
123
+ end
124
+
125
+ def stringify_keys(hash)
126
+ hash.keys.each do |key|
127
+ hash[key.to_s] = hash.delete(key)
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end