leadlight 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/Gemfile.lock +5 -7
  2. data/README.md +82 -0
  3. data/default.gems +1 -0
  4. data/leadlight.gemspec +16 -7
  5. data/lib/leadlight.rb +18 -62
  6. data/lib/leadlight/basic_converter.rb +18 -0
  7. data/lib/leadlight/codec.rb +2 -0
  8. data/lib/leadlight/entity.rb +1 -0
  9. data/lib/leadlight/errors.rb +10 -2
  10. data/lib/leadlight/header_helpers.rb +11 -0
  11. data/lib/leadlight/hyperlinkable.rb +1 -1
  12. data/lib/leadlight/representation.rb +19 -1
  13. data/lib/leadlight/request.rb +44 -11
  14. data/lib/leadlight/service.rb +4 -9
  15. data/lib/leadlight/service_class_methods.rb +69 -0
  16. data/lib/leadlight/service_middleware.rb +2 -14
  17. data/lib/leadlight/tint.rb +6 -2
  18. data/lib/leadlight/tint_helper.rb +27 -4
  19. data/lib/leadlight/type_map.rb +101 -0
  20. data/spec/cassettes/Leadlight/authorized_GitHub_example/_user/has_the_expected_content.yml +8 -8
  21. data/spec/cassettes/Leadlight/authorized_GitHub_example/_user/indicates_the_expected_oath_scopes.yml +8 -8
  22. data/spec/cassettes/Leadlight/authorized_GitHub_example/adding_and_removing_team_members.yml +273 -284
  23. data/spec/cassettes/Leadlight/authorized_GitHub_example/{adding_and_removing_team_members/.yml → adding_and_removing_teams.yml} +57 -84
  24. data/spec/cassettes/Leadlight/authorized_GitHub_example/test_team/.yml +111 -117
  25. data/spec/cassettes/Leadlight/basic_GitHub_example/_root/.yml +58 -25
  26. data/spec/cassettes/Leadlight/basic_GitHub_example/_root/__location__/.yml +58 -25
  27. data/spec/cassettes/Leadlight/basic_GitHub_example/_root/should_be_a_204_no_content.yml +58 -25
  28. data/spec/cassettes/Leadlight/tinted_GitHub_example/_root/.yml +58 -25
  29. data/spec/cassettes/Leadlight/tinted_GitHub_example/_root/__location__/.yml +58 -25
  30. data/spec/cassettes/Leadlight/tinted_GitHub_example/_root/should_be_a_204_no_content.yml +58 -25
  31. data/spec/cassettes/Leadlight/tinted_GitHub_example/_user/has_the_expected_content.yml +122 -50
  32. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/.yml +190 -77
  33. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_be_able_to_follow_next_link.yml +260 -104
  34. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_be_enumerable.yml +616 -212
  35. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_be_enumerable_over_page_boundaries.yml +331 -131
  36. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_have_next_and_last_links.yml +190 -77
  37. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_link/exists.yml +58 -25
  38. data/spec/cassettes/Leadlight/tinted_GitHub_example/user_link/links_to_the_expected_URL.yml +58 -25
  39. data/spec/leadlight/hyperlinkable_spec.rb +3 -1
  40. data/spec/leadlight/link_template_spec.rb +2 -1
  41. data/spec/leadlight/request_spec.rb +44 -21
  42. data/spec/leadlight/service_middleware_spec.rb +9 -46
  43. data/spec/leadlight/service_spec.rb +30 -24
  44. data/spec/leadlight/tint_helper_spec.rb +67 -1
  45. data/spec/leadlight/tint_spec.rb +69 -0
  46. data/spec/leadlight/type_map_spec.rb +127 -0
  47. data/spec/leadlight_spec.rb +102 -51
  48. data/spec/support/credentials.rb +10 -2
  49. data/spec/support/vcr.rb +1 -1
  50. metadata +61 -32
  51. data/lib/leadlight/type.rb +0 -71
  52. data/spec/leadlight/type_spec.rb +0 -137
