doze 0.0.11
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/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
|