roar 0.12.9 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +8 -10
  4. data/CHANGES.markdown +24 -2
  5. data/Gemfile +5 -2
  6. data/README.markdown +132 -28
  7. data/TODO.markdown +9 -7
  8. data/examples/example.rb +2 -0
  9. data/examples/example_server.rb +19 -3
  10. data/gemfiles/Gemfile.representable-2.0 +5 -0
  11. data/gemfiles/Gemfile.representable-2.1 +5 -0
  12. data/lib/roar.rb +1 -1
  13. data/lib/roar/client.rb +38 -0
  14. data/lib/roar/{representer/feature/coercion.rb → coercion.rb} +2 -1
  15. data/lib/roar/decorator.rb +3 -11
  16. data/lib/roar/http_verbs.rb +88 -0
  17. data/lib/roar/hypermedia.rb +174 -0
  18. data/lib/roar/json.rb +55 -0
  19. data/lib/roar/json/collection.rb +3 -0
  20. data/lib/roar/{representer/json → json}/collection_json.rb +20 -20
  21. data/lib/roar/{representer/json → json}/hal.rb +33 -31
  22. data/lib/roar/json/hash.rb +3 -0
  23. data/lib/roar/json/json_api.rb +208 -0
  24. data/lib/roar/representer.rb +3 -36
  25. data/lib/roar/transport/faraday.rb +49 -0
  26. data/lib/roar/transport/net_http.rb +57 -0
  27. data/lib/roar/transport/net_http/request.rb +72 -0
  28. data/lib/roar/version.rb +1 -1
  29. data/lib/roar/xml.rb +54 -0
  30. data/roar.gemspec +5 -4
  31. data/test/client_test.rb +3 -3
  32. data/test/coercion_feature_test.rb +6 -3
  33. data/test/collection_json_test.rb +8 -10
  34. data/test/decorator_test.rb +27 -15
  35. data/test/faraday_http_transport_test.rb +13 -15
  36. data/test/hal_json_test.rb +16 -16
  37. data/test/hal_links_test.rb +3 -3
  38. data/test/http_verbs_test.rb +17 -22
  39. data/test/hypermedia_feature_test.rb +23 -45
  40. data/test/hypermedia_test.rb +11 -23
  41. data/test/integration/band_representer.rb +2 -2
  42. data/test/integration/runner.rb +4 -3
  43. data/test/integration/server.rb +13 -2
  44. data/test/integration/ssl_server.rb +1 -1
  45. data/test/json_api_test.rb +336 -0
  46. data/test/json_representer_test.rb +16 -12
  47. data/test/lib/runner.rb +134 -0
  48. data/test/lonely_test.rb +9 -0
  49. data/test/net_http_transport_test.rb +4 -4
  50. data/test/representer_test.rb +2 -2
  51. data/test/{lib/roar/representer/transport/net_http/request_test.rb → ssl_client_certs_test.rb} +43 -5
  52. data/test/test_helper.rb +12 -5
  53. data/test/xml_representer_test.rb +26 -166
  54. metadata +49 -29
  55. data/gemfiles/Gemfile.representable-1.7 +0 -6
  56. data/gemfiles/Gemfile.representable-1.8 +0 -6
  57. data/lib/roar/representer/feature/client.rb +0 -39
  58. data/lib/roar/representer/feature/http_verbs.rb +0 -95
  59. data/lib/roar/representer/feature/hypermedia.rb +0 -175
  60. data/lib/roar/representer/json.rb +0 -67
  61. data/lib/roar/representer/transport/faraday.rb +0 -50
  62. data/lib/roar/representer/transport/net_http.rb +0 -59
  63. data/lib/roar/representer/transport/net_http/request.rb +0 -75
  64. data/lib/roar/representer/xml.rb +0 -61
@@ -1,6 +1,6 @@
1
- require 'roar/representer/json'
1
+ require 'roar/json'
2
2
 
3
- module Roar::Representer
3
+ module Roar
4
4
  module JSON
5
5
  # Including the JSON::HAL module in your representer will render and parse documents
6
6
  # following the HAL specification: http://stateless.co/hal_specification.html
@@ -44,7 +44,7 @@ module Roar::Representer
44
44
  module HAL
