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