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.
- 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
|