/
resource_template.rb
348 lines (296 loc) · 12 KB
/
resource_template.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
require "json"
require "addressable/template"
class ResourceTemplate
# The template's name. Optional. Making these unique across the application is helpful for clients
# that may wish to pick out nested templates by name.
attr_reader :name
# Optional attribute that describes a resource's relationship to its parent. For example:
# * a nested route to a resource's edit page would have rel of "edit"
# * a nested collection of articles under a "user" resource would have have a rel of "articles"
# Collection members generally don't need a rel as they are identified by their params
attr_reader :rel
# A template for generating paths relative to the application's base.
attr_reader :path_template
# The parameters required by the path template
attr_reader :params
# Optional paramaters that may be used by the path template
attr_reader :optional_params
# "options" in the sense of the HTTP option request - i.e. a list of HTTP methods. Optional.
attr_reader :options
# Nested resource templates, a Resources object
attr_reader :resource_templates
# Inverse of resource_templates
attr_reader :parent
#
# Initialize a ResourceTemplate from a hash. For example:
#
# user_articles = ResourceTemplate.new(
# "name" => "user_articles",
# "rel" => "articles",
# "uri_template" => "http://example.com/users/{user_id}/articles{-prefix|.|format}",
# "path_template" => "/users/{user_id}/articles{-prefix|.|format}",
# "params" => ["user_id"],
# "optional_params" => ["format"],
# "resource_templates" => [user_article, new_user_article])
#
# <code>resource_templates</code> (if provided) can be a ResourceTemplates object, an array of ResourceTemplate objects
# or an array of hashes. This last option makes it easy to initialize a whole hierarchy directly from deserialised JSON or YAML
# objects, e.g.:
#
# user_articles = ResourceTemplate.new(JSON.parse(json))
# user_articles = ResourceTemplate.new(YAML.load(yaml))
#
def initialize(hash={}, parent=nil)
@name, @rel, @uri_template, @path_template = %w(name rel uri_template path_template).map{|attr| hash[attr]}
@params, @optional_params, @options = %w(params optional_params options).map{|attr| hash[attr] || []}
@resource_templates = ResourceTemplates.new(hash["resource_templates"], self)
@parent = parent
end
# Convert to a hash (equivalent to its JSON or YAML representation)
def to_hash
hash = {}
hash["name"] = name if name && !name.empty?
hash["rel"] = rel if rel && !rel.empty?
hash["uri_template"] = uri_template if uri_template && !uri_template.empty?
hash["path_template"] = path_template if path_template && !path_template.empty?
hash["params"] = params if params && !params.empty?
hash["optional_params"] = optional_params if optional_params && !optional_params.empty?
hash["options"] = options if options && !options.empty?
hash["resource_templates"] = resource_templates.to_parsed if !resource_templates.empty?
hash
end
# Convert to JSON
def to_json
to_hash.to_json
end
# Convert to YAML
def to_yaml
to_hash.to_yaml
end
# Text report
def to_text
ResourceTemplates.new([self]).to_text
end
#
# Produces the XML format, given an XML builder object and an array of ResourceTemplate objects
#
def to_xml(xm)
xm.ResourceTemplate do |xm|
value_tag(xm, "rel")
value_tag(xm, "name")
value_tag(xm, "path_template")
value_tag(xm, "uri_template")
list_tag(xm, params, "Params", "param")
list_tag(xm, optional_params, "OptionalParams", "param")
# could use a list of elements here, but let's follow HTTP's lead and reduce the verbosity
xm.options(options.join(", ")) unless options.empty?
resource_templates.to_xml(xm) unless resource_templates.empty?
end
xm
end
def value_tag(xm, tag) #:nodoc:
value = self.send(tag.to_sym)
xm.tag!(tag, value) unless value.blank?
end
def list_tag(xm, collection, collection_tag, member_tag) #:nodoc:
unless collection.nil? or collection.empty?
xm.tag!(collection_tag) do |xm|
collection.each do |value|
xm.tag!(member_tag, value)
end
end
end
end
# Returns params and any optional_params in order, removing the parent's params
def positional_params(parent)
all_params = params + optional_params
if parent
all_params - parent.params
else
all_params
end
end
# Returns this template's URI template, or one constructed from the given base and path template.
def uri_template(base=nil)
if @uri_template
@uri_template
elsif base && path_template
base + path_template
end
end
# Returns an expanded URI template with template variables filled from the given params hash.
# Raises ArgumentError if params doesn't contain all mandatory params.
def uri_for(params_hash, base=nil)
missing_params = params - params_hash.keys
unless missing_params.empty?
raise ArgumentError.new("missing params #{missing_params.join(', ')}")
end
t = uri_template(base)
unless t
raise RuntimeError.new("uri_template(#{base.inspect})=nil; path_template=#{path_template.inspect}")
end
Addressable::Template.new(t).expand(params_hash).to_s
end
# Returns a URI (assuming the template needs to parameters!)
def uri
uri_for({}, nil)
end
# Returns an expanded path template with template variables filled from the given params hash.
# Raises ArgumentError if params doesn't contain all mandatory params, and a RuntimeError if there is no path_template.
def path_for(params_hash)
missing_params = params - params_hash.keys
raise ArgumentError.new("missing params #{missing_params.join(', ')}") unless missing_params.empty?
raise RuntimeError.new("#path_for called on resource template #{name.inspect} that has no path_template") if path_template.nil?
Addressable::Template.new(path_template).expand(params_hash).to_s
end
# Returns a path (assuming the template needs to parameters!)
def path
path_for({})
end
# Return a new resource template with the path_template or uri_template partially expanded with the given params; does the same
# recursively descending through child resource templates.
def partial_expand(actual_params)
self.class.new(
"name" => name,
"rel" => rel,
"uri_template" => partial_expand_uri_template(uri_template, actual_params),
"path_template" => partial_expand_uri_template(path_template, actual_params),
"params" => params - actual_params.keys,
"optional_params" => optional_params - actual_params.keys,
"options" => options,
"resource_templates" => resource_templates.partial_expand(actual_params))
end
# Partially expand a URI template
def partial_expand_uri_template(template, params)#:nodoc:
template && Addressable::Template.new(template).partial_expand(params).pattern
end
# Find member ResourceTemplate objects matching the given rel
def find_by_rel(matching_rel)
resource_templates.select{|resource_template| matching_rel === resource_template.rel}
end
# self and descendants
def all_preorder
[self] + resource_templates.all_preorder
end
# defendants and self
def all_postorder
resource_templates.all_postorder + [self]
end
class ResourceTemplates < Array
# Initialize Resources (i.e. a new collection of ResourceTemplate objects) from given collection of ResourceTemplates or hashes
def initialize(collection=[], parent=nil)
if collection
raise ArgumentError.new("#{collection.inspect} is not a collection") unless collection.kind_of?(Enumerable)
collection.each do |r|
if r.kind_of?(ResourceTemplate)
push(r)
elsif r.kind_of?(Hash)
push(ResourceTemplate.new(r, parent))
else
raise ArgumentError.new("#{r.inspect} is neither a ResourceTemplate nor a Hash")
end
end
end
end
# Create Resources from an XML string
def self.parse_xml
raise NotImplementedError.new
end
# Convert member ResourceTemplate objects to array of hashes equivalent to their JSON or YAML representations
def to_parsed
map {|resource_template| resource_template.to_hash}
end
# Convert an array of ResourceTemplate objects to JSON
def to_json
to_parsed.to_json
end
# Convert an array of ResourceTemplate objects to YAML
def to_yaml
to_parsed.to_yaml
end
#
# Produces the XML format, given an XML builder object and an array of ResourceTemplate objects
#
def to_xml(xm)
xm.ResourceTemplates do |xm|
each do |resource_template|
resource_template.to_xml(xm)
end
end
xm
end
# Get a hash of all named ResourceTemplate objects contained in the supplied collection, keyed by name
def all_by_name(h = {})
inject(h) do |hash, resource_template|
hash[resource_template.name] = resource_template if resource_template.name
resource_template.resource_templates.all_by_name(hash)
hash
end
h
end
# All members and their descendants, descendants last
def all_preorder
inject([]) do |a, resource_template|
a += resource_template.all_preorder
end
end
# All members and their descendants, descendants first
def all_postorder
inject([]) do |a, resource_template|
a += resource_template.all_postorder
end
end
# for #to_text
def to_table(parent_template = nil, t = [], indent = '')
inject(t) do |table, resource_template|
if parent_template
link = (resource_template.rel || '')
new_params = resource_template.params - parent_template.params
else
link = resource_template.name
new_params = resource_template.params
end
link += new_params.map{|p| "{#{p}}"}.join(', ')
table << [
indent + link,
resource_template.name || '',
resource_template.options.join(', '),
resource_template.uri_template || resource_template.path_template
]
resource_template.resource_templates.to_table(resource_template, t, indent + ' ')
end
end
# text report
def to_text
table = self.to_table
0.upto(2) do |i|
width = table.map{|row| row[i].length}.max
table.each do |row|
row[i] = row[i].ljust(width)
end
end
table.map{|row| row.join(' ')}.join("\n") + "\n"
end
# Partially expand the path_template or uri_template of the given resource templates with the given params,
# returning new resource templates
def partial_expand(actual_params)
ResourceTemplates.new(map{|resource_template| resource_template.partial_expand(actual_params)})
end
# Return a new resource template with the path_template or uri_template expanded; expands also any child resource templates
# that don't require additional parameters.
def expand_links(actual_params)
ResourceTemplates.new(
select{|rt| (rt.positional_params(rt.parent) - rt.optional_params).empty?}.map {|rt|
{
"name" => rt.name,
"rel" => rt.rel,
"uri_template" => rt.partial_expand_uri_template(rt.uri_template, actual_params),
"path_template" => rt.partial_expand_uri_template(rt.path_template, actual_params),
"params" => rt.params - actual_params.keys,
"optional_params" => rt.optional_params - actual_params.keys,
"options" => rt.options
}
})
end
end
end