leadlight 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/Gemfile +9 -0
  2. data/Gemfile.lock +79 -0
  3. data/Guardfile +19 -0
  4. data/Rakefile +133 -0
  5. data/leadlight.gemspec +125 -0
  6. data/lib/leadlight.rb +92 -0
  7. data/lib/leadlight/blank.rb +15 -0
  8. data/lib/leadlight/codec.rb +63 -0
  9. data/lib/leadlight/enumerable_representation.rb +24 -0
  10. data/lib/leadlight/errors.rb +14 -0
  11. data/lib/leadlight/hyperlinkable.rb +126 -0
  12. data/lib/leadlight/link.rb +35 -0
  13. data/lib/leadlight/link_template.rb +47 -0
  14. data/lib/leadlight/representation.rb +30 -0
  15. data/lib/leadlight/request.rb +91 -0
  16. data/lib/leadlight/service.rb +73 -0
  17. data/lib/leadlight/service_middleware.rb +50 -0
  18. data/lib/leadlight/tint.rb +26 -0
  19. data/lib/leadlight/tint_helper.rb +67 -0
  20. data/lib/leadlight/type.rb +71 -0
  21. data/spec/cassettes/Leadlight/authorized_GitHub_example/_user/has_the_expected_content.yml +75 -0
  22. data/spec/cassettes/Leadlight/authorized_GitHub_example/_user/indicates_the_expected_oath_scopes.yml +75 -0
  23. data/spec/cassettes/Leadlight/authorized_GitHub_example/adding_and_removing_team_members.yml +384 -0
  24. data/spec/cassettes/Leadlight/authorized_GitHub_example/adding_and_removing_team_members/.yml +309 -0
  25. data/spec/cassettes/Leadlight/authorized_GitHub_example/test_team/.yml +159 -0
  26. data/spec/cassettes/Leadlight/basic_GitHub_example/_root/.yml +32 -0
  27. data/spec/cassettes/Leadlight/basic_GitHub_example/_root/__location__/.yml +32 -0
  28. data/spec/cassettes/Leadlight/basic_GitHub_example/_root/should_be_a_204_no_content.yml +32 -0
  29. data/spec/cassettes/Leadlight/tinted_GitHub_example/_root/.yml +32 -0
  30. data/spec/cassettes/Leadlight/tinted_GitHub_example/_root/__location__/.yml +32 -0
  31. data/spec/cassettes/Leadlight/tinted_GitHub_example/_root/should_be_a_204_no_content.yml +32 -0
  32. data/spec/cassettes/Leadlight/tinted_GitHub_example/_user/has_the_expected_content.yml +65 -0
  33. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/.yml +100 -0
  34. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_be_able_to_follow_next_link.yml +135 -0
  35. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_be_enumerable.yml +275 -0
  36. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_be_enumerable_over_page_boundaries.yml +170 -0
  37. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_have_next_and_last_links.yml +100 -0
  38. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_link/exists.yml +32 -0
  39. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_link/links_to_the_expected_URL.yml +32 -0
  40. data/spec/leadlight/codec_spec.rb +93 -0
  41. data/spec/leadlight/hyperlinkable_spec.rb +136 -0
  42. data/spec/leadlight/link_spec.rb +53 -0
  43. data/spec/leadlight/link_template_spec.rb +45 -0
  44. data/spec/leadlight/representation_spec.rb +62 -0
  45. data/spec/leadlight/request_spec.rb +236 -0
  46. data/spec/leadlight/service_middleware_spec.rb +81 -0
  47. data/spec/leadlight/service_spec.rb +127 -0
  48. data/spec/leadlight/tint_helper_spec.rb +132 -0
  49. data/spec/leadlight/type_spec.rb +137 -0
  50. data/spec/leadlight_spec.rb +237 -0
  51. data/spec/spec_helper_lite.rb +6 -0
  52. data/spec/support/credentials.rb +16 -0
  53. data/spec/support/vcr.rb +18 -0
  54. metadata +229 -0
@@ -0,0 +1,15 @@
1
+ module Leadlight
2
+ class Blank
3
+ def blank?
4
+ true
5
+ end
6
+
7
+ def empty?
8
+ true
9
+ end
10
+
11
+ def present?
12
+ false
13
+ end
14
+ end
15
+ end
@@ -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