leadlight 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. data/Gemfile.lock +20 -18
  2. data/default.gems +1 -1
  3. data/leadlight.gemspec +24 -4
  4. data/lib/leadlight.rb +4 -5
  5. data/lib/leadlight/connection_builder.rb +29 -0
  6. data/lib/leadlight/errors.rb +11 -5
  7. data/lib/leadlight/hyperlinkable.rb +31 -10
  8. data/lib/leadlight/lib_ext.rb +3 -0
  9. data/lib/leadlight/lib_ext/faraday/README +6 -0
  10. data/lib/leadlight/lib_ext/faraday/adapter.rb +7 -0
  11. data/lib/leadlight/lib_ext/faraday/builder.rb +47 -0
  12. data/lib/leadlight/lib_ext/faraday/connection.rb +40 -0
  13. data/lib/leadlight/lib_ext/faraday/middleware.rb +7 -0
  14. data/lib/leadlight/link.rb +91 -4
  15. data/lib/leadlight/link_template.rb +13 -5
  16. data/lib/leadlight/null_link.rb +21 -0
  17. data/lib/leadlight/param_hash.rb +42 -0
  18. data/lib/leadlight/representation.rb +23 -3
  19. data/lib/leadlight/request.rb +42 -10
  20. data/lib/leadlight/service.rb +11 -9
  21. data/lib/leadlight/service_class_methods.rb +11 -3
  22. data/lib/leadlight/tint_helper.rb +32 -2
  23. data/spec/cassettes/Leadlight/authorized_GitHub_example/_user/has_the_expected_content.yml +72 -54
  24. data/spec/cassettes/Leadlight/authorized_GitHub_example/_user/indicates_the_expected_oath_scopes.yml +73 -55
  25. data/spec/cassettes/Leadlight/authorized_GitHub_example/adding_and_removing_team_members.yml +353 -265
  26. data/spec/cassettes/Leadlight/authorized_GitHub_example/adding_and_removing_teams.yml +75 -175
  27. data/spec/cassettes/Leadlight/authorized_GitHub_example/team_list/should_have_a_link_back_to_the_org.yml +196 -0
  28. data/spec/cassettes/Leadlight/authorized_GitHub_example/test_team/.yml +152 -108
  29. data/spec/cassettes/Leadlight/authorized_GitHub_example/test_team/has_a_root_link.yml +197 -0
  30. data/spec/cassettes/Leadlight/basic_GitHub_example/_root/.yml +15 -43
  31. data/spec/cassettes/Leadlight/basic_GitHub_example/_root/__location__/.yml +15 -43
  32. data/spec/cassettes/Leadlight/basic_GitHub_example/_root/should_be_a_204_no_content.yml +15 -43
  33. data/spec/cassettes/Leadlight/tinted_GitHub_example/_root/.yml +15 -43
  34. data/spec/cassettes/Leadlight/tinted_GitHub_example/_root/__location__/.yml +15 -43
  35. data/spec/cassettes/Leadlight/tinted_GitHub_example/_root/should_be_a_204_no_content.yml +15 -43
  36. data/spec/cassettes/Leadlight/tinted_GitHub_example/_user/has_the_expected_content.yml +35 -90
  37. data/spec/cassettes/Leadlight/tinted_GitHub_example/bad_links/enables_custom_error_matching.yml +25 -17
  38. data/spec/cassettes/Leadlight/tinted_GitHub_example/bad_links/should_raise_ResourceNotFound.yml +26 -90
  39. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/.yml +57 -140
  40. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_be_able_to_follow_next_link.yml +79 -192
  41. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_be_enumerable.yml +279 -350
  42. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_be_enumerable_over_page_boundaries.yml +98 -243
  43. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_have_next_and_last_links.yml +59 -142
  44. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_link/exists.yml +15 -43
  45. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_link/links_to_the_expected_URL.yml +15 -43
  46. data/spec/leadlight/hyperlinkable_spec.rb +50 -4
  47. data/spec/leadlight/link_spec.rb +70 -10
  48. data/spec/leadlight/link_template_spec.rb +20 -4
  49. data/spec/leadlight/null_link_spec.rb +14 -0
  50. data/spec/leadlight/param_hash_spec.rb +26 -0
  51. data/spec/leadlight/representation_spec.rb +36 -4
  52. data/spec/leadlight/request_spec.rb +39 -22
  53. data/spec/leadlight/service_spec.rb +14 -13
  54. data/spec/leadlight/tint_helper_spec.rb +36 -2
  55. data/spec/leadlight_spec.rb +31 -17
  56. data/spec/support/link_matchers.rb +59 -0
  57. data/spec/support/misc.rb +9 -0
  58. metadata +49 -34
