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