45
45
  def self.included(base)
46
46
  base.class_eval do
47
- include Roar::Representer::JSON
47
+ include Roar::JSON
48
48
  include Links # overwrites #links_definition_options.
49
49
  extend ClassMethods # overwrites #links_definition_options, again.
50
50
  include Resources
@@ -60,13 +60,13 @@ module Roar::Representer
60
60
  module Resources
61
61
  # Write the property to the +_embedded+ hash when it's a resource.
62
62
  def compile_fragment(bin, doc)
63
- embedded = ::Roar.representable_1_8? ? bin[:embedded] : bin.options[:embedded]
63
+ embedded = bin[:embedded]
64
64
  return super unless embedded
65
65
  super(bin, doc[:_embedded] ||= {})
66
66
  end
67
67
 
68
68
  def uncompile_fragment(bin, doc)
69
- embedded = Roar.representable_1_8? ? bin[:embedded] : bin.options[:embedded]
69
+ embedded = bin[:embedded]
70
70
  return super unless embedded
71
71
  super(bin, doc["_embedded"] || {})
72
72
  end
@@ -74,11 +74,11 @@ module Roar::Representer
74
74
 
75
75
  module ClassMethods
76
76
  def links_definition_options
77
- super.tap { |options| options[1].merge!(:as => :_links) }
77
+ super.merge(:as => :_links)
78
78
  end
79
79
  end
80
80
 
81
- class LinkCollection < Feature::Hypermedia::LinkCollection
81
+ class LinkCollection < Hypermedia::LinkCollection
82
82
  def initialize(array_rels, *args)
83
83
  super(*args)
84
84
  @array_rels = array_rels.map(&:to_s)
@@ -106,11 +106,9 @@ module Roar::Representer
106
106
  # Note that the HAL::Links module alone doesn't prepend an underscore to +links+. Use the JSON::HAL module for that.
107
107
  module Links
108
108
  def self.included(base)
109
- base.class_eval do
110
- extend Links::ClassMethods # ::links_definition_options
111
- include Roar::Representer::Feature::Hypermedia
112
- include InstanceMethods
113
- end
109
+ base.extend ClassMethods # ::links_definition_options
110
+ base.send :include, Hypermedia
111
+ base.send :include, InstanceMethods
114
112
  end
115
113
 
116
114
  module InstanceMethods
@@ -118,7 +116,7 @@ module Roar::Representer
118
116
  def prepare_link_for(href, options)
119
117
  return super(href, options) unless options[:array] # TODO: remove :array and use special instan
120
118
 
121
- list = href.collect { |opts| Feature::Hypermedia::Hyperlink.new(opts.merge!(:rel => options[:rel])) }
119
+ list = href.collect { |opts| Hypermedia::Hyperlink.new(opts.merge!(:rel => options[:rel])) }
122
120
  LinkArray.new(list, options[:rel])
123
121
  end
124
122
 
@@ -133,25 +131,29 @@ module Roar::Representer
133
131
  module LinkCollectionRepresenter
134
132
  include Representable::JSON::Hash
135
133
 
136
- values :extend => lambda { |item, *| item.is_a?(Array) ? LinkArrayRepresenter : Roar::Representer::JSON::HyperlinkRepresenter },
137
- :instance => lambda { |fragment, *| fragment.is_a?(LinkArray) ? fragment : Roar::Representer::Feature::Hypermedia::Hyperlink.new }
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
+ }
138
138
 
139
139
  def to_hash(options)
140
140
  super.tap do |hsh| # TODO: cool: super(:exclude => [:rel]).
141
- hsh.each { |k,v| v.delete(:rel) }
141
+ hsh.each { |k,v| v.delete("rel") }
142
142
  end
143
143
  end
144
144
 
145
145
 
146
- def from_hash(hash, options={})
146
+ def from_hash(hash, *args)
147
147
  hash.each { |k,v| hash[k] = LinkArray.new(v, k) if is_array?(k) }
148
148
 
149
149
  hsh = super(hash) # this is where :class and :extend do the work.
150
150
 
151
- hsh.each { |k, v| v.rel = k }
151
+ hsh.each { |k, v| v.merge!(:rel => k) }
152
+ hsh.values # links= expects [Hyperlink, Hyperlink]
152
153
  end