data/Gemfile.lock CHANGED
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- leadlight (0.0.1)
4
+ leadlight (0.0.2)
5
+ addressable
5
6
  faraday
6
7
  fattr
7
8
  hookr
@@ -20,7 +21,7 @@ GEM
20
21
  faraday (0.7.5)
21
22
  addressable (~> 2.2.6)
22
23
  multipart-post (~> 1.1.3)
23
- rack (< 2, >= 1.1.0)
24
+ rack (>= 1.1.0, < 2)
24
25
  fattr (2.2.0)
25
26
  ffi (1.0.11)
26
27
  guard (0.10.0)
@@ -33,7 +34,6 @@ GEM
33
34
  guard (>= 0.10.0)
34
35
  hookr (1.1.1)
35
36
  fail-fast (= 1.0.0)
36
- libnotify (0.7.1)
37
37
  linecache19 (0.5.12)
38
38
  ruby_core_source (>= 0.1.4)
39
39
  link_header (0.0.5)
@@ -41,8 +41,7 @@ GEM
41
41
  multi_json (1.0.4)
42
42
  multipart-post (1.1.4)
43
43
  rack (1.4.0)
44
- rb-inotify (0.8.8)
45
- ffi (>= 0.5.0)
44
+ rake (0.9.2.2)
46
45
  rspec (2.7.0)
47
46
  rspec-core (~> 2.7.0)
48
47
  rspec-expectations (~> 2.7.0)
@@ -72,8 +71,7 @@ DEPENDENCIES
72
71
  guard-bundler
73
72
  guard-rspec
74
73
  leadlight!
75
- libnotify
76
- rb-inotify
74
+ rake
77
75
  rspec
78
76
  ruby-debug19
79
77
  vcr (~> 2.0.0.rc1)
