doze 0.0.11
Sign up to get free protection for your applications and to get access to all the features.
- data/README +6 -0
- data/lib/doze/application.rb +92 -0
- data/lib/doze/collection/object.rb +14 -0
- data/lib/doze/entity.rb +62 -0
- data/lib/doze/error.rb +75 -0
- data/lib/doze/media_type.rb +135 -0
- data/lib/doze/negotiator.rb +107 -0
- data/lib/doze/request.rb +119 -0
- data/lib/doze/resource/error.rb +21 -0
- data/lib/doze/resource/proxy.rb +81 -0
- data/lib/doze/resource.rb +193 -0
- data/lib/doze/responder/error.rb +34 -0
- data/lib/doze/responder/main.rb +41 -0
- data/lib/doze/responder/resource.rb +262 -0
- data/lib/doze/responder.rb +58 -0
- data/lib/doze/response.rb +78 -0
- data/lib/doze/router/anchored_route_set.rb +68 -0
- data/lib/doze/router/route.rb +88 -0
- data/lib/doze/router/route_set.rb +34 -0
- data/lib/doze/router.rb +100 -0
- data/lib/doze/serialization/entity.rb +34 -0
- data/lib/doze/serialization/form_data_helpers.rb +40 -0
- data/lib/doze/serialization/html.rb +116 -0
- data/lib/doze/serialization/json.rb +29 -0
- data/lib/doze/serialization/multipart_form_data.rb +162 -0
- data/lib/doze/serialization/resource.rb +30 -0
- data/lib/doze/serialization/resource_proxy.rb +14 -0
- data/lib/doze/serialization/www_form_encoded.rb +42 -0
- data/lib/doze/serialization/yaml.rb +25 -0
- data/lib/doze/uri_template.rb +220 -0
- data/lib/doze/utils.rb +53 -0
- data/lib/doze/version.rb +3 -0
- data/lib/doze.rb +5 -0
- data/test/functional/auth_test.rb +69 -0
- data/test/functional/base.rb +159 -0
- data/test/functional/cache_header_test.rb +76 -0
- data/test/functional/direct_response_test.rb +16 -0
- data/test/functional/error_handling_test.rb +131 -0
- data/test/functional/get_and_conneg_test.rb +182 -0
- data/test/functional/media_type_extensions_test.rb +102 -0
- data/test/functional/media_type_test.rb +40 -0
- data/test/functional/method_support_test.rb +49 -0
- data/test/functional/non_get_method_test.rb +173 -0
- data/test/functional/precondition_test.rb +84 -0
- data/test/functional/raw_path_info_test.rb +69 -0
- data/test/functional/resource_representation_test.rb +14 -0
- data/test/functional/router_test.rb +196 -0
- data/test/functional/serialization_test.rb +142 -0
- data/test/functional/uri_template_test.rb +51 -0
- metadata +221 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
class Doze::Response
|
2
|
+
include Doze::Utils
|
3
|
+
|
4
|
+
def initialize(status=STATUS_OK, headers={}, body='')
|
5
|
+
@headers = Rack::Utils::HeaderHash.new(headers)
|
6
|
+
@status = status
|
7
|
+
@body = body
|
8
|
+
@head_only = false
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :headers
|
12
|
+
attr_accessor :body, :status, :head_only
|
13
|
+
|
14
|
+
def finish(head_only=@head_only)
|
15
|
+
@headers["Content-Length"] ||= content_length.to_s unless @status == STATUS_NO_CONTENT || @status == STATUS_NOT_MODIFIED
|
16
|
+
@headers['Date'] = Time.now.httpdate
|
17
|
+
[@status, @headers.to_hash, head_only ? [] : [@body]]
|
18
|
+
end
|
19
|
+
|
20
|
+
def content_length
|
21
|
+
@body.respond_to?(:bytesize) ? @body.bytesize : @body.size
|
22
|
+
end
|
23
|
+
|
24
|
+
def entity=(entity)
|
25
|
+
content_type = entity.media_type.output_name
|
26
|
+
content_type = "#{content_type}; charset=#{entity.encoding}" if entity.encoding
|
27
|
+
language = entity.language
|
28
|
+
etag = entity.etag
|
29
|
+
|
30
|
+
@headers['Content-Type'] = content_type
|
31
|
+
@headers['Content-Language'] = language if language
|
32
|
+
@headers['ETag'] = quote(etag) if etag
|
33
|
+
@headers.merge!(entity.extra_content_headers)
|
34
|
+
|
35
|
+
@body = entity.binary_data
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.new_from_entity(entity, status=STATUS_OK)
|
39
|
+
result = new(status)
|
40
|
+
result.entity = entity
|
41
|
+
result
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.new_empty(status=STATUS_NO_CONTENT, headers={})
|
45
|
+
new(status, headers)
|
46
|
+
end
|
47
|
+
|
48
|
+
def set_redirect(resource, request, status=STATUS_SEE_OTHER)
|
49
|
+
raise 'Resource specified as a representation must have a uri in order to redirect to it' unless resource.uri
|
50
|
+
@status = status
|
51
|
+
set_location(resource, request)
|
52
|
+
@body ||= ''
|
53
|
+
end
|
54
|
+
|
55
|
+
def set_location(resource, request)
|
56
|
+
base_uri = request.app.config[:base_uri] || request_base_uri(request)
|
57
|
+
@headers['Location'] = base_uri.merge(resource.uri).to_s
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.new_redirect(resource, request, status=STATUS_SEE_OTHER)
|
61
|
+
result = new
|
62
|
+
result.set_redirect(resource, request, status)
|
63
|
+
result
|
64
|
+
end
|
65
|
+
|
66
|
+
def add_header_values(header, *values)
|
67
|
+
values.unshift(@headers[header])
|
68
|
+
@headers[header] = values.compact.join(', ')
|
69
|
+
end
|
70
|
+
|
71
|
+
def set_cookie(key, value)
|
72
|
+
Rack::Utils.set_cookie_header!(@headers, key, value)
|
73
|
+
end
|
74
|
+
|
75
|
+
def delete_cookie(key, value={})
|
76
|
+
Rack::Utils.delete_cookie_header!(@headers, key, value)
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Doze::Router::AnchoredRouteSet
|
2
|
+
|
3
|
+
# Key interface to implement here is #routes and #router_uri_prefix
|
4
|
+
|
5
|
+
def routes
|
6
|
+
raise NotImplementedException
|
7
|
+
end
|
8
|
+
|
9
|
+
def router_uri_prefix
|
10
|
+
raise NotImplementedException
|
11
|
+
end
|
12
|
+
|
13
|
+
def router_uri_prefix=
|
14
|
+
raise NotImplementedException
|
15
|
+
end
|
16
|
+
|
17
|
+
# Some utility functions based on this interface
|
18
|
+
|
19
|
+
def route_template(name)
|
20
|
+
route = routes[name] and route.template(router_uri_prefix)
|
21
|
+
end
|
22
|
+
|
23
|
+
def expand_route_template(name, vars)
|
24
|
+
route = routes[name] and route.expand(vars, router_uri_prefix)
|
25
|
+
end
|
26
|
+
|
27
|
+
def partially_expand_route_template(name, vars)
|
28
|
+
route = routes[name] and route.partially_expand(vars, router_uri_prefix)
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_route(name, vars={}, session=nil)
|
32
|
+
route = routes[name] and route.call(self, vars, session)
|
33
|
+
end
|
34
|
+
|
35
|
+
def perform_routing_with_parent(parent_router, path, session, base_uri)
|
36
|
+
for route in routes
|
37
|
+
match, uri, trailing = route.match(path)
|
38
|
+
next unless match
|
39
|
+
base_uri_for_match = base_uri + uri
|
40
|
+
result = route.call(parent_router, match, session, base_uri_for_match) or next
|
41
|
+
return [result, base_uri_for_match, trailing]
|
42
|
+
end
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
|
46
|
+
# What this does:
|
47
|
+
# - Informs this instance of the fixed uri at which it's known to be anchored
|
48
|
+
# - Uses this information to infer fixed uris for the target_route_set of any of
|
49
|
+
# our routes which are not parameterized? and which routes_uniquely_to_target?,
|
50
|
+
# and recursively tell these AnchoredRouteSets their fixed uris.
|
51
|
+
#
|
52
|
+
# This is called on the root resource as part of application initialization,
|
53
|
+
# to ensure that statically-known information about routing paths is propagated
|
54
|
+
# as far as possible through the resource model, so that resource instances can
|
55
|
+
# know their uris without necessarily having been a part of the routing chain for
|
56
|
+
# the current request.
|
57
|
+
def propagate_static_routes(uri)
|
58
|
+
self.router_uri_prefix = uri
|
59
|
+
routes.each do |route|
|
60
|
+
next if route.parameterized? or !route.routes_uniquely_to_target?
|
61
|
+
target = route.target_route_set
|
62
|
+
if target
|
63
|
+
route_uri = route.template(router_uri_prefix).to_s
|
64
|
+
target.propagate_static_routes(route_uri)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
class Doze::Router::Route
|
2
|
+
|
3
|
+
# Route options:
|
4
|
+
# :name :: override default name, which is based on stripping non-alphanumerics from the route template
|
5
|
+
# :session_specific :: pass session as an extra last argument to the routing block
|
6
|
+
# :static :: route is not dependent on the parent router instance; parent router instance will not be
|
7
|
+
# passed as first argument to routing block
|
8
|
+
# :to (Router or Resource)
|
9
|
+
# :: Route directly to this instance. Only for use where it is reachable by the
|
10
|
+
# propagate_static_routes or where its uri is otherwise guaranteed to be the one routed to
|
11
|
+
# :to (Class) :: Pass routing arguments to the constructor of this class, instead of block.
|
12
|
+
# Assumes :static => true by default.
|
13
|
+
# :: Whenever :to => xyz routing is used, and the target is an AnchoredRouteSet (eg a Router instance or
|
14
|
+
# a class that includes Router) remembers this as the target_route_set of the route.
|
15
|
+
# :uniquely :: for use with :to, indicates that this is the only, unique route to the given target.
|
16
|
+
# where the target uri is statically inferable, allows propagate_static_routes to set the uri
|
17
|
+
# on the target_route_set.
|
18
|
+
# :uniquely_to :: short-cut for :to => foo, :uniquely => true
|
19
|
+
# &block :: block which is passed (router, matched_uri, matched_vars_from_template, session), except:
|
20
|
+
# * router is ommitted when :static
|
21
|
+
# * matched_vars_from_template is ommitted when the template has no vars (!parameterized?)
|
22
|
+
# * session is ommitted unless :session_specific set
|
23
|
+
# not required where :to is used.
|
24
|
+
def initialize(template, options={}, &block)
|
25
|
+
template = Doze::URITemplate.compile(template, options[:regexps] || {}) unless template.is_a?(Doze::URITemplate)
|
26
|
+
@template = template
|
27
|
+
@name = options[:name] || template.to_s.gsub(/[^a-z0-9]+/, '_').match(/^_?(.*?)_?$/)[1]
|
28
|
+
@session_specific = options[:session_specific]
|
29
|
+
@static = options[:static]
|
30
|
+
|
31
|
+
if options[:uniquely_to]
|
32
|
+
options[:uniquely] = true
|
33
|
+
options[:to] = options[:uniquely_to]
|
34
|
+
end
|
35
|
+
|
36
|
+
target = options[:to]
|
37
|
+
case target
|
38
|
+
when Doze::Router, Doze::Resource
|
39
|
+
@static = true
|
40
|
+
@target_instance = target
|
41
|
+
when Class
|
42
|
+
@static = true if @static.nil?
|
43
|
+
@block = block || target.method(:new)
|
44
|
+
else
|
45
|
+
@block = block or raise "You must specify :to or give a block"
|
46
|
+
end
|
47
|
+
|
48
|
+
if target.is_a?(Doze::Router::AnchoredRouteSet)
|
49
|
+
@target_route_set = target
|
50
|
+
@routes_uniquely_to_target = !options[:uniquely].nil?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
attr_reader :name, :block, :session_specific, :static, :target_route_set
|
55
|
+
|
56
|
+
def routes_uniquely_to_target?; @routes_uniquely_to_target; end
|
57
|
+
def static?; @static; end
|
58
|
+
|
59
|
+
def template(prefix=nil)
|
60
|
+
prefix ? @template.with_prefix(prefix) : @template
|
61
|
+
end
|
62
|
+
|
63
|
+
def call(router, vars=nil, session=nil, base_uri=nil)
|
64
|
+
return @target_instance if @target_instance
|
65
|
+
base_uri ||= expand(vars, router.router_uri_prefix) if router
|
66
|
+
args = [base_uri]
|
67
|
+
args << vars if vars && !vars.empty?
|
68
|
+
args << session if @session_specific
|
69
|
+
args.unshift(router) unless @static
|
70
|
+
@block.call(*args)
|
71
|
+
end
|
72
|
+
|
73
|
+
def match(path)
|
74
|
+
@template.match_with_trailing(path)
|
75
|
+
end
|
76
|
+
|
77
|
+
def expand(vars, prefix=nil)
|
78
|
+
template(prefix).expand(vars)
|
79
|
+
end
|
80
|
+
|
81
|
+
def partially_expand(vars, prefix=nil)
|
82
|
+
template(prefix).partially_expand(vars)
|
83
|
+
end
|
84
|
+
|
85
|
+
def parameterized?
|
86
|
+
!@template.variables.empty?
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class Doze::Router::RouteSet
|
2
|
+
def initialize(&block)
|
3
|
+
@routes = []
|
4
|
+
@routes_by_name = {}
|
5
|
+
instance_eval(&block) if block
|
6
|
+
end
|
7
|
+
|
8
|
+
def <<(route)
|
9
|
+
@routes << route unless @routes_by_name.has_key?(route.name)
|
10
|
+
@routes_by_name[route.name] = route
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](name)
|
15
|
+
@routes_by_name[name]
|
16
|
+
end
|
17
|
+
|
18
|
+
def route(*p, &b)
|
19
|
+
route = Doze::Router::Route.new(*p, &b)
|
20
|
+
self << route
|
21
|
+
route
|
22
|
+
end
|
23
|
+
|
24
|
+
def each(&b)
|
25
|
+
@routes.each(&b)
|
26
|
+
end
|
27
|
+
|
28
|
+
def dup(&block)
|
29
|
+
route_set = self.class.new
|
30
|
+
@routes.each {|r| route_set << r}
|
31
|
+
route_set.instance_eval(&block) if block
|
32
|
+
route_set
|
33
|
+
end
|
34
|
+
end
|
data/lib/doze/router.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
# A Doze::Router is a Doze::AnchoredRouteSet which routes with itself as the parent Router.
|
2
|
+
# Including it also extends the class with Doze::AnchoredRouteSet, and the instances delegates its routes
|
3
|
+
# to the class.
|
4
|
+
module Doze::Router
|
5
|
+
require 'doze/router/anchored_route_set'
|
6
|
+
|
7
|
+
include Doze::Router::AnchoredRouteSet
|
8
|
+
|
9
|
+
def self.included(mod)
|
10
|
+
mod.extend(ClassMethods) if mod.is_a?(Class) # if you include this in another module, its self.included should call this one
|
11
|
+
end
|
12
|
+
|
13
|
+
# The main method of the Router interface.
|
14
|
+
#
|
15
|
+
# You can override this if you like, but a flexible default implementation is provided
|
16
|
+
# which can be configured with the 'route' class method.
|
17
|
+
#
|
18
|
+
# args:
|
19
|
+
# path - the path to match.
|
20
|
+
# may be a suffix of the actual request path, if this router has been
|
21
|
+
# delegated to by a higher-level router)
|
22
|
+
# method - symbol for the http method being routed for this path
|
23
|
+
# session - session from the request - see Application.config[:session_from_rack_env]
|
24
|
+
# base_uri - the base uri at which the routing is taking place
|
25
|
+
#
|
26
|
+
# should return either:
|
27
|
+
# nil if no route matched / subresource not found, or
|
28
|
+
#
|
29
|
+
# [route_to, base_uri_for_match, trailing] where
|
30
|
+
#
|
31
|
+
# route_to - Resource or Router to route the request to
|
32
|
+
# base_uri_for_match - base uri for the resource or router which we matched
|
33
|
+
# trailing - any trailing bits of path following from base_uri_for_match to be passed onto the next router
|
34
|
+
def perform_routing(path, session, base_uri)
|
35
|
+
perform_routing_with_parent(self, path, session, base_uri)
|
36
|
+
end
|
37
|
+
|
38
|
+
# The default Router implementation can run against any RouteSet returned here.
|
39
|
+
# By default routes returns a RouteSet defined at the class level using the class-method routing helpers,
|
40
|
+
# but you can override this if you want to use some instance-specific RouteSet
|
41
|
+
def routes
|
42
|
+
@routes || self.class.routes
|
43
|
+
end
|
44
|
+
|
45
|
+
# Add an instance-specific route. This dups the default route-set
|
46
|
+
def add_route(*p, &b)
|
47
|
+
@routes ||= routes.dup
|
48
|
+
@routes.route(*p, &b)
|
49
|
+
end
|
50
|
+
|
51
|
+
# If this particular router instance has a uri prefix associated with it.
|
52
|
+
# Will use a resource's URI method where available, for the common case where the router is also a resource
|
53
|
+
def router_uri_prefix
|
54
|
+
@router_uri_prefix ||= begin
|
55
|
+
u = respond_to?(:uri) ? uri : nil
|
56
|
+
u && u.chomp('/')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Used primarily by propagate_static_routes. As a (desired) side-effect, will also set the uri using #uri=
|
61
|
+
# where this router is also a resource.
|
62
|
+
def router_uri_prefix=(uri_prefix)
|
63
|
+
if respond_to?(:uri=)
|
64
|
+
self.uri = (uri_prefix.empty? ? '/' : uri_prefix)
|
65
|
+
end
|
66
|
+
@router_uri_prefix = uri_prefix
|
67
|
+
end
|
68
|
+
|
69
|
+
# called upon by the framework
|
70
|
+
# return false to blanket deny authorization to all methods on all routed subresources
|
71
|
+
def authorize_routing(session)
|
72
|
+
true
|
73
|
+
end
|
74
|
+
|
75
|
+
module ClassMethods
|
76
|
+
include Doze::Router::AnchoredRouteSet
|
77
|
+
|
78
|
+
def router_uri_prefix
|
79
|
+
return @router_uri_prefix if defined?(@router_uri_prefix)
|
80
|
+
@router_uri_prefix = (superclass.respond_to?(:router_uri_prefix) ? superclass.router_uri_prefix : nil)
|
81
|
+
end
|
82
|
+
|
83
|
+
def router_uri_prefix=(uri)
|
84
|
+
@router_uri_prefix = uri
|
85
|
+
module_eval("def uri; self.class.router_uri_prefix; end", __FILE__, __LINE__)
|
86
|
+
module_eval("def router_uri_prefix; self.class.router_uri_prefix; end", __FILE__, __LINE__)
|
87
|
+
end
|
88
|
+
|
89
|
+
def routes
|
90
|
+
@routes ||= (superclass.respond_to?(:routes) ? superclass.routes.dup : Doze::Router::RouteSet.new)
|
91
|
+
end
|
92
|
+
|
93
|
+
def route(*p, &b)
|
94
|
+
routes.route(*p, &b)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
require 'doze/router/route'
|
100
|
+
require 'doze/router/route_set'
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'doze/entity'
|
2
|
+
|
3
|
+
module Doze::Serialization
|
4
|
+
class Entity < Doze::Entity
|
5
|
+
def initialize(media_type, options={}, &lazy_object_data)
|
6
|
+
super(media_type, options, &nil) # ruby quirk: blocks are implicitely passed to super
|
7
|
+
@object_data = options[:object_data]
|
8
|
+
@lazy_object_data = lazy_object_data || options[:lazy_object_data]
|
9
|
+
end
|
10
|
+
|
11
|
+
def object_data(try_deserialize=true)
|
12
|
+
@object_data ||= if @lazy_object_data
|
13
|
+
@lazy_object_data.call
|
14
|
+
elsif try_deserialize
|
15
|
+
data = binary_data(false)
|
16
|
+
data && deserialize(data)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def binary_data(try_serialize=true)
|
21
|
+
super() || if try_serialize
|
22
|
+
@binary_data = serialize(object_data(false))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def serialize(object_data)
|
27
|
+
raise 'serialize: not supported'
|
28
|
+
end
|
29
|
+
|
30
|
+
def deserialize(binary_data)
|
31
|
+
raise 'deserialize: not supported'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Doze::Serialization::FormDataHelpers
|
2
|
+
|
3
|
+
private
|
4
|
+
|
5
|
+
def escape(s)
|
6
|
+
s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
|
7
|
+
'%'+$1.unpack('H2'*$1.size).join('%').upcase
|
8
|
+
}.tr(' ', '+')
|
9
|
+
end
|
10
|
+
|
11
|
+
def unescape(s)
|
12
|
+
s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
|
13
|
+
[$1.delete('%')].pack('H*')
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def normalize_params(parms, name, val=nil)
|
18
|
+
name =~ %r([\[\]]*([^\[\]]+)\]*)
|
19
|
+
key = $1 || ''
|
20
|
+
after = $' || ''
|
21
|
+
|
22
|
+
if after == ""
|
23
|
+
parms[key] = val
|
24
|
+
elsif after == "[]"
|
25
|
+
(parms[key] ||= []) << val
|
26
|
+
elsif after =~ %r(^\[\]\[([^\[\]]+)\]$)
|
27
|
+
child_key = $1
|
28
|
+
parms[key] ||= []
|
29
|
+
if parms[key].last.is_a?(Hash) && !parms[key].last.key?(child_key)
|
30
|
+
parms[key].last.update(child_key => val)
|
31
|
+
else
|
32
|
+
parms[key] << { child_key => val }
|
33
|
+
end
|
34
|
+
else
|
35
|
+
parms[key] ||= {}
|
36
|
+
parms[key] = normalize_params(parms[key], after, val)
|
37
|
+
end
|
38
|
+
parms
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'doze/media_type'
|
3
|
+
require 'doze/serialization/entity'
|
4
|
+
require 'doze/utils'
|
5
|
+
|
6
|
+
module Doze::Serialization
|
7
|
+
class Entity::HTML < Entity
|
8
|
+
def serialize(data)
|
9
|
+
# TODO move to a template
|
10
|
+
html = <<END
|
11
|
+
<html>
|
12
|
+
<head>
|
13
|
+
<style>
|
14
|
+
body {
|
15
|
+
font-family: Arial;
|
16
|
+
}
|
17
|
+
body, td {
|
18
|
+
font-size: 13px;
|
19
|
+
}
|
20
|
+
body > table {
|
21
|
+
border: 1px solid black;
|
22
|
+
}
|
23
|
+
table {
|
24
|
+
border-color: black;
|
25
|
+
border-collapse: collapse;
|
26
|
+
}
|
27
|
+
td {
|
28
|
+
padding: 0;
|
29
|
+
vertical-align: top;
|
30
|
+
}
|
31
|
+
td > span, td > a, td > form {
|
32
|
+
display: block;
|
33
|
+
padding: 0.3em;
|
34
|
+
margin: 0;
|
35
|
+
}
|
36
|
+
td:first-child {
|
37
|
+
text-align: right;
|
38
|
+
font-weight: bold;
|
39
|
+
width: 1%; /* force as small as possible */
|
40
|
+
}
|
41
|
+
td > table {
|
42
|
+
width: 100%;
|
43
|
+
}
|
44
|
+
li > table {
|
45
|
+
width: 100%;
|
46
|
+
}
|
47
|
+
td > ol {
|
48
|
+
padding: 0.3em 0.3em 0.3em 2.3em;
|
49
|
+
}
|
50
|
+
td > ol > li > table {
|
51
|
+
}
|
52
|
+
</style>
|
53
|
+
</head>
|
54
|
+
<body>
|
55
|
+
#{make_html(data)}
|
56
|
+
</body>
|
57
|
+
</html>
|
58
|
+
END
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def make_html(data)
|
64
|
+
case data
|
65
|
+
when Hash
|
66
|
+
pairs = data.map {|k,v| "<tr><td>#{make_html(k)}</td><td>#{make_html(v)}</td></tr>"}
|
67
|
+
"<table rules='all' frame='void'>#{pairs.join("\n")}</table>"
|
68
|
+
when Array
|
69
|
+
i=-1; items = data.map {|v| "<tr><td><span>#{i+=1}</span></td><td>#{make_html(v)}</td></tr>"}
|
70
|
+
items.empty? ? ' ' : "<table rules='all' frame='void'>#{items.join("\n")}</table>"
|
71
|
+
when URI
|
72
|
+
"<a href='#{Rack::Utils.escape_html(data)}'>#{Rack::Utils.escape_html(data)}</a>"
|
73
|
+
when Doze::URITemplate
|
74
|
+
if data.variables.length > 0
|
75
|
+
# Clever HTML rendering of a URI template.
|
76
|
+
# Make a HTML form which uses some javascript onsubmit to navigate to an expanded version of the URI template,
|
77
|
+
# with blanks filled in via INPUTs.
|
78
|
+
inputs = data.parts.map do |part|
|
79
|
+
case part
|
80
|
+
when Doze::URITemplate::String
|
81
|
+
Rack::Utils.escape_html(part.string)
|
82
|
+
when Doze::URITemplate::QuadHexBytesVariable
|
83
|
+
"/<input name='#{Rack::Utils.escape_html(part.name)}'>"
|
84
|
+
when Doze::URITemplate::Variable
|
85
|
+
"<input name='#{Rack::Utils.escape_html(part.name)}'>"
|
86
|
+
end
|
87
|
+
end.join
|
88
|
+
|
89
|
+
i=-1; js = data.parts.map do |part|
|
90
|
+
case part
|
91
|
+
when Doze::URITemplate::String
|
92
|
+
part.string.to_json
|
93
|
+
when Doze::URITemplate::QuadHexBytesVariable
|
94
|
+
i += 1; "(v = parseInt(this.elements[#{i}].value, '10').toString(16), (new Array(9-v.length).join('0')+v).replace(/(..)/g, '/$1'))"
|
95
|
+
when Doze::URITemplate::Variable
|
96
|
+
i += 1; "this.elements[#{i}].value"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
js = "window.location.href = #{js.join(" + ")}; return false"
|
101
|
+
"<form method='GET' onsubmit='#{Rack::Utils.escape_html(js)}'>#{inputs}<input type='submit'></form>"
|
102
|
+
else
|
103
|
+
"<a href='#{Rack::Utils.escape_html(data)}'>#{Rack::Utils.escape_html(data)}</a>"
|
104
|
+
end
|
105
|
+
else
|
106
|
+
string = data.to_s.strip
|
107
|
+
string.empty? ? ' ' : "<span>#{Rack::Utils.escape_html(string)}</span>"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# A browser-friendly media type for use with Doze::Serialization::Resource.
|
113
|
+
# We don't register it, since you might not want to use it for submitted html entities,
|
114
|
+
# but you can call register! on it yourself if you do
|
115
|
+
HTML = Doze::MediaType.new('text/html', :entity_class => Entity::HTML, :extension => 'html')
|
116
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'doze/media_type'
|
3
|
+
require 'doze/serialization/entity'
|
4
|
+
require 'doze/error'
|
5
|
+
require 'doze/utils'
|
6
|
+
|
7
|
+
module Doze::Serialization
|
8
|
+
class Entity::JSON < Entity
|
9
|
+
def serialize(ruby_data)
|
10
|
+
ruby_data.to_json
|
11
|
+
end
|
12
|
+
|
13
|
+
def deserialize(binary_data)
|
14
|
+
begin
|
15
|
+
case binary_data
|
16
|
+
when /^[\[\{]/
|
17
|
+
::JSON.parse(binary_data)
|
18
|
+
else
|
19
|
+
# A pox on the arbitrary syntactic limitation that a top-level piece of JSON must be a hash or array
|
20
|
+
::JSON.parse("[#{binary_data}]").first
|
21
|
+
end
|
22
|
+
rescue ::JSON::ParserError
|
23
|
+
raise Doze::ClientEntityError, "Could not parse JSON"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
JSON = Doze::MediaType.register('application/json', :plus_suffix => 'json', :entity_class => Entity::JSON, :extension => 'json')
|
29
|
+
end
|