@@ -0,0 +1,7 @@
1
+ module Faraday
2
+ class Middleware
3
+ def self.adapter?
4
+ false
5
+ end
6
+ end
7
+ end
@@ -1,8 +1,16 @@
1
+ require 'addressable/uri'
2
+ require 'addressable/template'
3
+ require 'leadlight/param_hash'
4
+
1
5
  module Leadlight
2
6
  class Link
3
- HTTP_METHODS = [
4
- :options, :head, :get, :get_representation!, :post, :put, :delete, :patch
7
+ include ::Leadlight
8
+
9
+ HTTP_METHODS_WITH_BODY = [:post, :put, :patch]
10
+ HTTP_METHODS_WITHOUT_BODY = [
11
+ :options, :head, :get, :get_representation!, :delete
5
12
  ]
13
+ HTTP_METHODS = HTTP_METHODS_WITHOUT_BODY + HTTP_METHODS_WITH_BODY
6
14
 
7
15
  attr_reader :service
8
16
  attr_reader :rel
@@ -10,19 +18,42 @@ module Leadlight
10
18
  attr_reader :title
11
19
  attr_reader :href
12
20
  attr_reader :aliases
21
+ attr_reader :options
22
+
23
+ # Expansion params have no effect on the Link. They exist to keep
24
+ # a record of how this particular Link instance was constructed,
25
+ # if it was constructed via expansion.
26
+ attr_reader :expansion_params
13
27
 
14
28
  def initialize(service, href, rel=nil, title=rel, options={})
29
+ @options = options
15
30
  @service = service
16
31
  @href = Addressable::URI.parse(href)
17
32
  @rel = rel.to_s
18
33
  @title = title.to_s
19
34
  @rev = options[:rev]
20
35
  @aliases = Array(options[:aliases])
36
+ self.expansion_params = options.fetch(:expansion_params) { {} }
37
+ end
38
+
39
+ HTTP_METHODS_WITHOUT_BODY.each do |name|
40
+ define_method(name) do |*args, &block|
41
+ request_options = args.last.is_a?(Hash) ? args.pop : {}
42
+ request_options[:link] = self
43
+ service.public_send(name, href, nil, *args, request_options, &block)
44
+ end
21
45
  end
22
46
 
23
- [:options, :head, :get, :get_representation!, :post, :put, :delete, :patch].each do |name|
47
+ HTTP_METHODS_WITH_BODY.each do |name|
24
48
  define_method(name) do |*args, &block|
25
- service.public_send(name, href, *args, &block)
49
+ request_options = if args.size > 1 && args.last.is_a?(Hash)
50
+ args.pop
51
+ else
52
+ {}
53
+ end
54
+ body = args.shift
55
+ request_options[:link] = self
56
+ service.public_send(name, href, body, *args, request_options, &block)
26
57
  end
27
58
  end
28
59
 
@@ -31,5 +62,61 @@ module Leadlight
31
62
  return representation
32
63
  end
33
64
  end
65
+
66
+ def expand(expansion_params=nil)
67
+ if expansion_params
68
+ dup_with_new_href(expand_uri_with_params(href.dup, expansion_params), expansion_params)
69
+ else
70
+ self
71
+ end
72
+ end
73
+
74
+ def [](*args)
75
+ expand(*args)
76
+ end
77
+
78
+ def to_s
79
+ "Link(#{rel}:#{href}#{inspect_expansion_params})"
80
+ end
81
+
82
+ def params
83
+ extracted_params.merge(expansion_params)
84
+ end
85
+
86
+ protected
87
+
88
+ attr_writer :href
89
+
90
+ def expansion_params=(new_params)
91
+ @expansion_params = new_params.each_with_object({}){|(k,v),h|
92
+ h[k.to_s] = v.to_s
93
+ }
94
+ end
95
+
96
+ private
97
+
98
+ def expand_uri_with_params(uri, uri_params)
99
+ uri.query_values = ParamHash(uri_params) if uri_params.any?
100
+ uri.normalize
101
+ end
102
+
103
+ def dup_with_new_href(uri, expansion_params={})
104
+ self.dup.tap do |link|
105
+ link.href = uri
106
+ link.expansion_params = expansion_params
107
+ end
108
+ end
109
+
110
+ def inspect_expansion_params
111
+ if expansion_params.empty?
112
+ ""
113
+ else
114
+ " [#{expansion_params.inspect}]"
115
+ end
116
+ end
117
+
118
+ def extracted_params
119
+ href.query_values || {}
120
+ end
34
121
  end
35
122
  end
@@ -1,8 +1,12 @@
1
+ require 'forwardable'
1
2
  require 'leadlight/link'
2
3
  require 'addressable/template'
3
4
 
4
5
  module Leadlight
5
6
  class LinkTemplate < Link
7
+ extend Forwardable
8
+
9
+ def_delegators :href_template, :variables
6
10
 
7
11
  def href_template
8
12
  @href_template ||= Addressable::Template.new(href.to_s)
@@ -10,26 +14,30 @@ module Leadlight
10
14
 
11
15
  HTTP_METHODS.each do |name|
12
16
  define_method(name) do |*args, &block|
13
- expanded_href = expand(args)
14
- service.public_send(name, expanded_href, *args, &block)
17
+ expanded_href = expand(*args).href
18
+ service.public_send(name, expanded_href, &block)
15
19
  end
16
20
  end
17
21
 
18
- def expand(args)
22
+ def expand(*args)
23
+ return self if args.empty?
19
24
  mapping = args.last.is_a?(Hash) ? args.pop : {}
20
25
  mapping = mapping.inject({}) { |result, (k,v)| result.merge!(k.to_s => v) }
21
26
  mapping = href_template.variables.inject(mapping) do |mapping, var|
22
27
  mapping.merge!(var => args.shift) unless args.empty?
23
28
  mapping
24
29
  end
30
+ full_mapping = mapping.dup
25
31
  extra_keys = (mapping.keys.map(&:to_s) - href_template.variables)
26
32
  extra_params = extra_keys.inject({}) do |params, key|
27
33
  params[key] = mapping.delete(key)
28
34
  params
29
35
  end
30
36
  assert_all_variables_mapped(href_template, mapping)
31
- args.push extra_params unless extra_params.empty?
32
- href_template.expand(mapping).to_s
37
+ uri = href_template.expand(mapping)
38
+ expanded_uri = expand_uri_with_params(uri, extra_params)
39
+ Link.new(service, expanded_uri, rel, title,
40
+ rev: rev, aliases: aliases, expansion_params: full_mapping)
33
41
  end
34
42
 
35
43
  private
@@ -0,0 +1,21 @@
1
+ require 'addressable/uri'
2
+
3
+ module Leadlight
4
+ class NullLink
5
+ include Addressable
6
+ attr_reader :href
7
+
8
+ def initialize(href)
9
+ @href = href
10
+ end
11
+
12
+ def ==(other)
13
+ other.is_a?(self.class) &&
14
+ href == other.href
15
+ end
16
+
17
+ def params
18
+ URI.parse(href).query_values || {}
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,42 @@
1
+ require 'delegate'
2
+
3
+ module Leadlight
4
+ def ParamHash(hash)
5
+ case hash
6
+ when ParamHash then hash
7
+ else ParamHash.new(hash)
8
+ end
9
+ end
10
+ module_function :ParamHash
11
+
12
+ class ParamHash < DelegateClass(Hash)
13
+ def initialize(hash)
14
+ super(transform_hash(Hash[hash], :deep => true) {|h,k,v|
15
+ h[k] = transform_value(v)
16
+ })
17
+ end
18
+
19
+ private
20
+
21
+ def transform_hash(original, options={}, &block)
22
+ original.inject({}){|result, (key,value)|
23
+ value = if (options[:deep] && Hash === value)
24
+ transform_hash(value, options, &block)
25
+ else
26
+ value
27
+ end
28
+ block.call(result,key,value)
29
+ result
30
+ }
31
+ end
32
+
33
+ def transform_value(value)
34
+ case value
35
+ when ParamHash then value
36
+ when Hash then self.class.new(value)
37
+ when Array then value.map(&:to_s)
38
+ else value.to_s
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,17 +1,25 @@
1
+ require 'forwardable'
1
2
  require 'addressable/uri'
3
+ require 'fattr'
2
4
  require 'leadlight/link'
3
5
  require 'leadlight/errors'
4
6
 
5
7
  module Leadlight
6
8
  module Representation
9
+ extend Forwardable
10
+
7
11
  attr_accessor :__service__
8
12
  attr_accessor :__location__
9
13
  attr_accessor :__response__
14
+ attr_accessor :__request__
15
+
16
+ #fattr(:__captures__) { {} }
10
17
 
11
- def initialize_representation(service, location, response)
12
- self.__service__ = service
18
+ def initialize_representation(service, location, response, request)
19
+ self.__service__ = service
13
20
  self.__location__ = location
14
21
  self.__response__ = response
22
+ self.__request__ = request
15
23
  self
16
24
  end
17
25
 
@@ -27,7 +35,7 @@ module Leadlight
27
35
  when 404 then ResourceNotFound
28
36
  when (400..499) then ClientError
29
37
  when (500..599) then ServerError
30
- end.new(__response__, exception_message)
38
+ end.new(__request__, exception_message)
31
39
  end
32
40
 
33
41
  def exception_message
@@ -40,6 +48,18 @@ module Leadlight
40
48
  }
41
49
  end
42
50
 
51
+ def __link__
52
+ __request__.link
53
+ end
54
+
55
+ def __request_params__
56
+ __request__.params
57
+ end
58
+
59
+ def __captures__
60
+ @__captures__ ||= {}
61
+ end
62
+
43
63
  private
44
64
 
45
65
  def __apply_tint__
@@ -1,3 +1,4 @@
1
+ require 'ostruct'
1
2
  require 'monitor'
2
3
  require 'fattr'
3
4
  require 'forwardable'
@@ -8,6 +9,7 @@ require 'leadlight/hyperlinkable'
8
9
  require 'leadlight/representation'
9
10
  require 'leadlight/type_map'
10
11
  require 'leadlight/header_helpers'
12
+ require 'leadlight/null_link'
11
13
 
12
14
  module Leadlight
13
15
  class Request
@@ -20,29 +22,29 @@ module Leadlight
20
22
  fattr(:url)
21
23
  fattr(:connection)
22
24
  fattr(:body)
23
- fattr(:params)
24
25
  fattr(:service)
25
26
  fattr(:codec)
26
27
  fattr(:type_map) { service.type_map || TypeMap.new }
27
28
 
28
- attr_reader :response
29
+ attr_reader :response, :options, :link
29
30
 
30
31
  define_hook :on_prepare_request, :request
