leadlight 0.0.2 → 0.0.3

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