data/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # Leadlight [![Build Status](https://secure.travis-ci.org/avdi/leadlight.png)](http://travis-ci.org/avdi/leadlight)
2
+
3
+ Rose colored stained glass windows for HTTP.
4
+
5
+
6
+ ## Goals
7
+
8
+ ### Progressive enhancement for HTTP APIs
9
+
10
+ Don't cover up the web; just fill in the gaps here and there. Make it easy to
11
+ add links and other affordances API publishers might have forgotten.
12
+
13
+ ### Model RESTful APIs as a web of links
14
+
15
+ Don't try to make the web look like a database.
16
+
17
+ ### Representations over resources
18
+
19
+ Resources are the server's job to worry about. The things we get back from a
20
+ server are representations. Take representations at face value and interpret
21
+ them sensibly, rather than trying to fit them into a client-side model of an
22
+ imaginary server-side object graph.
23
+
24
+ ### Support current and emerging standards
25
+
26
+ Such as the [Link header][], [URI templates][], [PATCH][], [ETags][], and
27
+ [JSON-schema][].
28
+
29
+ ### Sensible defaults
30
+
31
+ Always try to convert representations returned by the server into a form that is
32
+ useful to the programmer--whether that is a Hash parsed from JSON data, a
33
+ Nokogiri document, or a text string.
34
+
35
+ ### Backend agnostic
36
+
37
+ Using the power of [Faraday][].
38
+
39
+ ### Exception-free
40
+
41
+ Only raise exceptions in API calls which explicitly request them. Provide ample
42
+ information to explain the cause of a failure.
43
+
44
+ ### Async-ready
45
+
46
+ Architected from the ground up with asynchrony in mind. It's easier to build a
47
+ synchronous API on top of an async one than vice-versa.
48
+
49
+ ### Controlled abstraction leakage
50
+
51
+ All abstractions are leaky. Provide ample and convenient access points into the
52
+ guts of the request lifecycle for situations when the defaults are not
53
+ sufficient.
54
+
55
+ ### Quality
56
+
57
+ Code quality is important. [Code Climate][] keeps a
58
+ [close eye on Leadlight][leadlight_climate] instilling confidence and showing
59
+ how any technical debt can be paid down.
60
+
61
+
62
+ [link header]: http://tools.ietf.org/html/draft-nottingham-http-link-header
63
+ [uri templates]: http://tools.ietf.org/html/draft-gregorio-uritemplate
64
+ [patch]: http://tools.ietf.org/html/rfc5789
65
+ [etags]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19
66
+ [json-schema]: http://tools.ietf.org/html/draft-zyp-json-schema
67
+ [faraday]: https://github.com/technoweenie/faraday
68
+ [code climate]: https://codeclimate.com
69
+ [leadlight_climate]: https://codeclimate.com/github/avdi/leadlight
70
+
71
+
72
+ ## Installation
73
+
74
+ ```ruby
75
+ gem 'leadlight'
76
+ ```
77
+
78
+ ## Usage
79
+
80
+ _See
81
+ [leadlight_spec.rb](https://github.com/avdi/leadlight/blob/master/spec/leadlight_spec.rb)
82
+ for now._
data/default.gems ADDED
@@ -0,0 +1 @@
1
+ bundler -v1.0.21
data/leadlight.gemspec CHANGED
@@ -13,14 +13,14 @@ Gem::Specification.new do |s|
13
13
  ## If your rubyforge_project name is different, then edit it and comment out
14
14
  ## the sub! line in the Rakefile
15
15
  s.name = 'leadlight'
16
- s.version = '0.0.2'
17
- s.date = '2012-01-09'
16
+ s.version = '0.0.3'
17
+ s.date = '2012-02-05'
18
18
  s.rubyforge_project = 'leadlight'
19
19
 
20
20
  ## Make sure your summary is short. The description may be as long
21
21
  ## as you like.
22
- s.summary = "Short description used in Gem listings."
23
- s.description = "Long description. Maybe copied from the README."
22
+ s.summary = "Rose colored stained glass windows for HTTP."
23
+ s.description = "Rose colored stained glass windows for HTTP."
24
24
 
25
25
  ## List the primary authors. If there are a bunch of authors, it's probably
26
26
  ## better to set the email to an email list or something. If you don't have
@@ -41,6 +41,7 @@ Gem::Specification.new do |s|
41
41
  ## List your runtime dependencies here. Runtime dependencies are those
42
42
  ## that are needed for an end user to actually USE your code.
43
43
  ## s.add_dependency('DEPNAME', [">= 1.1.0", "< 2.0.0"])
44
+ s.add_dependency 'addressable'
44
45
  s.add_dependency 'faraday'
45
46
  s.add_dependency 'fattr'
46
47
  s.add_dependency 'link_header'
@@ -51,6 +52,7 @@ Gem::Specification.new do |s|
51
52
  ## List your development dependencies here. Development dependencies are
52
53
  ## those that are only needed during development
53
54
  ## s.add_development_dependency('DEVDEPNAME', [">= 1.1.0", "< 2.0.0"])
55
+ s.add_development_dependency 'rake'
54
56
  s.add_development_dependency 'rspec'
55
57
  s.add_development_dependency 'vcr', '~> 2.0.0.rc1'
56
58
  s.add_development_dependency 'guard'
@@ -66,27 +68,33 @@ Gem::Specification.new do |s|
66
68
  Gemfile
67
69
  Gemfile.lock
68
70
  Guardfile
71
+ README.md
69
72
  Rakefile
73
+ default.gems
70
74
  leadlight.gemspec
71
75
  lib/leadlight.rb
76
+ lib/leadlight/basic_converter.rb
72
77
  lib/leadlight/blank.rb
73
78
  lib/leadlight/codec.rb
79
+ lib/leadlight/entity.rb
74
80
  lib/leadlight/enumerable_representation.rb
75
81
  lib/leadlight/errors.rb
82
+ lib/leadlight/header_helpers.rb
76
83
  lib/leadlight/hyperlinkable.rb
77
84
  lib/leadlight/link.rb
78
85
  lib/leadlight/link_template.rb
79
86
  lib/leadlight/representation.rb
80
87
  lib/leadlight/request.rb
81
88
  lib/leadlight/service.rb
89
+ lib/leadlight/service_class_methods.rb
82
90
  lib/leadlight/service_middleware.rb
83
91
  lib/leadlight/tint.rb
84
92
  lib/leadlight/tint_helper.rb
85
- lib/leadlight/type.rb
93
+ lib/leadlight/type_map.rb
86
94
  spec/cassettes/Leadlight/authorized_GitHub_example/_user/has_the_expected_content.yml
87
95
  spec/cassettes/Leadlight/authorized_GitHub_example/_user/indicates_the_expected_oath_scopes.yml
88
96
  spec/cassettes/Leadlight/authorized_GitHub_example/adding_and_removing_team_members.yml
89
- spec/cassettes/Leadlight/authorized_GitHub_example/adding_and_removing_team_members/.yml
97
+ spec/cassettes/Leadlight/authorized_GitHub_example/adding_and_removing_teams.yml
90
98
  spec/cassettes/Leadlight/authorized_GitHub_example/test_team/.yml
91
99
  spec/cassettes/Leadlight/basic_GitHub_example/_root/.yml
92
100
  spec/cassettes/Leadlight/basic_GitHub_example/_root/__location__/.yml
@@ -111,7 +119,8 @@ Gem::Specification.new do |s|
111
119
  spec/leadlight/service_middleware_spec.rb
112
120
  spec/leadlight/service_spec.rb
113
121
  spec/leadlight/tint_helper_spec.rb
114
- spec/leadlight/type_spec.rb
122
+ spec/leadlight/tint_spec.rb
123
+ spec/leadlight/type_map_spec.rb
115
124
  spec/leadlight_spec.rb
116
125
  spec/spec_helper_lite.rb
117
126
  spec/support/credentials.rb
data/lib/leadlight.rb CHANGED
@@ -1,27 +1,37 @@
1
1
  require 'faraday'
2
2
  require 'fattr'
3
3
  require 'logger'
4
+ require 'hookr'
4
5
  require 'leadlight/errors'
5
6
  require 'leadlight/link'
6
7
  require 'leadlight/hyperlinkable'
7
8
  require 'leadlight/service_middleware'
8
9
  require 'leadlight/representation'
9
10
  require 'leadlight/tint'
10
- require 'leadlight/type'
11
11
  require 'leadlight/service'
12
+ require 'leadlight/service_class_methods'
12
13
  require 'leadlight/enumerable_representation'
14
+ require 'leadlight/basic_converter'
13
15
 
14
16
 
15
17
  module Leadlight
16
18
 
17
- VERSION = '0.0.2'
18
-
19
- def self.build_service(target, &block)
20
- target.module_eval do
21
- extend ServiceClassMethods
22
- include Service
19
+ VERSION = '0.0.3'
20
+
21
+ def self.build_service(target=Class.new, &block)
22
+ target.tap do
23
+ target.module_eval do
24
+ extend ServiceClassMethods
25
+ include Service
26
+ include HookR::Hooks
27
+ extend SingleForwardable
28
+
29
+ request_events = request_class.hooks.map(&:name)
30
+ def_delegators :request_class, *request_events
31
+ define_hook :on_init, :service
32
+ end
33
+ target.module_eval(&block)
23
34
  end
24
- target.module_eval(&block)
25
35
  end
26
36
 
27
37
  def self.build_connection_common(&common_connection_stack)
@@ -34,59 +44,5 @@ module Leadlight
34
44
  }
