leadlight 0.0.5 → 0.0.6

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 (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