31
32
  define_hook :on_complete, :response
32
33
 
33
34
  def_delegator :service, :service_options
34
35
 
35
- def initialize(service, connection, url, method, params={}, body=nil)
36
+ def initialize(service, connection, url, method, body=nil, options={})
37
+ @options = options
36
38
  self.connection = connection
37
39
  self.url = url
38
40
  self.http_method = method
39
41
  self.body = body
40
- self.params = params
41
42
  self.service = service
42
43
  @completed = new_cond
43
44
  @state = :initialized
44
45
  @env = nil
45
46
  @response = nil
47
+ @link = options.fetch(:link) { NullLink.new(url) }
46
48
  super()
47
49
  end
48
50
 
@@ -54,10 +56,11 @@ module Leadlight
54
56
  entity = type_map.to_entity_body(body)
55
57
  entity_body = entity.body
56
58
  content_type = entity.content_type
57
- connection.run_request(http_method, url, entity_body, {}) do |request|
58
- request.params.update(params) unless params.empty?
59
+ connection.run_request(http_method, url.to_s, entity_body, {}) do
60
+ |request|
61
+
59
62
  request.headers['Content-Type'] = content_type if content_type
60
- request.options[:leadlight_request] = self
63
+ request.options[:leadlight_request] = self
61
64
  execute_hook(:on_prepare_request, request)
