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.
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