hypertemplate 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +29 -0
- data/README.md +134 -0
- data/lib/hypertemplate.rb +14 -0
- data/lib/hypertemplate/builder.rb +33 -0
- data/lib/hypertemplate/builder/base.rb +111 -0
- data/lib/hypertemplate/builder/json.rb +132 -0
- data/lib/hypertemplate/builder/values.rb +33 -0
- data/lib/hypertemplate/builder/xml.rb +119 -0
- data/lib/hypertemplate/errors.rb +3 -0
- data/lib/hypertemplate/hook.rb +6 -0
- data/lib/hypertemplate/hook/rails.rb +112 -0
- data/lib/hypertemplate/hook/sinatra.rb +44 -0
- data/lib/hypertemplate/hook/tilt.rb +68 -0
- data/lib/hypertemplate/recipes.rb +25 -0
- data/lib/hypertemplate/registry.rb +24 -0
- data/lib/hypertemplate/version.rb +13 -0
- data/script/console +7 -0
- data/test/hypertemplate/builder/base_test.rb +45 -0
- data/test/hypertemplate/builder/json_test.rb +506 -0
- data/test/hypertemplate/builder/values_test.rb +12 -0
- data/test/hypertemplate/builder/xml_test.rb +479 -0
- data/test/hypertemplate/helper_test.rb +116 -0
- data/test/hypertemplate/hook/rails_test.rb +77 -0
- data/test/hypertemplate/hook/sinatra_test.rb +89 -0
- data/test/hypertemplate/hook/tilt_test.rb +48 -0
- data/test/hypertemplate/recipes_test.rb +94 -0
- data/test/rails2_skel/Rakefile +16 -0
- data/test/rails2_skel/app/controllers/application_controller.rb +1 -0
- data/test/rails2_skel/app/controllers/test_controller.rb +18 -0
- data/test/rails2_skel/app/views/test/_feed_member.tokamak +9 -0
- data/test/rails2_skel/app/views/test/feed.tokamak +24 -0
- data/test/rails2_skel/app/views/test/show.hyper +31 -0
- data/test/rails2_skel/config/boot.rb +110 -0
- data/test/rails2_skel/config/environment.rb +20 -0
- data/test/rails2_skel/config/environments/development.rb +17 -0
- data/test/rails2_skel/config/environments/production.rb +28 -0
- data/test/rails2_skel/config/environments/test.rb +28 -0
- data/test/rails2_skel/config/initializers/cookie_verification_secret.rb +2 -0
- data/test/rails2_skel/config/initializers/mime_types.rb +3 -0
- data/test/rails2_skel/config/initializers/new_rails_defaults.rb +10 -0
- data/test/rails2_skel/config/initializers/session_store.rb +5 -0
- data/test/rails2_skel/config/routes.rb +43 -0
- data/test/rails2_skel/log/development.log +10190 -0
- data/test/rails2_skel/script/console +3 -0
- data/test/test_helper.rb +9 -0
- 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.
|
data/README.md
ADDED
@@ -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
|