roar 1.0.4 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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