35
45
  end
36
46
 
37
- module ServiceClassMethods
38
- fattr(:tints) { default_tints }
39
- fattr(:types) { [] }
40
-
41
- def url(new_url=:none)
42
- if new_url == :none
43
- @url ||= Addressable::URI.parse('http://example.com')
44
- else
45
- @url = Addressable::URI.parse(new_url)
46
- end
47
- end
48
-
49
- def session(options={})
50
- sessions[options]
51
- end
52
-
53
- def sessions
54
- @sessions ||= Hash.new{|h,k|
55
- h[k] = new(k)
56
- }
57
- end
58
-
59
- def connection_stack
60
- @connection_stack ||= ->(builder){}
61
- end
62
-
63
- def default_tints
64
- [
65
- EnumerableRepresentation::Tint
66
- ]
67
- end
68
-
69
- private
70
-
71
- def tint(name, &block)
72
- self.tints << Tint.new(name, &block)
73
- end
74
-
75
- def type(name, &block)
76
- self.types << Type.new(name, self, &block)
77
- end
78
-
79
- def type_for_name(name)
80
- raise_on_missing = -> do
81
- raise KeyError, "Type not found: #{name}"
82
- end
83
- types.detect(raise_on_missing){|type| type.name.to_s == name.to_s}
84
- end
85
-
86
- def build_connection(&block)
87
- @connection_stack = block
88
- end
89
-
90
- end
91
47
 