153
154
  end
154
155
 
156
+ # DISCUSS: we can probably get rid of this asset.
155
157
  class LinkArray < Array
156
158
  def initialize(elems, rel)
157
159
  super(elems)
@@ -160,8 +162,8 @@ module Roar::Representer
160
162
 
161
163
  attr_reader :rel
162
164
 
163
- def rel=(rel)
164
- each { |lnk| lnk.rel = rel }
165
+ def merge!(attrs)
166
+ each { |lnk| lnk.merge!(attrs) }
165
167
  end
166
168
  end
167
169
 
@@ -169,12 +171,12 @@ module Roar::Representer
169
171
  module LinkArrayRepresenter
170
172
  include Representable::JSON::Collection
171
173
 
172
- items :extend => Roar::Representer::JSON::HyperlinkRepresenter,
173
- :class => Roar::Representer::Feature::Hypermedia::Hyperlink
174
+ items :extend => Roar::JSON::HyperlinkRepresenter,
175
+ :class => Roar::Hypermedia::Hyperlink
174
176
 
175
177
  def to_hash(*)
176
178
  super.tap do |ary|
177
- ary.each { |lnk| rel = lnk.delete(:rel) }
179
+ ary.each { |lnk| rel = lnk.delete("rel") }
178
180
  end
179
181
  end
180
182
  end
@@ -182,13 +184,13 @@ module Roar::Representer
182
184
 
183
185
  module ClassMethods
184
186
  def links_definition_options
185
- [:links,
186
- {
187
- :extend => HAL::Links::LinkCollectionRepresenter,
188
- :instance => lambda { |*| LinkCollection.new(link_array_rels) }, # defined in InstanceMethods as this is executed in represented context.
189
- :decorator_scope => true
190
- }
191
- ]
187
+ # property :links_array,
188
+ {
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.
192
+ :exec_context => :decorator,
193
+ }
192
194
  end
193
195
 
194
196
  # Use this to define link arrays. It accepts the shared rel attribute and an array of options per link object.
@@ -199,7 +201,7 @@ module Roar::Representer
199
201
  # end
200
202
  def links(options, &block)
201
203
  options = {:rel => options} if options.is_a?(Symbol)
202
- options[:array] = true # FIXME: we need to save this information somewhere.
204
+ options[:array] = true
203
205
  link(options, &block)
204
206
  end
205
207
 
