roar 1.0.4 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +14 -6
- data/CHANGES.markdown +75 -58
- data/CONTRIBUTING.md +31 -0
- data/Gemfile +12 -2
- data/ISSUE_TEMPLATE.md +20 -0
- data/LICENSE +1 -1
- data/README.markdown +126 -250
- data/Rakefile +3 -1
- data/examples/example.rb +0 -0
- data/examples/example_server.rb +0 -0
- data/lib/roar.rb +3 -3
- data/lib/roar/client.rb +8 -3
- data/lib/roar/decorator.rb +2 -2
- data/lib/roar/http_verbs.rb +0 -16
- data/lib/roar/hypermedia.rb +30 -56
- data/lib/roar/json.rb +5 -5
- data/lib/roar/json/collection.rb +10 -2
- data/lib/roar/json/hal.rb +72 -82
- data/lib/roar/version.rb +1 -1
- data/lib/roar/xml.rb +1 -1
- data/roar.gemspec +6 -6
- data/test/client_test.rb +1 -1
- data/test/coercion_feature_test.rb +7 -2
- data/test/decorator_test.rb +17 -7
- data/test/hal_json_test.rb +98 -106
- data/test/hypermedia_feature_test.rb +13 -31
- data/test/hypermedia_test.rb +26 -92
- data/test/{decorator_client_test.rb → integration/decorator_client_test.rb} +5 -4
- data/test/{faraday_http_transport_test.rb → integration/faraday_http_transport_test.rb} +1 -0
- data/test/{http_verbs_test.rb → integration/http_verbs_test.rb} +3 -2
- data/test/integration/json_collection_test.rb +35 -0
- data/test/{net_http_transport_test.rb → integration/net_http_transport_test.rb} +1 -0
- data/test/integration/runner.rb +2 -3
- data/test/integration/server.rb +6 -0
- data/test/json_representer_test.rb +2 -29
- data/test/lonely_test.rb +1 -2
- data/test/ssl_client_certs_test.rb +1 -1
- data/test/test_helper.rb +21 -3
- data/test/xml_representer_test.rb +6 -5
- metadata +22 -36
- data/gemfiles/Gemfile.representable-1.7 +0 -6
- data/gemfiles/Gemfile.representable-1.8 +0 -6
- data/gemfiles/Gemfile.representable-2.0 +0 -5
- data/gemfiles/Gemfile.representable-2.1 +0 -5
- data/gemfiles/Gemfile.representable-head +0 -6
- data/lib/roar/json/collection_json.rb +0 -208
- data/lib/roar/json/json_api.rb +0 -233
- data/test/collection_json_test.rb +0 -132
- data/test/hal_links_test.rb +0 -31
- data/test/json_api_test.rb +0 -451
- data/test/lib/runner.rb +0 -134
data/Rakefile
CHANGED
@@ -7,6 +7,8 @@ task :default => [:test]
|
|
7
7
|
|
8
8
|
Rake::TestTask.new(:test) do |test|
|
9
9
|
test.libs << 'test'
|
10
|
-
test.test_files = FileList
|
10
|
+
test.test_files = FileList.new('test/**/*_test.rb') do |fl|
|
11
|
+
fl.exclude('test/integration/**') if RUBY_VERSION < '2.2.2'
|
12
|
+
end
|
11
13
|
test.verbose = true
|
12
14
|
end
|
data/examples/example.rb
CHANGED
File without changes
|
data/examples/example_server.rb
CHANGED
File without changes
|
data/lib/roar.rb
CHANGED
data/lib/roar/client.rb
CHANGED
@@ -2,7 +2,7 @@ require "roar/http_verbs"
|
|
2
2
|
|
3
3
|
module Roar
|
4
4
|
|
5
|
-
# Mix in HttpVerbs.
|
5
|
+
# Mix in HttpVerbs.
|
6
6
|
module Client
|
7
7
|
include HttpVerbs
|
8
8
|
|
@@ -28,12 +28,17 @@ module Roar
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def to_hash(options={})
|
31
|
-
options[:links] ||= false
|
31
|
+
# options[:links] ||= false
|
32
|
+
options[:user_options] ||= {}
|
33
|
+
options[:user_options][:links] ||= false
|
34
|
+
|
32
35
|
super(options)
|
33
36
|
end
|
34
37
|
|
35
38
|
def to_xml(options={}) # sorry, but i'm not even sure if anyone uses this module.
|
36
|
-
options[:
|
39
|
+
options[:user_options] ||= {}
|
40
|
+
options[:user_options][:links] ||= false
|
41
|
+
|
37
42
|
super(options)
|
38
43
|
end
|
39
44
|
end
|
data/lib/roar/decorator.rb
CHANGED
@@ -4,8 +4,8 @@ require 'representable/decorator'
|
|
4
4
|
class Roar::Decorator < Representable::Decorator
|
5
5
|
module HypermediaConsumer
|
6
6
|
def links=(arr)
|
7
|
-
|
8
|
-
represented.instance_variable_set :@links, links
|
7
|
+
super
|
8
|
+
represented.instance_variable_set :@links, self.links
|
9
9
|
end
|
10
10
|
end
|
11
11
|
end
|
data/lib/roar/http_verbs.rb
CHANGED
@@ -68,21 +68,5 @@ module Roar
|
|
68
68
|
def http
|
69
69
|
transport_engine.new
|
70
70
|
end
|
71
|
-
|
72
|
-
def handle_deprecated_args(body, *args) # TODO: remove in 1.0.
|
73
|
-
options = args.first
|
74
|
-
|
75
|
-
if args.size > 1
|
76
|
-
warn %{DEPRECATION WARNING: #get, #post, #put, #delete and #patch no longer accept positional arguments. Please call them as follows:
|
77
|
-
get(uri: "http://localhost/songs", as: "application/json")
|
78
|
-
post(uri: "http://localhost/songs", as: "application/json")
|
79
|
-
Thank you and have a beautiful day.}
|
80
|
-
options = {:uri => args[0], :as => args[1]} if args.size == 2
|
81
|
-
options = {:uri => args[0], :as => args[2]}
|
82
|
-
end
|
83
|
-
|
84
|
-
options[:body] = body
|
85
|
-
options
|
86
|
-
end
|
87
71
|
end
|
88
72
|
end
|
data/lib/roar/hypermedia.rb
CHANGED
@@ -33,33 +33,26 @@ module Roar
|
|
33
33
|
#
|
34
34
|
# model.to_json(:id => 1)
|
35
35
|
module Hypermedia
|
36
|
-
# links= [Hyperlink, Hyperlink] is where parsing happens.
|
37
36
|
def self.included(base)
|
38
37
|
base.extend ClassMethods
|
39
38
|
end
|
40
39
|
|
41
|
-
|
42
|
-
|
40
|
+
# public API: #links (only helpful in clients, though).
|
41
|
+
attr_writer :links # this is called in parsing when Hyperlinks are deserialized.
|
42
|
+
def links # this is _not_ called by rendering as we go via ::links_config.
|
43
|
+
tuples = (@links||[]).collect { |link| [link.rel, link] }
|
44
|
+
# tuples.to_h
|
45
|
+
::Hash[tuples] # TODO: tuples.to_h when dropping < 2.1.
|
43
46
|
end
|
44
47
|
|
45
|
-
attr_reader :links # this is only useful after parsing.
|
46
|
-
|
47
|
-
|
48
|
-
module LinkConfigsMethod
|
49
|
-
def link_configs # we could store the ::link configs in links Definition.
|
50
|
-
representable_attrs[:links] ||= Representable::Inheritable::Array.new
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
include LinkConfigsMethod
|
55
|
-
|
56
48
|
private
|
57
49
|
# Create hypermedia links for this instance by invoking their blocks.
|
58
50
|
# This is called in links: getter: {}.
|
59
51
|
def prepare_links!(options)
|
60
|
-
return [] if options[:links] == false
|
52
|
+
return [] if (options[:user_options] || {})[:links] == false
|
61
53
|
|
62
|
-
|
54
|
+
link_configs = representable_attrs["links"].link_configs
|
55
|
+
compile_links_for(link_configs, options)
|
63
56
|
end
|
64
57
|
|
65
58
|
def compile_links_for(configs, *args)
|
@@ -72,41 +65,12 @@ module Roar
|
|
72
65
|
end
|
73
66
|
|
74
67
|
def prepare_link_for(href, options)
|
75
|
-
options = options.merge(href.is_a?(::Hash) ? href : {:
|
68
|
+
options = options.merge(href.is_a?(::Hash) ? href : {href: href})
|
76
69
|
Hyperlink.new(options)
|
77
70
|
end
|
78
71
|
|
79
|
-
def run_link_block(block,
|
80
|
-
instance_exec(
|
81
|
-
end
|
82
|
-
|
83
|
-
|
84
|
-
# LinkCollection keeps an array of Hyperlinks to be rendered (setup in #prepare_links!)
|
85
|
-
# or parsed (array is passed to #links= which transforms it into a LinkCollection).
|
86
|
-
# It is implemented as a hash and keys links by their rel value.
|
87
|
-
#
|
88
|
-
# {"self" => <Hyperlink ..>, ..}
|
89
|
-
class LinkCollection < ::Hash
|
90
|
-
# The only way to create is LinkCollection[<Hyperlink>, <Hyperlink>]
|
91
|
-
def self.[](*arr)
|
92
|
-
super(arr.collect { |link| [link.rel, link] })
|
93
|
-
end
|
94
|
-
|
95
|
-
def [](rel)
|
96
|
-
super(rel.to_s)
|
97
|
-
end
|
98
|
-
|
99
|
-
# Iterating links. Block parameters: |link| or |rel, link|.
|
100
|
-
# This is used Hash::HashBinding#serialize.
|
101
|
-
def each(&block)
|
102
|
-
return values.each(&block) if block.arity == 1
|
103
|
-
super(&block)
|
104
|
-
end
|
105
|
-
|
106
|
-
def collect(&block) # TODO: remove me when we drop representable 2.0.x support!
|
107
|
-
return values.collect(&block) if block.arity == 1
|
108
|
-
super(&block)
|
109
|
-
end
|
72
|
+
def run_link_block(block, options)
|
73
|
+
instance_exec(options[:user_options], &block)
|
110
74
|
end
|
111
75
|
|
112
76
|
|
@@ -122,23 +86,34 @@ module Roar
|
|
122
86
|
# The block is executed in instance context, so you may call properties or other accessors.
|
123
87
|
# Note that you're free to put decider logic into #link blocks, too.
|
124
88
|
def link(options, &block)
|
125
|
-
|
89
|
+
heritage.record(:link, options, &block)
|
90
|
+
|
91
|
+
links_dfn = create_links_definition! # this assures the links are rendered at the right position.
|
126
92
|
|
127
93
|
options = {:rel => options} unless options.is_a?(::Hash)
|
128
|
-
link_configs << [options, block]
|
129
|
-
end
|
130
94
|
|
131
|
-
|
95
|
+
links_dfn.link_configs << [options, block]
|
96
|
+
end
|
132
97
|
|
133
98
|
private
|
134
99
|
# Add a :links Definition to the representable_attrs so they get rendered/parsed.
|
135
100
|
def create_links_definition!
|
136
|
-
|
101
|
+
dfn = definitions["links"] and return dfn # only create it once.
|
137
102
|
|
138
103
|
options = links_definition_options
|
139
|
-
options.merge!(:
|
104
|
+
options.merge!(getter: ->(opts) { prepare_links!(opts) })
|
105
|
+
|
106
|
+
dfn = build_definition(:links, options)
|
107
|
+
|
108
|
+
|
109
|
+
dfn.extend(DefinitionOptions)
|
110
|
+
dfn
|
111
|
+
end
|
112
|
+
end
|
140
113
|
|
141
|
-
|
114
|
+
module DefinitionOptions
|
115
|
+
def link_configs
|
116
|
+
@link_configs ||= []
|
142
117
|
end
|
143
118
|
end
|
144
119
|
|
@@ -172,7 +147,6 @@ module Roar
|
|
172
147
|
attrs.inject({}) { |hsh, kv| hsh[kv.first.to_s] = kv.last; hsh }.tap do |hsh|
|
173
148
|
hsh["rel"] = hsh["rel"].to_s if hsh["rel"]
|
174
149
|
end
|
175
|
-
# raise "Hyperlink without rel doesn't work!" unless @attrs["rel"]
|
176
150
|
end
|
177
151
|
end
|
178
152
|
end
|
data/lib/roar/json.rb
CHANGED
@@ -37,10 +37,10 @@ module Roar
|
|
37
37
|
def links_definition_options
|
38
38
|
# FIXME: this doesn't belong into the generic JSON representer.
|
39
39
|
{
|
40
|
-
:
|
41
|
-
:
|
42
|
-
:
|
43
|
-
:
|
40
|
+
class: Hypermedia::Hyperlink,
|
41
|
+
decorator: HyperlinkDecorator,
|
42
|
+
collection: true,
|
43
|
+
exec_context: :decorator
|
44
44
|
}
|
45
45
|
end
|
46
46
|
end
|
@@ -48,7 +48,7 @@ module Roar
|
|
48
48
|
|
49
49
|
require "representable/json/hash"
|
50
50
|
# Represents a hyperlink in standard roar+json hash representation.
|
51
|
-
|
51
|
+
class HyperlinkDecorator < Representable::Decorator
|
52
52
|
include Representable::JSON::Hash
|
53
53
|
end
|
54
54
|
end
|
data/lib/roar/json/collection.rb
CHANGED
@@ -1,3 +1,11 @@
|
|
1
|
-
require
|
1
|
+
require 'roar/json'
|
2
2
|
|
3
|
-
Roar::JSON
|
3
|
+
module Roar::JSON
|
4
|
+
module Collection
|
5
|
+
include Roar::JSON
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.send :include, Representable::Hash::Collection
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
data/lib/roar/json/hal.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
require
|
1
|
+
require "roar/json"
|
2
|
+
require "representable/json/collection"
|
3
|
+
require "representable/json/hash"
|
2
4
|
|
3
5
|
module Roar
|
4
6
|
module JSON
|
@@ -46,8 +48,8 @@ module Roar
|
|
46
48
|
base.class_eval do
|
47
49
|
include Roar::JSON
|
48
50
|
include Links # overwrites #links_definition_options.
|
49
|
-
extend ClassMethods # overwrites #links_definition_options, again.
|
50
51
|
include Resources
|
52
|
+
include LinksReader # gives us Decorator#links => {self=>< >}
|
51
53
|
end
|
52
54
|
end
|
53
55
|
|
@@ -56,7 +58,7 @@ module Roar
|
|
56
58
|
super.tap do |hash|
|
57
59
|
embedded = {}
|
58
60
|
representable_attrs.find_all do |dfn|
|
59
|
-
name = dfn[:as].(nil) # DISCUSS: should we simplify that in Representable?
|
61
|
+
name = dfn[:as] ? dfn[:as].(nil) : dfn.name # DISCUSS: should we simplify that in Representable?
|
60
62
|
next unless dfn[:embedded] and fragment = hash.delete(name)
|
61
63
|
embedded[name] = fragment
|
62
64
|
end
|
@@ -72,38 +74,7 @@ module Roar
|
|
72
74
|
end
|
73
75
|
end
|
74
76
|
|
75
|
-
module ClassMethods
|
76
|
-
def links_definition_options
|
77
|
-
super.merge(:as => :_links)
|
78
|
-
end
|
79
|
-
end
|
80
77
|
|
81
|
-
class LinkCollection < Hypermedia::LinkCollection
|
82
|
-
def initialize(array_rels, *args)
|
83
|
-
super(*args)
|
84
|
-
@array_rels = array_rels.map(&:to_s)
|
85
|
-
end
|
86
|
-
|
87
|
-
def is_array?(rel)
|
88
|
-
@array_rels.include?(rel.to_s)
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
# Including this module in your representer will render and parse your embedded hyperlinks
|
93
|
-
# following the HAL specification: http://stateless.co/hal_specification.html
|
94
|
-
#
|
95
|
-
# module SongRepresenter
|
96
|
-
# include Roar::JSON
|
97
|
-
# include Roar::JSON::HAL::Links
|
98
|
-
#
|
99
|
-
# link :self { "http://self" }
|
100
|
-
# end
|
101
|
-
#
|
102
|
-
# Renders to
|
103
|
-
#
|
104
|
-
# {"links":{"self":{"href":"http://self"}}}
|
105
|
-
#
|
106
|
-
# Note that the HAL::Links module alone doesn't prepend an underscore to +links+. Use the JSON::HAL module for that.
|
107
78
|
module Links
|
108
79
|
def self.included(base)
|
109
80
|
base.extend ClassMethods # ::links_definition_options
|
@@ -112,83 +83,84 @@ module Roar
|
|
112
83
|
end
|
113
84
|
|
114
85
|
module InstanceMethods
|
86
|
+
def _links
|
87
|
+
links
|
88
|
+
end
|
89
|
+
|
115
90
|
private
|
116
91
|
def prepare_link_for(href, options)
|
117
|
-
return super(href, options) unless options[:array] #
|
92
|
+
return super(href, options) unless options[:array] # returns Hyperlink.
|
118
93
|
|
119
|
-
|
120
|
-
LinkArray.new(list, options[:rel])
|
121
|
-
end
|
122
|
-
|
123
|
-
# TODO: move to LinksDefinition.
|
124
|
-
def link_array_rels
|
125
|
-
link_configs.collect { |cfg| cfg.first[:array] ? cfg.first[:rel] : nil }.compact
|
94
|
+
ArrayLink.new(options[:rel], href.collect { |opts| Hypermedia::Hyperlink.new(opts) })
|
126
95
|
end
|
127
96
|
end
|
128
97
|
|
129
98
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
values :extend => lambda { |item, *|
|
135
|
-
item.is_a?(Array) ? LinkArrayRepresenter : Roar::JSON::HyperlinkRepresenter },
|
136
|
-
:instance => lambda { |fragment, *| fragment.is_a?(LinkArray) ? fragment : Roar::Hypermedia::Hyperlink.new
|
137
|
-
}
|
99
|
+
class SingleLink
|
100
|
+
class Representer < Representable::Decorator
|
101
|
+
include Representable::JSON::Hash
|
138
102
|
|
139
|
-
|
140
|
-
|
141
|
-
|
103
|
+
def to_hash(*)
|
104
|
+
hash = super
|
105
|
+
{hash.delete("rel").to_s => hash}
|
142
106
|
end
|
143
107
|
end
|
144
|
-
|
145
|
-
|
146
|
-
def from_hash(hash, *args)
|
147
|
-
hash.each { |k,v| hash[k] = LinkArray.new(v, k) if is_array?(k) }
|
148
|
-
|
149
|
-
hsh = super(hash) # this is where :class and :extend do the work.
|
150
|
-
|
151
|
-
hsh.each { |k, v| v.merge!(:rel => k) }
|
152
|
-
hsh.values # links= expects [Hyperlink, Hyperlink]
|
153
|
-
end
|
154
108
|
end
|
155
109
|
|
156
|
-
|
157
|
-
|
158
|
-
def initialize(elems, rel)
|
159
|
-
super(elems)
|
110
|
+
class ArrayLink < Array
|
111
|
+
def initialize(rel, links)
|
160
112
|
@rel = rel
|
113
|
+
super(links)
|
161
114
|
end
|
162
|
-
|
163
115
|
attr_reader :rel
|
164
116
|
|
165
|
-
|
166
|
-
|
117
|
+
|
118
|
+
# [Hyperlink, Hyperlink]
|
119
|
+
class Representer < Representable::Decorator
|
120
|
+
include Representable::JSON::Collection
|
121
|
+
|
122
|
+
items extend: SingleLink::Representer,
|
123
|
+
class: Roar::Hypermedia::Hyperlink
|
124
|
+
|
125
|
+
def to_hash(*)
|
126
|
+
links = super.flat_map(&:values) # [{"self"=>{"href": ..}}, ..]
|
127
|
+
|
128
|
+
{ represented.rel.to_s => links } # {"self"=>[{"lang"=>"en", "href"=>"http://en.hit"}, {"lang"=>"de", "href"=>"http://de.hit"}]}
|
129
|
+
end
|
167
130
|
end
|
168
131
|
end
|
169
132
|
|
170
|
-
|
171
|
-
|
133
|
+
|
134
|
+
# Represents all links for "_links": [Hyperlink, [Hyperlink, Hyperlink]]
|
135
|
+
class Representer < Representable::Decorator # links could be a simple collection property.
|
172
136
|
include Representable::JSON::Collection
|
173
137
|
|
174
|
-
|
175
|
-
|
138
|
+
# render: decorates represented.links with ArrayLink::R or SingleLink::R and calls #to_hash.
|
139
|
+
# parse: instantiate either Array or Hypermedia instance, decorate respectively, call #from_hash.
|
140
|
+
items decorator: ->(options) { options[:input].is_a?(Array) ? ArrayLink::Representer : SingleLink::Representer },
|
141
|
+
class: ->(options) { options[:input].is_a?(Array) ? Array : Hypermedia::Hyperlink }
|
142
|
+
|
143
|
+
def to_hash(options)
|
144
|
+
super.inject({}) { |links, hash| links.merge!(hash) } # [{ rel=>{}, rel=>[{}, {}] }]
|
145
|
+
end
|
176
146
|
|
177
|
-
def
|
178
|
-
|
179
|
-
|
147
|
+
def from_hash(hash, *args)
|
148
|
+
collection = hash.collect do |rel, value| # "self" => [{"href": "//"}, ] or "self" => {"href": "//"}
|
149
|
+
value.is_a?(Array) ? value.collect { |link| link.merge("rel"=>rel) } : value.merge("rel"=>rel)
|
180
150
|
end
|
151
|
+
|
152
|
+
super(collection) # [{rel=>self, href=>//}, ..] or {rel=>self, href=>//}
|
181
153
|
end
|
182
154
|
end
|
183
155
|
|
184
156
|
|
185
157
|
module ClassMethods
|
186
158
|
def links_definition_options
|
187
|
-
# property :links_array,
|
188
159
|
{
|
189
|
-
:
|
190
|
-
:
|
191
|
-
:
|
160
|
+
# collection: false,
|
161
|
+
:as => :_links,
|
162
|
+
decorator: Links::Representer,
|
163
|
+
instance: ->(*) { Array.new }, # defined in InstanceMethods as this is executed in represented context.
|
192
164
|
:exec_context => :decorator,
|
193
165
|
}
|
194
166
|
end
|
@@ -200,7 +172,7 @@ module Roar
|
|
200
172
|
# {:lang => "de", :href => "http://de.hit"}]
|
201
173
|
# end
|
202
174
|
def links(options, &block)
|
203
|
-
options = {:rel => options} if options.is_a?(Symbol)
|
175
|
+
options = {:rel => options} if options.is_a?(Symbol) || options.is_a?(String)
|
204
176
|
options[:array] = true
|
205
177
|
link(options, &block)
|
206
178
|
end
|
@@ -218,6 +190,24 @@ module Roar
|
|
218
190
|
end
|
219
191
|
end
|
220
192
|
end
|
193
|
+
|
194
|
+
# This is only helpful in client mode. It shouldn't be used per default.
|
195
|
+
module LinksReader
|
196
|
+
def links
|
197
|
+
return unless @links
|
198
|
+
tuples = @links.collect do |link|
|
199
|
+
if link.is_a?(Array)
|
200
|
+
next unless link.any?
|
201
|
+
[link.first.rel, link]
|
202
|
+
else
|
203
|
+
[link.rel, link]
|
204
|
+
end
|
205
|
+
end.compact
|
206
|
+
|
207
|
+
# tuples.to_h
|
208
|
+
::Hash[tuples] # TODO: tuples.to_h when dropping < 2.1.
|
209
|
+
end
|
210
|
+
end
|
221
211
|
end
|
222
212
|
end
|
223
213
|
end
|