roar 1.0.4 → 1.1.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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +14 -6
  3. data/CHANGES.markdown +75 -58
  4. data/CONTRIBUTING.md +31 -0
  5. data/Gemfile +12 -2
  6. data/ISSUE_TEMPLATE.md +20 -0
  7. data/LICENSE +1 -1
  8. data/README.markdown +126 -250
  9. data/Rakefile +3 -1
  10. data/examples/example.rb +0 -0
  11. data/examples/example_server.rb +0 -0
  12. data/lib/roar.rb +3 -3
  13. data/lib/roar/client.rb +8 -3
  14. data/lib/roar/decorator.rb +2 -2
  15. data/lib/roar/http_verbs.rb +0 -16
  16. data/lib/roar/hypermedia.rb +30 -56
  17. data/lib/roar/json.rb +5 -5
  18. data/lib/roar/json/collection.rb +10 -2
  19. data/lib/roar/json/hal.rb +72 -82
  20. data/lib/roar/version.rb +1 -1
  21. data/lib/roar/xml.rb +1 -1
  22. data/roar.gemspec +6 -6
  23. data/test/client_test.rb +1 -1
  24. data/test/coercion_feature_test.rb +7 -2
  25. data/test/decorator_test.rb +17 -7
  26. data/test/hal_json_test.rb +98 -106
  27. data/test/hypermedia_feature_test.rb +13 -31
  28. data/test/hypermedia_test.rb +26 -92
  29. data/test/{decorator_client_test.rb → integration/decorator_client_test.rb} +5 -4
  30. data/test/{faraday_http_transport_test.rb → integration/faraday_http_transport_test.rb} +1 -0
  31. data/test/{http_verbs_test.rb → integration/http_verbs_test.rb} +3 -2
  32. data/test/integration/json_collection_test.rb +35 -0
  33. data/test/{net_http_transport_test.rb → integration/net_http_transport_test.rb} +1 -0
  34. data/test/integration/runner.rb +2 -3
  35. data/test/integration/server.rb +6 -0
  36. data/test/json_representer_test.rb +2 -29
  37. data/test/lonely_test.rb +1 -2
  38. data/test/ssl_client_certs_test.rb +1 -1
  39. data/test/test_helper.rb +21 -3
  40. data/test/xml_representer_test.rb +6 -5
  41. metadata +22 -36
  42. data/gemfiles/Gemfile.representable-1.7 +0 -6
  43. data/gemfiles/Gemfile.representable-1.8 +0 -6
  44. data/gemfiles/Gemfile.representable-2.0 +0 -5
  45. data/gemfiles/Gemfile.representable-2.1 +0 -5
  46. data/gemfiles/Gemfile.representable-head +0 -6
  47. data/lib/roar/json/collection_json.rb +0 -208
  48. data/lib/roar/json/json_api.rb +0 -233
  49. data/test/collection_json_test.rb +0 -132
  50. data/test/hal_links_test.rb +0 -31
  51. data/test/json_api_test.rb +0 -451
  52. 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['test/*_test.rb']
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
File without changes
File without changes
@@ -1,6 +1,6 @@
1
1
  require 'roar/version'
2
+
2
3
  module Roar
3
- def self.root
4
- File.expand_path '../..', __FILE__
5
- end
6
4
  end
5
+
6
+ require "roar/decorator"
@@ -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[:links] ||= false
39
+ options[:user_options] ||= {}
40
+ options[:user_options][:links] ||= false
41
+
37
42
  super(options)
38
43
  end
39
44
  end
@@ -4,8 +4,8 @@ require 'representable/decorator'
4
4
  class Roar::Decorator < Representable::Decorator
5
5
  module HypermediaConsumer
6
6
  def links=(arr)
7
- links = super
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
@@ -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
@@ -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
- def links=(arr) # called when assigning parsed links.
42
- @links = LinkCollection[*arr]
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
- LinkCollection[*compile_links_for(link_configs, options)]
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 : {:href => 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, *args)
80
- instance_exec(*args, &block)
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
- create_links_definition! # this assures the links are rendered at the right position.
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
- include LinkConfigsMethod
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
- return if representable_attrs.get(:links) # only create it once.
101
+ dfn = definitions["links"] and return dfn # only create it once.
137
102
 
138
103
  options = links_definition_options
139
- options.merge!(:getter => lambda { |opts| prepare_links!(opts) })
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
- representable_attrs.add(:links, options)
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
@@ -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
- :collection => true,
41
- :class => Hypermedia::Hyperlink,
42
- :extend => HyperlinkRepresenter,
43
- :exec_context => :decorator,
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
- module HyperlinkRepresenter
51
+ class HyperlinkDecorator < Representable::Decorator
52
52
  include Representable::JSON::Hash
53
53
  end
54
54
  end
@@ -1,3 +1,11 @@
1
- require "representable/json/collection"
1
+ require 'roar/json'
2
2
 
3
- Roar::JSON::Collection = Representable::JSON::Collection
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
@@ -1,4 +1,6 @@
1
- require 'roar/json'
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] # TODO: remove :array and use special instan
92
+ return super(href, options) unless options[:array] # returns Hyperlink.
118
93
 
119
- list = href.collect { |opts| Hypermedia::Hyperlink.new(opts.merge!(:rel => options[:rel])) }
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
- require 'representable/json/hash'
131
- module LinkCollectionRepresenter
132
- include Representable::JSON::Hash
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
- def to_hash(options)
140
- super.tap do |hsh| # TODO: cool: super(:exclude => [:rel]).
141
- hsh.each { |k,v| v.delete("rel") }
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
- # DISCUSS: we can probably get rid of this asset.
157
- class LinkArray < Array
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
- def merge!(attrs)
166
- each { |lnk| lnk.merge!(attrs) }
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
- require 'representable/json/collection'
171
- module LinkArrayRepresenter
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
- items :extend => Roar::JSON::HyperlinkRepresenter,
175
- :class => Roar::Hypermedia::Hyperlink
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 to_hash(*)
178
- super.tap do |ary|
179
- ary.each { |lnk| rel = lnk.delete("rel") }
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
- :as => :links,
190
- :extend => HAL::Links::LinkCollectionRepresenter,
191
- :instance => lambda { |*| LinkCollection.new(link_array_rels) }, # defined in InstanceMethods as this is executed in represented context.
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