@@ -0,0 +1,3 @@
1
+ require "representable/json/hash"
2
+
3
+ Roar::JSON::Hash = Representable::JSON::Hash
@@ -0,0 +1,208 @@
1
+ require 'roar/json'
2
+ require 'roar/decorator'
3
+
4
+ module Roar
5
+ module JSON
6
+ module JSONAPI
7
+ def self.included(base)
8
+ base.class_eval do
9
+ include Representable::JSON
10
+ include Roar::JSON::JSONAPI::Singular
11
+ include Roar::JSON::JSONAPI::Resource
12
+ include Roar::JSON::JSONAPI::Document
13
+
14
+ extend ForCollection
15
+
16
+ representable_attrs[:resource_representer] = Class.new(Resource::Representer)
17
+ end
18
+ end
19
+
20
+ module ForCollection
21
+ def for_collection # same API as representable. TODO: we could use ::collection_representer! here.
22
+ singular = self # e.g. Song::Representer
23
+
24
+ # this basically does Module.new { include Hash::Collection .. }
25
+ build_inline(nil, [Document::Collection, Representable::Hash::Collection, Roar::JSON], "", {}) do
26
+ items extend: singular, :parse_strategy => :sync
27
+
28
+ representable_attrs[:resource_representer] = singular.representable_attrs[:resource_representer]
29
+ representable_attrs[:_wrap] = singular.representable_attrs[:_wrap]
30
+ end
31
+ end
32
+ end
33
+
34
+
35
+ module Singular
36
+ def to_hash(options={})
37
+ # per resource:
38
+ super(:exclude => [:links]).tap do |hash|
39
+ hash["links"] = hash.delete("_links") if hash["_links"]
40
+ end
41
+ end
42
+
43
+ def from_hash(hash, options={})
44
+ hash["_links"] = hash["links"]
45
+ super
46
+ end
47
+ end
48
+
49
+
50
+ module Resource
51
+ # ::link is delegated to Representer which handles the hypermedia (rendering
52
+ # and parsing links).
53
+ class Representer < Roar::Decorator
54
+ include Roar::JSON
55
+ include Roar::Hypermedia
56
+
57
+ def self.links_definition_options
58
+ {
59
+ :extend => LinkCollectionRepresenter,
60
+ :exec_context => :decorator
61
+ }
62
+ end
63
+ end
64
+
65
+ def self.included(base)
66
+ base.extend Declarative # inject our ::link.
67
+ end
68
+
69
+ # New API for JSON-API representers.
70
+ module Declarative
71
+ def type(name=nil)
72
+ return super unless name # original name.
73
+ representable_attrs[:_wrap] = name.to_s
74
+ end
75
+
76
+ # Define global document links for the links: directive.
77
+ def link(*args, &block)
78
+ representable_attrs[:resource_representer].link(*args, &block)
79
+ end
80
+
81
+ # Per-model links.
82
+ def links(&block)
83
+ nested(:_links, &block)
84
+ end
85
+
86
+ # TODO: always create _links.
87
+ def has_one(name)
88
+ property :_links, :inherit => true, :use_decorator => true do # simply extend the Decorator _links.
89
+ property "#{name}_id", :as => name
90
+ end
91
+ end
92
+
93
+ def has_many(name)
94
+ property :_links, :inherit => true, :use_decorator => true do # simply extend the Decorator _links.
95
+ collection "#{name.to_s.sub(/s$/, "")}_ids", :as => name
96
+ end
97
+ end
98
+
99
+ def compound(&block)
100
+ nested(:linked, &block)
101
+ end
102
+ end
103
+ end
104
+
105
+
106
+ # TODO: don't use Document for singular+wrap AND singular in collection (this way, we can get rid of the only_body)
107
+ module Document
108
+ def to_hash(options={})
109
+ # per resource:
110
+ res = super # render single resource or collection.
111
+ return res if options[:only_body]
112
+ # this is the only "dirty" part: this module is always included in the Singular document representer, when used in collection, we don't want it to do the extra work. this mechanism here might be changed soon.
113
+
114
+ to_document(res)
115
+ end
116
+
117
+ def from_hash(hash, options={})
118
+
119
+ return super(hash, options) if options[:only_body] # singular
120
+
121
+ super(from_document(hash)) # singular
122
+ end
123
+
124
+ private
125
+ def to_document(res)
126
+ links = representable_attrs[:resource_representer].new(represented).to_hash # creates links: section.
127
+ # FIXME: provide two different #to_document
128
+
129
+ if res.is_a?(Array)
130
+ compound = collection_compound!(res, {})
131
+ else
132
+ compound = compile_compound!(res.delete("linked"), {})
133
+ end
134
+
135
+ {representable_attrs[:_wrap] => res}.tap do |doc|
136
+ doc.merge!(links)
137
+ doc.merge!("linked" => compound) if compound && compound.size > 0
138
+ end
139
+ end
140
+
141
+ def from_document(hash)
142
+ hash[representable_attrs[:_wrap]]
143
+ end
144
+
145
+ # Compiles the linked: section for compound objects in the document.
146
+ def collection_compound!(collection, compound)
147
+ collection.each { |res|
148
+ kv = res.delete("linked") or next
149
+
150
+ compile_compound!(kv, compound)
151
+ }
152
+
153
+ compound
154
+ end
155
+
156
+ # Go through {"album"=>{"title"=>"Hackers"}, "musicians"=>[{"name"=>"Eddie Van Halen"}, ..]} from linked:
157
+ # and wrap every item in an array.
158
+ def compile_compound!(linked, compound)
159
+ return unless linked
160
+
161
+ linked.each { |k,v| # {"album"=>{"title"=>"Hackers"}, "musicians"=>[{"name"=>"Eddie Van Halen"}, {"name"=>"Greg Howe"}]}
162
+ compound[k] ||= []
163
+
164
+ if v.is_a?(::Hash) # {"title"=>"Hackers"}
165
+ compound[k] << v
166
+ else
167
+ compound[k].push(*v) # [{"name"=>"Eddie Van Halen"}, {"name"=>"Greg Howe"}]
168
+ end
169
+
170
+ compound[k] = compound[k].uniq
171
+ }
172
+
173
+ compound
174
+ end
175
+
176
+
177
+ module Collection
178
+ include Document
179
+
180
+ def to_hash(options={})
181
+ res = super(options.merge(:only_body => true))
182
+ to_document(res)
183
+ end
184
+
185
+ def from_hash(hash, options={})
186
+ hash = from_document(hash)
187
+ super(hash, options.merge(:only_body => true))
188
+ end
189
+ end
190
+ end
191
+
192
+
193
+ module LinkRepresenter
194
+ include Roar::JSON
195
+
196
+ property :href
197
+ property :type
198
+ end
199
+
200
+ require 'representable/json/hash'
201
+ module LinkCollectionRepresenter
202
+ include Representable::JSON::Hash
203
+
204
+ values :extend => LinkRepresenter # TODO: parsing.
205
+ end
206
+ end
207
+ end
208
+ end
@@ -1,44 +1,11 @@
1
1
  require 'representable'