92
48
  end
@@ -0,0 +1,18 @@
1
+ module Leadlight
2
+ module BasicConverter
3
+ fattr(:codec) { Codec.new }
4
+
5
+ def initialize(codec)
6
+ @codec = codec
7
+ end
8
+
9
+ def decode_with_type(content_type, entity_body, options={})
10
+ codec.decode(content_type, entity_body, options)
11
+ end
12
+
13
+ def encode_with_type(content_type, object, options={})
14
+ body = codec.encode(content_type, object, options)
15
+ Entity.new(content_type, body)
16
+ end
17
+ end
18
+ end
@@ -1,3 +1,5 @@
1
+ require 'multi_json'
2
+
1
3
  module Leadlight
2
4
  class Codec
3
5
  Strategy ||= Struct.new(:name, :encoder, :decoder, :patterns)
@@ -0,0 +1 @@
1
+ Entity ||= Struct.new(:content_type, :body)
@@ -1,14 +1,22 @@
1
+ require 'forwardable'
2
+
1
3
  module Leadlight
2
4
  class Error < StandardError; end
3
5
  class CredentialsRequiredError < Error; end
4
6
  class HttpError < Error
7
+ extend Forwardable
8
+
5
9
  attr_reader :response
6
- def initialize(response)
10
+
11
+ def_delegator :response, :status
12
+
13
+ def initialize(response, message=response.status.to_s)
7
14
  @response = response
8
- super("HTTP Error #{response.status}")
15
+ super(message)
9
16
  end
10
17
  end
11
18
  class ClientError < HttpError; end
12
19
  class ResourceNotFound < ClientError; end
13
20
  class ServerError < HttpError; end
21
+ class TypeError < Error; end
14
22
  end
@@ -0,0 +1,11 @@
1
+ module Leadlight
2
+ module HeaderHelpers
3
+ def clean_content_type(content_type)
4
+ unless content_type.nil?
5
+ mimetype = MIME::Type.new(content_type)
6
+ content_type = "#{mimetype.media_type}/#{mimetype.sub_type}"
7
+ end
8
+ content_type
9
+ end
10
+ end
11
+ end
@@ -10,7 +10,7 @@ module Leadlight
10
10
  module Hyperlinkable
11
11
  def self.extended(representation)
12
12
  super(representation)
