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.
- data/Gemfile.lock +20 -18
- data/default.gems +1 -1
- data/leadlight.gemspec +24 -4
- data/lib/leadlight.rb +4 -5
- data/lib/leadlight/connection_builder.rb +29 -0
- data/lib/leadlight/errors.rb +11 -5
- data/lib/leadlight/hyperlinkable.rb +31 -10
- data/lib/leadlight/lib_ext.rb +3 -0
- data/lib/leadlight/lib_ext/faraday/README +6 -0
- data/lib/leadlight/lib_ext/faraday/adapter.rb +7 -0
- data/lib/leadlight/lib_ext/faraday/builder.rb +47 -0
- data/lib/leadlight/lib_ext/faraday/connection.rb +40 -0
- data/lib/leadlight/lib_ext/faraday/middleware.rb +7 -0
- data/lib/leadlight/link.rb +91 -4
- data/lib/leadlight/link_template.rb +13 -5
- data/lib/leadlight/null_link.rb +21 -0
- data/lib/leadlight/param_hash.rb +42 -0
- data/lib/leadlight/representation.rb +23 -3
- data/lib/leadlight/request.rb +42 -10
- data/lib/leadlight/service.rb +11 -9
- data/lib/leadlight/service_class_methods.rb +11 -3
- data/lib/leadlight/tint_helper.rb +32 -2
- data/spec/cassettes/Leadlight/authorized_GitHub_example/_user/has_the_expected_content.yml +72 -54
- data/spec/cassettes/Leadlight/authorized_GitHub_example/_user/indicates_the_expected_oath_scopes.yml +73 -55
- data/spec/cassettes/Leadlight/authorized_GitHub_example/adding_and_removing_team_members.yml +353 -265
- data/spec/cassettes/Leadlight/authorized_GitHub_example/adding_and_removing_teams.yml +75 -175
- data/spec/cassettes/Leadlight/authorized_GitHub_example/team_list/should_have_a_link_back_to_the_org.yml +196 -0
- data/spec/cassettes/Leadlight/authorized_GitHub_example/test_team/.yml +152 -108
- data/spec/cassettes/Leadlight/authorized_GitHub_example/test_team/has_a_root_link.yml +197 -0
- data/spec/cassettes/Leadlight/basic_GitHub_example/_root/.yml +15 -43
- data/spec/cassettes/Leadlight/basic_GitHub_example/_root/__location__/.yml +15 -43
- data/spec/cassettes/Leadlight/basic_GitHub_example/_root/should_be_a_204_no_content.yml +15 -43
- data/spec/cassettes/Leadlight/tinted_GitHub_example/_root/.yml +15 -43
- data/spec/cassettes/Leadlight/tinted_GitHub_example/_root/__location__/.yml +15 -43
- data/spec/cassettes/Leadlight/tinted_GitHub_example/_root/should_be_a_204_no_content.yml +15 -43
- data/spec/cassettes/Leadlight/tinted_GitHub_example/_user/has_the_expected_content.yml +35 -90
- data/spec/cassettes/Leadlight/tinted_GitHub_example/bad_links/enables_custom_error_matching.yml +25 -17
- data/spec/cassettes/Leadlight/tinted_GitHub_example/bad_links/should_raise_ResourceNotFound.yml +26 -90
- data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/.yml +57 -140
- data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_be_able_to_follow_next_link.yml +79 -192
- data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_be_enumerable.yml +279 -350
- data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_be_enumerable_over_page_boundaries.yml +98 -243
- data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_have_next_and_last_links.yml +59 -142
- data/spec/cassettes/Leadlight/tinted_GitHub_example/user_link/exists.yml +15 -43
- data/spec/cassettes/Leadlight/tinted_GitHub_example/user_link/links_to_the_expected_URL.yml +15 -43
- data/spec/leadlight/hyperlinkable_spec.rb +50 -4
- data/spec/leadlight/link_spec.rb +70 -10
- data/spec/leadlight/link_template_spec.rb +20 -4
- data/spec/leadlight/null_link_spec.rb +14 -0
- data/spec/leadlight/param_hash_spec.rb +26 -0
- data/spec/leadlight/representation_spec.rb +36 -4
- data/spec/leadlight/request_spec.rb +39 -22
- data/spec/leadlight/service_spec.rb +14 -13
- data/spec/leadlight/tint_helper_spec.rb +36 -2
- data/spec/leadlight_spec.rb +31 -17
- data/spec/support/link_matchers.rb +59 -0
- data/spec/support/misc.rb +9 -0
- metadata +49 -34
data/lib/leadlight/link.rb
CHANGED
@@ -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
|
-
|
4
|
-
|
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
|
-
|
47
|
+
HTTP_METHODS_WITH_BODY.each do |name|
|
24
48
|
define_method(name) do |*args, &block|
|
25
|
-
|
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,
|
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
|
-
|
32
|
-
|
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__
|
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(
|
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__
|
data/lib/leadlight/request.rb
CHANGED
@@ -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,
|
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
|
58
|
-
request
|
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
|
data/lib/leadlight/service.rb
CHANGED
@@ -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 ||=
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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,
|
57
|
-
req = request_class.new(self, connection, url, http_method,
|
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
|