2
2
 
3
3
  module Roar
4
+ # generic features can be included here and will be available in all format-specific representers.
4
5
  module Representer
5
- # TODO: move to separate module
6
- # DISCUSS: experimental. this will soon be moved to a separate gem
7
- module InheritableArray
8
- def representable_attrs
9
- super.extend(ConfigExtensions)
10
- end
11
-
12
- module ConfigExtensions
13
- def inheritable_array(name)
14
- inheritable_arrays[name] ||= []
15
- end
16
- def inheritable_arrays
17
- @inheritable_arrays ||= {}
18
- end
19
-
20
- def inherit(parent)
21
- super
22
-
23
- parent.inheritable_arrays.keys.each do |k|
24
- inheritable_array(k).push *parent.inheritable_array(k).clone
25
- end
26
- end
27
- end
28
- end
29
-
30
6
  def self.included(base)
31
- base.class_eval do
32
- include Representable
33
- extend InheritableArray # this adds InheritableArray::representable_attrs to the module, e.g. when a representer includes a representer, we don't work with the instance method, yet.
34
- end
35
- end
36
-
37
- include InheritableArray
38
-
39
-
40
- private
41
- def before_serialize(*)
7
+ super
8
+ base.send(:include, Representable)
42
9
  end
43
10
  end
44
11
  end
@@ -0,0 +1,49 @@
1
+ gem 'faraday'
2
+ require 'faraday'
3
+
4
+ module Roar
5
+ module Transport
6
+ # Advanced implementation of the HTTP verbs with the Faraday HTTP library
7
+ # (which can, in turn, use adapters based on Net::HTTP or libcurl)
8
+ #
9
+ # Depending on how the Faraday middleware stack is configured, this
10
+ # Transport can support features such as HTTP error code handling,
11
+ # redirects, etc.
12
+ #
13
+ # @see http://rubydoc.info/gems/faraday/file/README.md Faraday README
14
+ class Faraday
15
+
16
+ def get_uri(options)
17
+ build_connection(options[:uri], options[:as]).get
18
+ end
19
+
20
+ def post_uri(options)
21
+ build_connection(options[:uri], options[:as]).post(nil, options[:body])
22
+ end
23
+
24
+ def put_uri(options)
25
+ build_connection(options[:uri], options[:as]).put(nil, options[:body])
26
+ end
27
+
28
+ def patch_uri(options)
29
+ build_connection(options[:uri], options[:as]).patch(nil, options[:body])
30
+ end
31
+
32
+ def delete_uri(options)
33
+ build_connection(options[:uri], options[:as]).delete
34
+ end
35
+
36
+ private
37
+
38
+ def build_connection(uri, as)
39
+ ::Faraday::Connection.new(
40
+ :url => uri,
41
+ :headers => { :accept => as, :content_type => as }
42
+ ) do |builder|
43
+ builder.use ::Faraday::Response::RaiseError
44
+ builder.adapter ::Faraday.default_adapter
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end