13
- representation.add_link(representation.__response__.env[:url],
13
+ representation.add_link(representation.__location__,
14
14
  'self', 'self', rev: 'self')
15
15
  representation.add_links_from_headers
16
16
  end
@@ -1,12 +1,12 @@
1
1
  require 'addressable/uri'
2
2
  require 'leadlight/link'
3
+ require 'leadlight/errors'
3
4
 
4
5
  module Leadlight
5
6
  module Representation
6
7
  attr_accessor :__service__
7
8
  attr_accessor :__location__
8
9
  attr_accessor :__response__
9
- attr_accessor :__type__
10
10
 
11
11
  def initialize_representation(service, location, response)
12
12
  self.__service__ = service
@@ -21,6 +21,24 @@ module Leadlight
21
21
  self
22
22
  end
23
23
 
24
+ def exception(message=exception_message)
25
+ return super if defined?(super)
26
+ case __response__.status.to_i
27
+ when 404 then ResourceNotFound
28
+ when (400..499) then ClientError
29
+ when (500..599) then ServerError
30
+ end.new(__response__, exception_message)
31
+ end
32
+
33
+ def exception_message
34
+ http_status_message
35
+ end
36
+
37
+ def http_status_message
38
+ __response__.env.fetch(:response_headers).fetch('status'){
39
+ status.to_s
40
+ }
41
+ end
24
42
 
25
43
  private
26
44
 
@@ -1,35 +1,49 @@
1
1
  require 'monitor'
2
2
  require 'fattr'
3
+ require 'forwardable'
3
4
  require 'hookr'
4
5
  require 'leadlight/errors'
6
+ require 'leadlight/blank'
7
+ require 'leadlight/hyperlinkable'
8
+ require 'leadlight/representation'
9
+ require 'leadlight/type_map'
10
+ require 'leadlight/header_helpers'
5
11
 
6
12
  module Leadlight
7
13
  class Request
8
14
  include HookR::Hooks
9
15
  include MonitorMixin
16
+ extend Forwardable
17
+ include HeaderHelpers
10
18
 
11
19
  fattr(:http_method)
12
20
  fattr(:url)
13
21
  fattr(:connection)
14
22
  fattr(:body)
15
23
  fattr(:params)
24
+ fattr(:service)
25
+ fattr(:codec)
26
+ fattr(:type_map) { service.type_map || TypeMap.new }
16
27
 
17
28
  attr_reader :response
18
29
 
19
30
  define_hook :on_prepare_request, :request
20
31
  define_hook :on_complete, :response
21
32
 
22
- def initialize(connection, url, method, params={}, body=nil)
33
+ def_delegator :service, :service_options
34
+
35
+ def initialize(service, connection, url, method, params={}, body=nil)
23
36
  self.connection = connection
24
37
  self.url = url
25
38
  self.http_method = method
26
39
  self.body = body
27
40
  self.params = params
41
+ self.service = service
28
42
  @completed = new_cond
29
43
  @state = :initialized
30
44
  @env = nil
31
45
  @response = nil
32
- super
46
+ super()
33
47
  end
34
48
 
35
49
  def completed?
@@ -37,11 +51,16 @@ module Leadlight
37
51
  end
38
52
 
39
53
  def submit
40
- connection.run_request(http_method, url, body, {}) do |request|
54
+ entity = type_map.to_entity_body(body)
55
+ entity_body = entity.body
56
+ content_type = entity.content_type
57
+ connection.run_request(http_method, url, entity_body, {}) do |request|
58
+ request.headers['Content-Type'] = content_type if content_type
59
+ request.options[:leadlight_request] = self
41
60
  execute_hook(:on_prepare_request, request)
42
61
  end.on_complete do |env|
43
62
  synchronize do
44
- @response = Faraday::Response.new(env)
63
+ @response = env.fetch(:response)
45
64
  execute_hook :on_complete, @response
46
65
  @env = env
47
66
  @state = :completed
@@ -66,13 +85,8 @@ module Leadlight
66
85
 
67
86
  def raise_on_error
68
87
  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
88
+ unless response.success?
89
+ raise response.env.fetch(:leadlight_representation)
76
90
  end
77
91
  end
78
92
  self
@@ -87,5 +101,24 @@ module Leadlight
87
101
  end
88
102
  end
89
103
  end
104
+
105
+ def represent(env)
106
+ content_type = env[:response_headers]['Content-Type']
107
+ content_type = clean_content_type(content_type)
108
+ representation = type_map.to_native(content_type, env[:body])
109
+ location = Addressable::URI.parse(env[:response_headers].fetch('location'){ env[:url] })
110
+ representation.
111
+ extend(Representation).
112
+ initialize_representation(env[:leadlight_service], location, env[:response]).
113
+ extend(Hyperlinkable).
114
+ apply_all_tints
115
+ end
116
+
117
+ private
118
+
119
+ def representation
120
+ raise "No representation until complete" unless completed?
121
+ @env.fetch(:leadlight_representation)
122
+ end
90
123
  end
91
124
  end