62
65
  end.on_complete do |env|
63
66
  synchronize do
@@ -109,23 +112,52 @@ module Leadlight
109
112
  end
110
113
  end
111
114
 
112
- def represent(env)
115
+ def represent(env)
113
116
  content_type = env[:response_headers]['Content-Type']
114
117
  content_type = clean_content_type(content_type)
115
118
  representation = type_map.to_native(content_type, env[:body])
116
- location = Addressable::URI.parse(env[:response_headers].fetch('location'){ env[:url] })
117
119
  representation.
118
120
  extend(Representation).
119
- initialize_representation(env[:leadlight_service], location, env[:response]).
121
+ initialize_representation(env[:leadlight_service], location(env), env[:response], self).
120
122
  extend(Hyperlinkable).
121
123
  apply_all_tints
122
124
  end
123
125
 
126
+ def params
127
+ link_params.merge(request_params)
128
+ end
129
+
130
+ def location(env=@env)
131
+ env ||= {}
132
+ url = env.fetch(:response_headers){{}}.fetch('location'){ env.fetch(:url){ self.url } }
133
+ Addressable::URI.parse(url)
134
+ end
135
+
124
136
  private
125
137
 
138
+ fattr(:faraday_request) {
139
+ env.fetch(:request) { OpenStruct.new(params: {}) }
140
+ }
141
+
142
+ def env
143
+ if completed?
144
+ @env
145
+ else
146
+ {}
147
+ end
148
+ end
149
+
126
150
  def representation
127
151
  raise "No representation until complete" unless completed?
128
152
  @env.fetch(:leadlight_representation)
129
153
  end
154
+
155
+ def request_params
156
+ faraday_request.params
157
+ end
158
+
159
+ def link_params
160
+ link.params
161
+ end
130
162
  end
131
163
  end
@@ -1,6 +1,7 @@
1
1
  require 'fattr'
2
2
  require 'forwardable'
3
3
  require 'leadlight/request'
4
+ require 'leadlight/connection_builder'
4
5
 
5
6
  module Leadlight
6
7
  module Service
@@ -13,7 +14,8 @@ module Leadlight
13
14
  fattr(:type_map) { TypeMap.new }
14
15
 
15
16
  def_delegators :codec, :encode, :decode
16
- def_delegators 'self.class', :types, :type_for_name, :request_class
17
+ def_delegators 'self.class', :types, :type_for_name, :request_class, :http_adapter
18
+ def_delegators :Leadlight, :common_connection_stack
17
19
 
18
20
  def initialize(service_options={})
19
21
  @service_options = service_options
@@ -31,12 +33,12 @@ module Leadlight
31
33
  end
32
34
 
33
35
  def connection
34
- @connection ||= Faraday.new(url: self.url) do |builder|
35
- builder.use Leadlight::ServiceMiddleware, service: self
36
- instance_exec(builder, &connection_stack)
37
- builder.use Faraday::Response::Logger, logger
38
- instance_exec(builder, &Leadlight.common_connection_stack)
39
- end
36
+ @connection ||= ConnectionBuilder.new do |cxn|
37
+ cxn.url url
38
+ cxn.service self
39
+ cxn.common_stack common_connection_stack
40
+ cxn.adapter http_adapter
41
+ end.call
40
42
  end
41
43
 
42
44
  [:options, :head, :get, :post, :put, :delete, :patch].each do |name|
@@ -53,8 +55,8 @@ module Leadlight
53
55
 
54
56
  private
55
57
 
56
- def perform_request(url, http_method, params={}, body=nil, &representation_handler)
57
- req = request_class.new(self, connection, url, http_method, params, body)
58
+ def perform_request(url, http_method, body=nil, options={}, &representation_handler)
59
+ req = request_class.new(self, connection, url, http_method, body, options)
58
60
  if representation_handler
59
61
  req.submit_and_wait(&representation_handler)
60
62
  end