restless_router 0.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c6e52a3b8548ac48bdc13f601d9a83b1d61ecd4d
4
+ data.tar.gz: feaafcf86072c33421c0b69778e594be7c98cba4
5
+ SHA512:
6
+ metadata.gz: 6499767d0da1d50c4224d3ac1908ab37750da49fa26c44c33a8085e6e2a24a32944490903e168040f02881163e9a415f6429720442f7ae1bfd12ef254cbbeee3
7
+ data.tar.gz: 595d490b7212e0d5ce5b774b6c790d2dcb2c278ab34c24faa980b4f9d24b01d4666d299c8897ed4dfd9a4eaa65167ae837fffdfa0f9ef802a725c529c57a837a
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ /.ruby-gemset
2
+ /.ruby-version
3
+ *.gem
4
+ *.rbc
5
+ .bundle
6
+ .config
7
+ .yardoc
8
+ Gemfile.lock
9
+ InstalledFiles
10
+ _yardoc
11
+ coverage
12
+ doc/
13
+ lib/bundler/man
14
+ pkg
15
+ rdoc
16
+ spec/reports
17
+ test/tmp
18
+ test/version_tmp
19
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in restless_router.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Nate Klaiber
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # RestlessRouter
2
+
3
+ This helps fill the gap where web services only provide their routing
4
+ via external documentation. In order to prevent URL building scattered
5
+ throughout your client, you can define the routes up-front via fully
6
+ qualified URIs or [URI Templates](http://tools.ietf.org/html/rfc6570).
7
+
8
+ You can then reference a URL by looking it up by it's _link
9
+ relationship_.
10
+
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ gem 'restless_router'
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install restless_router
25
+
26
+ ## Usage
27
+
28
+ The first step is to **define the possible routes that a service may
29
+ utilize**. In most cases they can be found in their online documentation
30
+ of the service.
31
+
32
+ ```ruby
33
+ require 'restless_router'
34
+
35
+ routes = RestlessRouter::Routes.new
36
+
37
+ # Add a fully qualified URI
38
+ routes.add_route(RestlessRouter::Route.new('directory', 'https://example.com/directory')
39
+
40
+ # Add a URI Templated
41
+ routes.add_route(RestlessRouter::Route.new('http://example.com/rels/user-detail', 'https://example.com/users/{id}', templated: true)
42
+ ```
43
+
44
+ > You may also use the `<<` operator to add routes to the collection.
45
+
46
+ Once the routes have been defined, you may **lookup the routes** by their
47
+ [IANA Link
48
+ Relationship](http://www.iana.org/assignments/link-relations/link-relations.xhtml)
49
+ or _Custom Link Relationships_.
50
+
51
+ ```ruby
52
+
53
+ # Look up the Directory route
54
+ directory_route = routes.route_for('directory')
55
+ directory_url = directory_route.url_for
56
+ # => 'https://example.com/directory'
57
+
58
+ # Look up the User Detail route
59
+ user_detail_route = routes.route_for('http://example.com/rels/user-detail')
60
+ user_defail_url = user_detail_route.url_for(id: '1234')
61
+ # => 'https://example.com/users/1234'
62
+ ```
63
+
64
+ This can then be utilized as you see fit with your `HTTP` adapter.
65
+
66
+ ```ruby
67
+ require 'faraday'
68
+ require 'restless_router'
69
+
70
+ # Routes are defined in the core application
71
+ class Application
72
+ def self.routes
73
+ # Include route definitions here
74
+ end
75
+ end
76
+
77
+ # We can then reference the routes
78
+ directory_route = Application.routes.route_for('directory')
79
+ directory_url = directory_route.url_for
80
+
81
+ # And make a request
82
+ directory_request = Faraday.get(directory_url)
83
+ ```
84
+
85
+ ## Approach
86
+
87
+ * There is a `Routes` collection that holds the route definitions.
88
+ * There is a `Route` object that holds the details of the route definition.
89
+ * There are mechanisms to _find_ the route, and _expand_ the route if necessary.
90
+
91
+ Some APIs may provide hypermedia envelopes and you should use those where
92
+ available.
93
+
94
+ ## Contributing
95
+
96
+ 1. Fork it ( http://github.com/<my-github-username>/restless_router/fork )
97
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
98
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
99
+ 4. Push to the branch (`git push origin my-new-feature`)
100
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,89 @@
1
+ require 'addressable/template'
2
+
3
+ module RestlessRouter
4
+ class Route
5
+ include Comparable
6
+
7
+ # Create a new Route that can be used
8
+ # to issue requests against.
9
+ #
10
+ # @example
11
+ # # With a fully qualified URI
12
+ # route = RestlessRouter::Route.new('home', 'http://example.com')
13
+ #
14
+ # route.name
15
+ # # => 'home'
16
+ #
17
+ # route.url_for
18
+ # # => 'http://example.com'
19
+ #
20
+ # # With a templated route
21
+ # route = RestlessRouter::Route.new('search', 'http://example.com/search{?q}', templated: true)
22
+ #
23
+ # route.name
24
+ # # => 'search'
25
+ #
26
+ # route.url_for(q: 'search-term')
27
+ # # => 'http://example.com/search?q=search-term'
28
+ #
29
+ def initialize(name, path, options={})
30
+ @name = name
31
+ @path = path
32
+
33
+ @options = default_options.merge(options)
34
+ end
35
+
36
+ # Define the spaceship operator for use with Comparable
37
+ #
38
+ def <=>(other)
39
+ self.name <=> other.name
40
+ end
41
+
42
+ # Return the name of the Route. This is either
43
+ # the IANA or custom link relationship.
44
+ #
45
+ # @return [String] Name of the route
46
+ def name
47
+ @name
48
+ end
49
+
50
+ # Returns the URL for the route. If it's templated,
51
+ # then we utilize the provided options hash to expand
52
+ # the route. Otherwise we return the `path`
53
+ #
54
+ # @return [String] The templated or base URI
55
+ def url_for(options={})
56
+ if templated?
57
+ template = Addressable::Template.new(base_path)
58
+ template = template.expand(options)
59
+ template.to_s
60
+ else
61
+ base_path
62
+ end
63
+ end
64
+
65
+ private
66
+ # Provide a set of default options. Currently
67
+ # this is used to set `templated` to false.
68
+ def default_options
69
+ {
70
+ :templated => false
71
+ }
72
+ end
73
+
74
+ # Query method to see if the URI path provided
75
+ # is templated
76
+ #
77
+ # @return [Boolean] True if the path is templated
78
+ def templated?
79
+ @options.fetch(:templated)
80
+ end
81
+
82
+ # The base path provided for the route
83
+ #
84
+ # @return [String] The base path
85
+ def base_path
86
+ @path
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,103 @@
1
+ require File.expand_path('../route', __FILE__)
2
+
3
+ module RestlessRouter
4
+ class Routes
5
+ include Enumerable
6
+
7
+ # Error Definitions
8
+ InvalidRouteError = Class.new(StandardError)
9
+ ExistingRouteError = Class.new(StandardError)
10
+ RouteNotFoundError = Class.new(StandardError)
11
+
12
+ # Creates a new instance of a Route collection. This allows
13
+ # us to keep all definitions in a single object and then
14
+ # later retrieve them by their link relationship name.
15
+ #
16
+ # @example
17
+ # routes = RestlessRouter::Routers.new
18
+ # routes.add_route(RestlessRouter::Route.new('home', 'https://example.com/home')
19
+ #
20
+ # routes.route_for('home').url_for
21
+ # # => 'https://example.com/home'
22
+ #
23
+ def initialize
24
+ @routes = []
25
+ end
26
+
27
+ # Define each for use with Enumerable
28
+ #
29
+ def each(&block)
30
+ @routes.each(&block)
31
+ end
32
+
33
+ # Add a new route to the Routes collection
34
+ #
35
+ # @return [Array] Routes collection
36
+ def add_route(route)
37
+ raise InvalidRouteError.new('Route must respond to #url_for') unless valid_route?(route)
38
+ @routes << route unless route_exists?(route)
39
+ end
40
+ alias :<< :add_route
41
+
42
+ # Raise an exception if the route is invalid or already exists
43
+ #
44
+ def add_route!(route)
45
+ # Raise exception if the route is existing, too
46
+ raise InvalidRouteError.new('Route must respond to #url_for') unless valid_route?(route)
47
+ raise ExistingRouteError.new(("Route already exists for %s" % [route.name])) if route_exists?(route)
48
+
49
+ @routes << route
50
+ end
51
+
52
+ # Retrieve a route by it's link relationship name
53
+ #
54
+ # @return [Route, nil] Instance of the route by name or nil
55
+ def route_for(name)
56
+ name = name.to_s
57
+ @routes.select { |entry| entry.name == name }.first
58
+ end
59
+
60
+ # Raise an exception of the route's not found
61
+ #
62
+ #
63
+ def route_for!(name)
64
+ route = route_for(name)
65
+ raise RouteNotFoundError.new(("Route not found for %s" % [name])) if route.nil?
66
+ route
67
+ end
68
+
69
+ # Returns the collection of Route definitions
70
+ #
71
+ # @return [Array] Routes
72
+ def routes
73
+ @routes
74
+ end
75
+
76
+ # Query method to check if any routes have been defined.
77
+ #
78
+ # @return [Boolean] True if there are definitions in the collection
79
+ def routes?
80
+ @routes.any?
81
+ end
82
+
83
+ private
84
+ # Query method to see if the specified route already exists.
85
+ #
86
+ # @return [Boolean] True if we already have an existing route
87
+ def route_exists?(route)
88
+ !!self.route_for(route.name)
89
+ end
90
+
91
+ # Query method to see if the route definition being added is valid.
92
+ #
93
+ # It must respond to:
94
+ #
95
+ # * `name`
96
+ # * `url_for`
97
+ #
98
+ # @return [Boolean] True if the route definition is valid
99
+ def valid_route?(route)
100
+ route.respond_to?(:url_for) && route.respond_to?(:name)
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,3 @@
1
+ module RestlessRouter
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,5 @@
1
+ require "restless_router/version"
2
+ require "restless_router/routes"
3
+
4
+ module RestlessRouter
5
+ end
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'restless_router/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "restless_router"
8
+ spec.version = RestlessRouter::VERSION
9
+ spec.authors = ["Nate Klaiber"]
10
+ spec.email = ["nate@theklaibers.com"]
11
+ spec.summary = %q{Enable simple route definitions for external resources.}
12
+ spec.description = %q{Many web services lack hypermedia or consistent routing. This gives a single place to house routes using URI Templates instead of building URLs throughout the client.}
13
+ spec.homepage = "https://github.com/nateklaiber/restless_router"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency("bundler", "~> 1.5")
22
+ spec.add_development_dependency("rake")
23
+ spec.add_development_dependency("rspec")
24
+ spec.add_development_dependency("yard")
25
+
26
+ spec.add_dependency("addressable")
27
+ end
@@ -0,0 +1,35 @@
1
+ require File.expand_path('../../../lib/restless_router/route', __FILE__)
2
+
3
+ describe RestlessRouter::Route do
4
+ let(:name) { 'home' }
5
+ let(:path) { 'http://www.example.com' }
6
+ let(:options) { {} }
7
+
8
+ subject { described_class.new(name, path, options) }
9
+
10
+ # TODO: Validations on empty `name` or `path`
11
+
12
+ context "with URI" do
13
+ it "returns the #name" do
14
+ expect(subject.name).to eq('home')
15
+ end
16
+
17
+ it "returns the #url_for" do
18
+ expect(subject.url_for).to eq('http://www.example.com')
19
+ end
20
+ end
21
+
22
+ context "with URI Template" do
23
+ let(:name) { 'search' }
24
+ let(:path) { 'http://www.example.com/search{?q}' }
25
+ let(:options) { { templated: true } }
26
+
27
+ it "returns the expanded #url_for" do
28
+ expect(subject.url_for(q: 'search-term')).to eq('http://www.example.com/search?q=search-term')
29
+ end
30
+
31
+ it "returns the expanded #url_for with no expansions" do
32
+ expect(subject.url_for).to eq('http://www.example.com/search')
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,116 @@
1
+ require 'ostruct'
2
+ require File.expand_path('../../../lib/restless_router/routes', __FILE__)
3
+
4
+ describe RestlessRouter::Routes do
5
+ let(:home_route) { RestlessRouter::Route.new('home', 'https://example.com/home') }
6
+ let(:search_route) { RestlessRouter::Route.new('search', 'https://example.com/search{?q}', templated: true) }
7
+ let(:route_definitions) { [home_route, search_route] }
8
+
9
+ subject { described_class.new }
10
+
11
+ before(:each) do
12
+ route_definitions.each { |route| subject.add_route(route) }
13
+ end
14
+
15
+ context "without routes" do
16
+ let(:route_definitions) { [] }
17
+
18
+ it "returns false for #any?" do
19
+ expect(subject.any?).to be_false
20
+ end
21
+
22
+ it "returns 0 for the count" do
23
+ expect(subject.count).to eq(0)
24
+ end
25
+
26
+ context "retrieving a route definition" do
27
+ describe "#route_for" do
28
+ it "returns nil when searching for 'home'" do
29
+ expect(subject.route_for('home')).to be_nil
30
+ end
31
+ end
32
+
33
+ describe "#route_for!" do
34
+ it "raises an exception with searching for 'home'" do
35
+ lambda { subject.route_for!('home') }.should raise_error(RestlessRouter::Routes::RouteNotFoundError)
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ context "with routes" do
42
+ it "returns true for #any?" do
43
+ expect(subject.any?).to be_true
44
+ end
45
+
46
+ it "returns 2 for the count" do
47
+ expect(subject.count).to eq(2)
48
+ end
49
+
50
+
51
+ context "retrieving a route definition" do
52
+ describe "#route_for" do
53
+ it "returns the route specified for 'home'" do
54
+ expect(subject.route_for('home')).to eq(home_route)
55
+ end
56
+
57
+ it "returns the route specified for 'search'" do
58
+ expect(subject.route_for('search')).to eq(search_route)
59
+ end
60
+
61
+ it "returns nil when route specified for 'not-defined'" do
62
+ expect(subject.route_for('not-defined')).to be_nil
63
+ end
64
+ end
65
+
66
+ describe "#route_for!" do
67
+ it "raises an exception with searching for 'not-defined'" do
68
+ lambda { subject.route_for!('not-defined') }.should raise_error(RestlessRouter::Routes::RouteNotFoundError)
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ context "ensure uniqueness by link relationship name" do
75
+ let(:route_definitions) { [] }
76
+
77
+ describe "#add_route" do
78
+ it "does not add a route with the same name" do
79
+ subject.add_route(home_route)
80
+ subject.add_route(home_route)
81
+ expect(subject.count).to eq(1)
82
+ end
83
+ end
84
+
85
+ describe "#add_route!" do
86
+ it "raises an exception if route is specified with the same name" do
87
+ subject.add_route!(home_route)
88
+
89
+ lambda { subject.add_route!(home_route) }.should raise_error(RestlessRouter::Routes::ExistingRouteError)
90
+ end
91
+ end
92
+ end
93
+
94
+ context "adding a new route definition" do
95
+ it "raises an exception if nil is added" do
96
+ lambda { subject.add_route(nil) }.should raise_error(RestlessRouter::Routes::InvalidRouteError)
97
+ end
98
+
99
+ it "raises an error if route definition does not respond to #name" do
100
+ route_definition = OpenStruct.new(url_for: 'http://example.com')
101
+ lambda { subject.add_route(route_definition) }.should raise_error(RestlessRouter::Routes::InvalidRouteError)
102
+ end
103
+
104
+ it "raises an error if route definition does not respond to #url_for" do
105
+ route_definition = OpenStruct.new(name: 'home')
106
+ lambda { subject.add_route(route_definition) }.should raise_error(RestlessRouter::Routes::InvalidRouteError)
107
+ end
108
+
109
+ it "pushes the valid route onto the stack" do
110
+ route_definition = OpenStruct.new(name: 'custom-name', url_for: 'http://example.com')
111
+ subject.add_route(route_definition)
112
+
113
+ expect(subject).to include(route_definition)
114
+ end
115
+ end
116
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: restless_router
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Nate Klaiber
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-05-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.5'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: yard
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: addressable
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Many web services lack hypermedia or consistent routing. This gives a
84
+ single place to house routes using URI Templates instead of building URLs throughout
85
+ the client.
86
+ email:
87
+ - nate@theklaibers.com
88
+ executables: []
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - .gitignore
93
+ - Gemfile
94
+ - LICENSE.txt
95
+ - README.md
96
+ - Rakefile
97
+ - lib/restless_router.rb
98
+ - lib/restless_router/route.rb
99
+ - lib/restless_router/routes.rb
100
+ - lib/restless_router/version.rb
101
+ - restless_router.gemspec
102
+ - spec/unit/route_spec.rb
103
+ - spec/unit/routes_spec.rb
104
+ homepage: https://github.com/nateklaiber/restless_router
105
+ licenses:
106
+ - MIT
107
+ metadata: {}
108
+ post_install_message:
109
+ rdoc_options: []
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - '>='
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - '>='
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ requirements: []
123
+ rubyforge_project:
124
+ rubygems_version: 2.2.0
125
+ signing_key:
126
+ specification_version: 4
127
+ summary: Enable simple route definitions for external resources.
128
+ test_files:
129
+ - spec/unit/route_spec.rb
130
+ - spec/unit/routes_spec.rb
131
+ has_rdoc: