leadlight 0.0.2
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 +9 -0
- data/Gemfile.lock +79 -0
- data/Guardfile +19 -0
- data/Rakefile +133 -0
- data/leadlight.gemspec +125 -0
- data/lib/leadlight.rb +92 -0
- data/lib/leadlight/blank.rb +15 -0
- data/lib/leadlight/codec.rb +63 -0
- data/lib/leadlight/enumerable_representation.rb +24 -0
- data/lib/leadlight/errors.rb +14 -0
- data/lib/leadlight/hyperlinkable.rb +126 -0
- data/lib/leadlight/link.rb +35 -0
- data/lib/leadlight/link_template.rb +47 -0
- data/lib/leadlight/representation.rb +30 -0
- data/lib/leadlight/request.rb +91 -0
- data/lib/leadlight/service.rb +73 -0
- data/lib/leadlight/service_middleware.rb +50 -0
- data/lib/leadlight/tint.rb +26 -0
- data/lib/leadlight/tint_helper.rb +67 -0
- data/lib/leadlight/type.rb +71 -0
- data/spec/cassettes/Leadlight/authorized_GitHub_example/_user/has_the_expected_content.yml +75 -0
- data/spec/cassettes/Leadlight/authorized_GitHub_example/_user/indicates_the_expected_oath_scopes.yml +75 -0
- data/spec/cassettes/Leadlight/authorized_GitHub_example/adding_and_removing_team_members.yml +384 -0
- data/spec/cassettes/Leadlight/authorized_GitHub_example/adding_and_removing_team_members/.yml +309 -0
- data/spec/cassettes/Leadlight/authorized_GitHub_example/test_team/.yml +159 -0
- data/spec/cassettes/Leadlight/basic_GitHub_example/_root/.yml +32 -0
- data/spec/cassettes/Leadlight/basic_GitHub_example/_root/__location__/.yml +32 -0
- data/spec/cassettes/Leadlight/basic_GitHub_example/_root/should_be_a_204_no_content.yml +32 -0
- data/spec/cassettes/Leadlight/tinted_GitHub_example/_root/.yml +32 -0
- data/spec/cassettes/Leadlight/tinted_GitHub_example/_root/__location__/.yml +32 -0
- data/spec/cassettes/Leadlight/tinted_GitHub_example/_root/should_be_a_204_no_content.yml +32 -0
- data/spec/cassettes/Leadlight/tinted_GitHub_example/_user/has_the_expected_content.yml +65 -0
- data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/.yml +100 -0
- data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_be_able_to_follow_next_link.yml +135 -0
- data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_be_enumerable.yml +275 -0
- data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_be_enumerable_over_page_boundaries.yml +170 -0
- data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_have_next_and_last_links.yml +100 -0
- data/spec/cassettes/Leadlight/tinted_GitHub_example/user_link/exists.yml +32 -0
- data/spec/cassettes/Leadlight/tinted_GitHub_example/user_link/links_to_the_expected_URL.yml +32 -0
- data/spec/leadlight/codec_spec.rb +93 -0
- data/spec/leadlight/hyperlinkable_spec.rb +136 -0
- data/spec/leadlight/link_spec.rb +53 -0
- data/spec/leadlight/link_template_spec.rb +45 -0
- data/spec/leadlight/representation_spec.rb +62 -0
- data/spec/leadlight/request_spec.rb +236 -0
- data/spec/leadlight/service_middleware_spec.rb +81 -0
- data/spec/leadlight/service_spec.rb +127 -0
- data/spec/leadlight/tint_helper_spec.rb +132 -0
- data/spec/leadlight/type_spec.rb +137 -0
- data/spec/leadlight_spec.rb +237 -0
- data/spec/spec_helper_lite.rb +6 -0
- data/spec/support/credentials.rb +16 -0
- data/spec/support/vcr.rb +18 -0
- metadata +229 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
module Leadlight
|
2
|
+
class Codec
|
3
|
+
Strategy ||= Struct.new(:name, :encoder, :decoder, :patterns)
|
4
|
+
|
5
|
+
def self.strategies
|
6
|
+
@strategies ||= []
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.strategy(name, encoder, decoder, patterns)
|
10
|
+
strategies << Strategy.new(name, encoder, decoder, Array(patterns))
|
11
|
+
end
|
12
|
+
|
13
|
+
strategies.clear
|
14
|
+
|
15
|
+
strategy :text,
|
16
|
+
->(rep, options) {rep.to_s},
|
17
|
+
->(entity_body, options) {entity_body},
|
18
|
+
%r{^text/plain}
|
19
|
+
|
20
|
+
strategy :json,
|
21
|
+
MultiJson.method(:encode),
|
22
|
+
MultiJson.method(:decode),
|
23
|
+
[%r{^application/json}, %r{\+json$}]
|
24
|
+
|
25
|
+
def decode(content_type, entity_body, options={})
|
26
|
+
transcode(:decode, content_type, entity_body, options)
|
27
|
+
end
|
28
|
+
|
29
|
+
def encode(content_type, representation, options={})
|
30
|
+
transcode(:encode, content_type, representation, options)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def transcode(direction, content_type, input, options)
|
36
|
+
fallback = unknown_type_handler(content_type)
|
37
|
+
strategy = fetch_strategy(content_type, &fallback)
|
38
|
+
transcoder = case direction
|
39
|
+
when :encode then strategy.encoder
|
40
|
+
when :decode then strategy.decoder
|
41
|
+
else raise ArgumentError, "Should never get here"
|
42
|
+
end
|
43
|
+
transcoder.(input, options)
|
44
|
+
end
|
45
|
+
|
46
|
+
def strategies
|
47
|
+
self.class.strategies
|
48
|
+
end
|
49
|
+
|
50
|
+
def fetch_strategy(content_type, &fallback)
|
51
|
+
content_type = content_type.to_s.strip.split.first
|
52
|
+
strategies.detect(fallback) { |strategy|
|
53
|
+
strategy.patterns.any?{|pattern| pattern === content_type}
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
def unknown_type_handler(content_type)
|
58
|
+
-> do
|
59
|
+
raise ArgumentError, "Unrecognized content type #{content_type.inspect}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Leadlight
|
2
|
+
module EnumerableRepresentation
|
3
|
+
Tint = Tint.new('EnumerableRepresentation') do
|
4
|
+
match_class(Enumerable)
|
5
|
+
extend(EnumerableRepresentation)
|
6
|
+
end
|
7
|
+
|
8
|
+
def each(call_super=false,&block)
|
9
|
+
if call_super
|
10
|
+
return super(&block)
|
11
|
+
end
|
12
|
+
|
13
|
+
page = self
|
14
|
+
loop do
|
15
|
+
page.each(true, &block)
|
16
|
+
if (next_link = page.link('next'){nil})
|
17
|
+
page = next_link.follow
|
18
|
+
else
|
19
|
+
break
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Leadlight
|
2
|
+
class Error < StandardError; end
|
3
|
+
class CredentialsRequiredError < Error; end
|
4
|
+
class HttpError < Error
|
5
|
+
attr_reader :response
|
6
|
+
def initialize(response)
|
7
|
+
@response = response
|
8
|
+
super("HTTP Error #{response.status}")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
class ClientError < HttpError; end
|
12
|
+
class ResourceNotFound < ClientError; end
|
13
|
+
class ServerError < HttpError; end
|
14
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'addressable/uri'
|
2
|
+
require 'addressable/template'
|
3
|
+
require 'link_header'
|
4
|
+
require 'leadlight/link'
|
5
|
+
require 'leadlight/link_template'
|
6
|
+
require 'forwardable'
|
7
|
+
require 'fattr'
|
8
|
+
|
9
|
+
module Leadlight
|
10
|
+
module Hyperlinkable
|
11
|
+
def self.extended(representation)
|
12
|
+
super(representation)
|
13
|
+
representation.add_link(representation.__response__.env[:url],
|
14
|
+
'self', 'self', rev: 'self')
|
15
|
+
representation.add_links_from_headers
|
16
|
+
end
|
17
|
+
|
18
|
+
def links(key=:none)
|
19
|
+
if :none == key
|
20
|
+
@__links__ ||= LinkSet.new
|
21
|
+
else
|
22
|
+
links[key]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def link(key, &fallback)
|
27
|
+
result = links.at(key, &fallback)
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_link(url, rel=nil, title=rel, options={})
|
31
|
+
link = Link.new(__service__, url, rel, title, options)
|
32
|
+
define_link_helper(rel) if rel
|
33
|
+
links << link
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_link_template(template, rel=nil, title=rel, options={})
|
37
|
+
link = LinkTemplate.new(__service__, template, rel, title, options)
|
38
|
+
define_link_helper(rel) if rel
|
39
|
+
links << link
|
40
|
+
end
|
41
|
+
|
42
|
+
def add_link_set(rel=nil, helper_name=rel)
|
43
|
+
default_link_attributes = {
|
44
|
+
rel: rel
|
45
|
+
}
|
46
|
+
yield.each do |link_attributes|
|
47
|
+
attributes = default_link_attributes.merge(link_attributes)
|
48
|
+
define_link_set_helper(rel, helper_name) if helper_name
|
49
|
+
links << Link.new(__service__,
|
50
|
+
attributes[:href],
|
51
|
+
attributes[:rel],
|
52
|
+
attributes[:title],
|
53
|
+
attributes)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def add_links_from_headers
|
58
|
+
raw_link_header = __response__.env[:response_headers]['Link']
|
59
|
+
link_header = LinkHeader.parse(raw_link_header.to_s)
|
60
|
+
link_header.links.each do |link|
|
61
|
+
add_link(link.href, link['rel'], link['rel'])
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
class LinkSet
|
68
|
+
extend Forwardable
|
69
|
+
include Enumerable
|
70
|
+
fattr(:links) { Set.new }
|
71
|
+
|
72
|
+
def_delegators :links, :<<, :push, :size, :length, :empty?, :each,
|
73
|
+
:initialize
|
74
|
+
|
75
|
+
# Match links on rel or title
|
76
|
+
def [](key)
|
77
|
+
self.class.new(select(&link_matcher(key)))
|
78
|
+
end
|
79
|
+
|
80
|
+
# Matches only one link
|
81
|
+
def at(key, &fallback)
|
82
|
+
fallback ||= -> do raise KeyError, "No link matches #{key.inspect}" end
|
83
|
+
detect(fallback, &link_matcher(key))
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def link_matcher(key)
|
89
|
+
->(link) {
|
90
|
+
key === link.rel ||
|
91
|
+
key === link.title ||
|
92
|
+
link.aliases.any?{|a| key === a}
|
93
|
+
}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def __link_helper_module__
|
98
|
+
@__link_helper_module__ ||=
|
99
|
+
begin
|
100
|
+
mod = Module.new
|
101
|
+
self.extend(mod)
|
102
|
+
mod
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def define_link_helper(name)
|
107
|
+
__link_helper_module__.module_eval do
|
108
|
+
define_method(name) do |*args|
|
109
|
+
link(name).follow(*args) do |result|
|
110
|
+
return result
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def define_link_set_helper(rel, name)
|
117
|
+
__link_helper_module__.module_eval do
|
118
|
+
define_method(name) do |key, *args|
|
119
|
+
links(rel).at(key).follow(*args) do |result|
|
120
|
+
return result
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Leadlight
|
2
|
+
class Link
|
3
|
+
HTTP_METHODS = [
|
4
|
+
:options, :head, :get, :get_representation!, :post, :put, :delete, :patch
|
5
|
+
]
|
6
|
+
|
7
|
+
attr_reader :service
|
8
|
+
attr_reader :rel
|
9
|
+
attr_reader :rev
|
10
|
+
attr_reader :title
|
11
|
+
attr_reader :href
|
12
|
+
attr_reader :aliases
|
13
|
+
|
14
|
+
def initialize(service, href, rel=nil, title=rel, options={})
|
15
|
+
@service = service
|
16
|
+
@href = Addressable::URI.parse(href)
|
17
|
+
@rel = rel.to_s
|
18
|
+
@title = title.to_s
|
19
|
+
@rev = options[:rev]
|
20
|
+
@aliases = Array(options[:aliases])
|
21
|
+
end
|
22
|
+
|
23
|
+
[:options, :head, :get, :get_representation!, :post, :put, :delete, :patch].each do |name|
|
24
|
+
define_method(name) do |*args, &block|
|
25
|
+
service.public_send(name, href, *args, &block)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def follow(*args)
|
30
|
+
get_representation!(*args) do |representation|
|
31
|
+
return representation
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'leadlight/link'
|
2
|
+
require 'addressable/template'
|
3
|
+
|
4
|
+
module Leadlight
|
5
|
+
class LinkTemplate < Link
|
6
|
+
|
7
|
+
def href_template
|
8
|
+
@href_template ||= Addressable::Template.new(href.to_s)
|
9
|
+
end
|
10
|
+
|
11
|
+
HTTP_METHODS.each do |name|
|
12
|
+
define_method(name) do |*args, &block|
|
13
|
+
expanded_href = expand(args)
|
14
|
+
service.public_send(name, expanded_href, *args, &block)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def expand(args)
|
19
|
+
mapping = args.last.is_a?(Hash) ? args.pop : {}
|
20
|
+
mapping = mapping.inject({}) { |result, (k,v)| result.merge!(k.to_s => v) }
|
21
|
+
mapping = href_template.variables.inject(mapping) do |mapping, var|
|
22
|
+
mapping.merge!(var => args.shift) unless args.empty?
|
23
|
+
mapping
|
24
|
+
end
|
25
|
+
extra_keys = (mapping.keys.map(&:to_s) - href_template.variables)
|
26
|
+
extra_params = extra_keys.inject({}) do |params, key|
|
27
|
+
params[key] = mapping.delete(key)
|
28
|
+
params
|
29
|
+
end
|
30
|
+
assert_all_variables_mapped(href_template, mapping)
|
31
|
+
args.push extra_params unless extra_params.empty?
|
32
|
+
href_template.expand(mapping).to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def assert_all_variables_mapped(template, mapping)
|
38
|
+
supplied_keys = mapping.keys.map(&:to_s)
|
39
|
+
needed_keys = template.variables
|
40
|
+
missing_keys = needed_keys - supplied_keys
|
41
|
+
if !missing_keys.empty?
|
42
|
+
raise ArgumentError,
|
43
|
+
"Missing URI template parameters: #{missing_keys.inspect}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'addressable/uri'
|
2
|
+
require 'leadlight/link'
|
3
|
+
|
4
|
+
module Leadlight
|
5
|
+
module Representation
|
6
|
+
attr_accessor :__service__
|
7
|
+
attr_accessor :__location__
|
8
|
+
attr_accessor :__response__
|
9
|
+
attr_accessor :__type__
|
10
|
+
|
11
|
+
def initialize_representation(service, location, response)
|
12
|
+
self.__service__ = service
|
13
|
+
self.__location__ = location
|
14
|
+
self.__response__ = response
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def apply_all_tints
|
19
|
+
__service__.tints.inject(self, &:extend)
|
20
|
+
__apply_tint__
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def __apply_tint__
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'monitor'
|
2
|
+
require 'fattr'
|
3
|
+
require 'hookr'
|
4
|
+
require 'leadlight/errors'
|
5
|
+
|
6
|
+
module Leadlight
|
7
|
+
class Request
|
8
|
+
include HookR::Hooks
|
9
|
+
include MonitorMixin
|
10
|
+
|
11
|
+
fattr(:http_method)
|
12
|
+
fattr(:url)
|
13
|
+
fattr(:connection)
|
14
|
+
fattr(:body)
|
15
|
+
fattr(:params)
|
16
|
+
|
17
|
+
attr_reader :response
|
18
|
+
|
19
|
+
define_hook :on_prepare_request, :request
|
20
|
+
define_hook :on_complete, :response
|
21
|
+
|
22
|
+
def initialize(connection, url, method, params={}, body=nil)
|
23
|
+
self.connection = connection
|
24
|
+
self.url = url
|
25
|
+
self.http_method = method
|
26
|
+
self.body = body
|
27
|
+
self.params = params
|
28
|
+
@completed = new_cond
|
29
|
+
@state = :initialized
|
30
|
+
@env = nil
|
31
|
+
@response = nil
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
def completed?
|
36
|
+
:completed == @state
|
37
|
+
end
|
38
|
+
|
39
|
+
def submit
|
40
|
+
connection.run_request(http_method, url, body, {}) do |request|
|
41
|
+
execute_hook(:on_prepare_request, request)
|
42
|
+
end.on_complete do |env|
|
43
|
+
synchronize do
|
44
|
+
@response = Faraday::Response.new(env)
|
45
|
+
execute_hook :on_complete, @response
|
46
|
+
@env = env
|
47
|
+
@state = :completed
|
48
|
+
@completed.broadcast
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def wait
|
54
|
+
synchronize do
|
55
|
+
@completed.wait_until{completed?}
|
56
|
+
end
|
57
|
+
yield(@env.fetch(:leadlight_representation)) if block_given?
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
def submit_and_wait(&block)
|
62
|
+
submit
|
63
|
+
wait(&block)
|
64
|
+
end
|
65
|
+
alias_method :then, :submit_and_wait
|
66
|
+
|
67
|
+
def raise_on_error
|
68
|
+
on_or_after_complete do |response|
|
69
|
+
case response.status.to_i
|
70
|
+
when 404
|
71
|
+
raise ResourceNotFound, response
|
72
|
+
when (400..499)
|
73
|
+
raise ClientError, response
|
74
|
+
when (500..599)
|
75
|
+
raise ServerError, response
|
76
|
+
end
|
77
|
+
end
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
def on_or_after_complete(&block)
|
82
|
+
synchronize do
|
83
|
+
if completed?
|
84
|
+
block.call(response)
|
85
|
+
else
|
86
|
+
on_complete(&block)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|