hubert 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 +7 -0
- data/.gitignore +1 -0
- data/.rspec +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +31 -0
- data/LICENCE +21 -0
- data/README.md +65 -0
- data/Rakefile +15 -0
- data/benchmark/template.rb +13 -0
- data/examples/github.rb +46 -0
- data/hubert.gemspec +22 -0
- data/lib/hubert.rb +11 -0
- data/lib/hubert/builder.rb +46 -0
- data/lib/hubert/components.rb +64 -0
- data/lib/hubert/dsl.rb +60 -0
- data/lib/hubert/errors.rb +11 -0
- data/lib/hubert/template.rb +52 -0
- data/lib/hubert/template/context.rb +28 -0
- data/lib/hubert/template/renderer.rb +47 -0
- data/lib/hubert/version.rb +3 -0
- data/spec/lib/hubert/builder_spec.rb +76 -0
- data/spec/lib/hubert/components_spec.rb +108 -0
- data/spec/lib/hubert/dsl_spec.rb +87 -0
- data/spec/lib/hubert/template_spec.rb +119 -0
- data/spec/lib/hubert_spec.rb +1 -0
- data/spec/spec_helper.rb +7 -0
- metadata +119 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 82674970a036232aa45dbf9a07b2ef0f0650628c
|
|
4
|
+
data.tar.gz: a944796e70aa1b5079927533137683daa14c41c8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 46524cde0077bf047a9c0fe4defa29ea67b0b1e652c4da5d3dc375ed662aead1bbcf0952e33c2de7c05cc514efee4923b5d6ba4c262e5fce86396ddf2628f2d6
|
|
7
|
+
data.tar.gz: 5e0c5069c05665ed04dd637c2fb40f1c0f3a268eccd0a0f46644bd1e03bbda4c7dfea79ba23d8d636fcae83ebea03fb72b6a49dbbaef5505333e872d7b531ae4
|
data/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
TODO.txt
|
data/.rspec
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
hubert (0.0.1)
|
|
5
|
+
|
|
6
|
+
GEM
|
|
7
|
+
remote: https://rubygems.org/
|
|
8
|
+
specs:
|
|
9
|
+
diff-lcs (1.2.5)
|
|
10
|
+
rake (10.3.2)
|
|
11
|
+
rspec (3.1.0)
|
|
12
|
+
rspec-core (~> 3.1.0)
|
|
13
|
+
rspec-expectations (~> 3.1.0)
|
|
14
|
+
rspec-mocks (~> 3.1.0)
|
|
15
|
+
rspec-core (3.1.7)
|
|
16
|
+
rspec-support (~> 3.1.0)
|
|
17
|
+
rspec-expectations (3.1.2)
|
|
18
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
19
|
+
rspec-support (~> 3.1.0)
|
|
20
|
+
rspec-mocks (3.1.3)
|
|
21
|
+
rspec-support (~> 3.1.0)
|
|
22
|
+
rspec-support (3.1.2)
|
|
23
|
+
|
|
24
|
+
PLATFORMS
|
|
25
|
+
ruby
|
|
26
|
+
|
|
27
|
+
DEPENDENCIES
|
|
28
|
+
bundler (~> 1.7)
|
|
29
|
+
hubert!
|
|
30
|
+
rake (~> 10.0)
|
|
31
|
+
rspec (~> 3.0)
|
data/LICENCE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2014 Andrzej Kajetanowicz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#Hubert: simple tool for building HTTP URIs
|
|
2
|
+
Hubert makes it easy to generate URLs using simple template strings. Building
|
|
3
|
+
http API clients usually requires some way composing different URLs. This task
|
|
4
|
+
is easy if the API provides only a few endpoints but it becomes more cumbersome
|
|
5
|
+
as the number of available endpoints increases. Clients need to specify correct
|
|
6
|
+
protocols, ports, escape any dynamic values inserted into URI segments and query
|
|
7
|
+
strings. This gem is attempting to address this problem by providing a simple DSL
|
|
8
|
+
for defining methods that will return desired URLs or paths.
|
|
9
|
+
|
|
10
|
+
## Example usage:
|
|
11
|
+
```ruby
|
|
12
|
+
require 'hubert'
|
|
13
|
+
|
|
14
|
+
class MyAwesomeApiClient
|
|
15
|
+
|
|
16
|
+
# to make this work you need to extend your class with DSL module
|
|
17
|
+
extend Hubert::DSL
|
|
18
|
+
|
|
19
|
+
# all URLs will use https protocol (http is the default)
|
|
20
|
+
https!
|
|
21
|
+
|
|
22
|
+
# specifying hostname is required (unless you only want to generate paths)
|
|
23
|
+
host 'api.example.com'
|
|
24
|
+
|
|
25
|
+
# any path can be optionally prefixed - this can be usefull
|
|
26
|
+
# if for example your code uses specific API version or
|
|
27
|
+
# hits some specific group of endpoints (/users/... etc.)
|
|
28
|
+
path_prefix '/v1'
|
|
29
|
+
|
|
30
|
+
# this will allow to call user_url(id: 123) in your class
|
|
31
|
+
url '/users/:id', as: user
|
|
32
|
+
|
|
33
|
+
# this is a simplified version of the one above
|
|
34
|
+
# you can use it by calling user_path(id: 123)
|
|
35
|
+
path '/users/:id', as: user
|
|
36
|
+
|
|
37
|
+
def print_user_url
|
|
38
|
+
puts user_url(id: 123)
|
|
39
|
+
end
|
|
40
|
+
# => 'https://api.example.com/v1/users/123'
|
|
41
|
+
|
|
42
|
+
def print_user_path
|
|
43
|
+
puts user_path(id: 123)
|
|
44
|
+
end
|
|
45
|
+
# => '/v1/users/123'
|
|
46
|
+
|
|
47
|
+
# any additional key - value pairs will become a part of the query string
|
|
48
|
+
# all values will be escaped using CGI.escape
|
|
49
|
+
def print_user_url_with_additional_ke_value_pairs
|
|
50
|
+
puts user_url(id: 123, format: 'very pretty', include_description: true)
|
|
51
|
+
end
|
|
52
|
+
# => 'https://api.example.com/v1/users/123?format=very+pretty&include_description=true'
|
|
53
|
+
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
More examples can be found in spec files.
|
|
59
|
+
|
|
60
|
+
## Contributing
|
|
61
|
+
1. Fork it
|
|
62
|
+
2. Create your feature branch (git checkout -b feature/my-new-feature)
|
|
63
|
+
3. Commit your changes (git commit -am 'Add some feature')
|
|
64
|
+
4. Push to the branch (git push origin feature/my-new-feature)
|
|
65
|
+
5. Create new Pull Request`
|
data/Rakefile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
begin
|
|
2
|
+
require 'rspec/core/rake_task'
|
|
3
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
|
4
|
+
t.verbose = false
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
task default: :spec
|
|
8
|
+
rescue LoadError
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
task :benchmark do
|
|
12
|
+
Dir['benchmark/**/*.rb'].each do |benchmark|
|
|
13
|
+
sh 'ruby', benchmark
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require 'benchmark'
|
|
2
|
+
require 'hubert'
|
|
3
|
+
|
|
4
|
+
probes = 100000
|
|
5
|
+
Benchmark.bm(25) do |b|
|
|
6
|
+
b.report('Hubert::Template#render') do
|
|
7
|
+
|
|
8
|
+
template = Hubert::Template.new('/simple/path/:id/some/:name')
|
|
9
|
+
context = { id: 123, name: 'hello world', sort: 'name', order: 'asc' }
|
|
10
|
+
|
|
11
|
+
probes.times { template.render(context) }
|
|
12
|
+
end
|
|
13
|
+
end
|
data/examples/github.rb
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Simple github client
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'hubert'
|
|
5
|
+
|
|
6
|
+
module Github
|
|
7
|
+
class User
|
|
8
|
+
extend Hubert::DSL
|
|
9
|
+
|
|
10
|
+
https!
|
|
11
|
+
host 'api.github.com'
|
|
12
|
+
path_prefix '/users'
|
|
13
|
+
|
|
14
|
+
url '/:username', as: 'summary'
|
|
15
|
+
url '/:username/followers', as: 'followers'
|
|
16
|
+
url '/:username/gists', as: 'gists'
|
|
17
|
+
url '/:username/gists/:gist_id', as: 'gist'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def initialize(username)
|
|
21
|
+
@username = username
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def summary
|
|
25
|
+
get summary_url(username: @username)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def followers
|
|
29
|
+
get followers_url(username: @username)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def gists
|
|
33
|
+
get gists_url(username: @username)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def gist(id)
|
|
37
|
+
get gist_url(username: @username, gist_id: id)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def get(uri)
|
|
43
|
+
Net::HTTP.get URI(uri)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/hubert.gemspec
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require File.expand_path('../lib/hubert/version', __FILE__)
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |gem|
|
|
4
|
+
gem.name = 'hubert'
|
|
5
|
+
gem.version = Hubert::VERSION
|
|
6
|
+
gem.summary = 'Hubert: simple http URL builder tool'
|
|
7
|
+
gem.description = 'Hubert makes it easy to generate URLs using template strings known from Ruby On Rails framework.'
|
|
8
|
+
|
|
9
|
+
gem.authors = ['Andrzej Kajetanowicz']
|
|
10
|
+
gem.email = ['andrzej.kajetanowicz@gmail.com']
|
|
11
|
+
gem.homepage = 'https://github.com/kajetanowicz/hubert'
|
|
12
|
+
gem.license = 'MIT'
|
|
13
|
+
|
|
14
|
+
gem.files = `git ls-files`.split($\)
|
|
15
|
+
gem.executables = []
|
|
16
|
+
gem.test_files = gem.files.grep(%r{^spec/})
|
|
17
|
+
gem.require_paths = ['lib']
|
|
18
|
+
|
|
19
|
+
gem.add_development_dependency 'bundler', '~> 1.7'
|
|
20
|
+
gem.add_development_dependency 'rake', '~> 10.0'
|
|
21
|
+
gem.add_development_dependency 'rspec', '~> 3.0'
|
|
22
|
+
end
|
data/lib/hubert.rb
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require 'forwardable'
|
|
2
|
+
|
|
3
|
+
module Hubert
|
|
4
|
+
class Builder
|
|
5
|
+
extend Forwardable
|
|
6
|
+
|
|
7
|
+
def_delegators :components, :protocol=, :protocol
|
|
8
|
+
def_delegators :components, :host=, :host
|
|
9
|
+
def_delegators :components, :path_prefix=, :path_prefix
|
|
10
|
+
def_delegators :components, :port=, :port
|
|
11
|
+
def_delegators :components, :http!, :https!
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@templates = {}
|
|
15
|
+
yield(self) if block_given?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def path(template, context = {})
|
|
19
|
+
templates(template).render(context)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def url(template, context = {})
|
|
23
|
+
fail HostNotSet, 'Unable to generate URL without host' if host.nil?
|
|
24
|
+
|
|
25
|
+
String.new.tap do|url|
|
|
26
|
+
url << protocol + '://'
|
|
27
|
+
url << host
|
|
28
|
+
url << ':' + port unless components.default_port?
|
|
29
|
+
url << path_prefix
|
|
30
|
+
url << path(template, context)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def templates(name)
|
|
35
|
+
@templates.fetch(name) do
|
|
36
|
+
@templates[name] = Template.new(name)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def components
|
|
43
|
+
@components ||= Components.new
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
module Hubert
|
|
3
|
+
class Components
|
|
4
|
+
|
|
5
|
+
DEFAULT_PORTS = { 'http' => '80', 'https' => '443' }
|
|
6
|
+
|
|
7
|
+
def protocol=(protocol)
|
|
8
|
+
protocol = protocol.to_s unless protocol.is_a?(String)
|
|
9
|
+
protocol.strip!
|
|
10
|
+
if protocol =~ /(http)|(https)/i
|
|
11
|
+
@protocol = protocol.downcase
|
|
12
|
+
else
|
|
13
|
+
fail InvalidProtocol, "Provided protocol: [#{protocol}] is invalid"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def protocol
|
|
18
|
+
@protocol ||= 'http'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def http!
|
|
22
|
+
@protocol = 'http'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def https!
|
|
26
|
+
@protocol = 'https'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def host=(host)
|
|
30
|
+
host.strip!
|
|
31
|
+
host = 'http://' + host if URI.parse(host).scheme.nil?
|
|
32
|
+
@host = URI.parse(host).host
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def host
|
|
36
|
+
@host
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def path_prefix=(path)
|
|
40
|
+
path.strip!
|
|
41
|
+
path = URI.parse(path).path
|
|
42
|
+
path = path[0..-2] if path.end_with?('/')
|
|
43
|
+
path = path[1..-1] if path.start_with?('/')
|
|
44
|
+
|
|
45
|
+
@path_prefix = '/' + path
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def path_prefix
|
|
49
|
+
@path_prefix ||= ''
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def port
|
|
53
|
+
@port ||= DEFAULT_PORTS.fetch(protocol, '80')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def port=(port)
|
|
57
|
+
@port = port.to_s
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def default_port?
|
|
61
|
+
DEFAULT_PORTS.fetch(protocol) == port
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/hubert/dsl.rb
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module Hubert
|
|
2
|
+
module DSL
|
|
3
|
+
|
|
4
|
+
class MethodDefiner
|
|
5
|
+
def initialize(klass)
|
|
6
|
+
@klass = klass
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def define(method, path, options)
|
|
10
|
+
ensure_alias!(options, method)
|
|
11
|
+
|
|
12
|
+
@klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
13
|
+
def #{options[:as]}_#{method}(ctx = {}) # def get_items_path(ctx = {})
|
|
14
|
+
self.class._hubert_builder.#{method}('#{path}', ctx) # self.class._hubert_builder.url('some/:interesting/path:id', ctx)
|
|
15
|
+
end # end
|
|
16
|
+
RUBY
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def ensure_alias!(options, method)
|
|
22
|
+
if options.fetch(:as, '') =~ /^\s*$/
|
|
23
|
+
fail AliasNotSet, "Please specify ':as' option when defining new #{method}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def path(path, options = {})
|
|
29
|
+
MethodDefiner.new(self).define(:path, path, options)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def url(path, options = {})
|
|
33
|
+
MethodDefiner.new(self).define(:url, path, options)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def http!
|
|
37
|
+
_hubert_builder.http!
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def https!
|
|
41
|
+
_hubert_builder.https!
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def host(host)
|
|
45
|
+
_hubert_builder.host = host
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def port(port)
|
|
49
|
+
_hubert_builder.port = port
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def path_prefix(path_prefix)
|
|
53
|
+
_hubert_builder.path_prefix = path_prefix
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def _hubert_builder
|
|
57
|
+
@_hubert_builder ||= Builder.new
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require 'forwardable'
|
|
2
|
+
|
|
3
|
+
module Hubert
|
|
4
|
+
class Template
|
|
5
|
+
PH = /:[a-zA-Z_]+/
|
|
6
|
+
|
|
7
|
+
extend Forwardable
|
|
8
|
+
def_delegator :@compiled, :map
|
|
9
|
+
|
|
10
|
+
def initialize(template)
|
|
11
|
+
@template = template
|
|
12
|
+
@compiled = []
|
|
13
|
+
|
|
14
|
+
compile!
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def render(ctx = {})
|
|
18
|
+
Renderer.new(self, ctx).render
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def placeholders
|
|
22
|
+
@compiled.select { |segment| segment.is_a?(Symbol) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def compile!
|
|
28
|
+
normalize_template!
|
|
29
|
+
extract_placeholders!
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def normalize_template!
|
|
33
|
+
path = @template
|
|
34
|
+
|
|
35
|
+
path = path[0..-2] if path.end_with?('/')
|
|
36
|
+
path = path[1..-1] if path.start_with?('/')
|
|
37
|
+
|
|
38
|
+
@template = '/' + path
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def extract_placeholders!
|
|
42
|
+
tail = @template
|
|
43
|
+
|
|
44
|
+
loop do
|
|
45
|
+
head, placeholder, tail = tail.partition(PH)
|
|
46
|
+
@compiled << head
|
|
47
|
+
break if placeholder.empty?
|
|
48
|
+
@compiled << placeholder[1..-1].to_sym
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Hubert
|
|
2
|
+
class Template
|
|
3
|
+
class Context
|
|
4
|
+
def initialize(ctx = {})
|
|
5
|
+
@context = ctx
|
|
6
|
+
@lookups = []
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def lookup(key)
|
|
10
|
+
@lookups << key
|
|
11
|
+
|
|
12
|
+
@context.fetch(key) do
|
|
13
|
+
fail KeyNotFound, "Unable to find a value for a given key [#{key}]"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def unused
|
|
18
|
+
lookups.reduce(@context.dup) do |ctx, key|
|
|
19
|
+
ctx.delete(key); ctx
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def lookups
|
|
24
|
+
@lookups.uniq
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'cgi'
|
|
2
|
+
|
|
3
|
+
module Hubert
|
|
4
|
+
class Template
|
|
5
|
+
class Renderer
|
|
6
|
+
|
|
7
|
+
def initialize(template, ctx)
|
|
8
|
+
@template = template
|
|
9
|
+
@context = Context.new(ctx)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def render
|
|
13
|
+
result = path
|
|
14
|
+
result << '?' + query if query?
|
|
15
|
+
result
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :template, :context
|
|
21
|
+
|
|
22
|
+
def path
|
|
23
|
+
template.map do |segment|
|
|
24
|
+
segment.kind_of?(Symbol) ? lookup(segment) : segment
|
|
25
|
+
end * ''
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def query
|
|
29
|
+
context.unused.map do |key, value|
|
|
30
|
+
"#{escape(key)}=#{escape(value)}"
|
|
31
|
+
end * '&'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def query?
|
|
35
|
+
context.unused.any?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def lookup(segment)
|
|
39
|
+
escape(context.lookup(segment))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def escape(stringish)
|
|
43
|
+
CGI.escape(stringish.to_s)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module Hubert
|
|
4
|
+
describe Builder do
|
|
5
|
+
let(:builder) do
|
|
6
|
+
Builder.new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
describe '#path' do
|
|
10
|
+
it 'renders a path' do
|
|
11
|
+
expect(builder.path('/foo/bar')).to eq('/foo/bar')
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'replaces placeholders with values' do
|
|
15
|
+
expect(
|
|
16
|
+
builder.path('create/:id/some/:name', id: 11, name: 'example')
|
|
17
|
+
).to eq('/create/11/some/example')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'caches templates' do
|
|
21
|
+
expect(Template).to receive(:new).with('/path/to/resource/:id').once.and_call_original
|
|
22
|
+
|
|
23
|
+
builder.path('/path/to/resource/:id', id: 11)
|
|
24
|
+
builder.path('/path/to/resource/:id', id: 12)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe '#url' do
|
|
29
|
+
let(:builder) do
|
|
30
|
+
Builder.new do |b|
|
|
31
|
+
b.http!
|
|
32
|
+
b.host = 'example.com'
|
|
33
|
+
b.port = port
|
|
34
|
+
b.path_prefix = 'api'
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
let(:port) { 80 }
|
|
39
|
+
|
|
40
|
+
it 'returns full URL' do
|
|
41
|
+
expect(builder.url('/path/to/resource/:id', id: 11, sort: 'name'))
|
|
42
|
+
.to eq('http://example.com/api/path/to/resource/11?sort=name')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
context 'when using non-default port number' do
|
|
46
|
+
let(:port) { 8080 }
|
|
47
|
+
|
|
48
|
+
it 'creates an url with port number' do
|
|
49
|
+
expect(builder.url('/path/to/resource/:id', id: 11, sort: 'name'))
|
|
50
|
+
.to eq('http://example.com:8080/api/path/to/resource/11?sort=name')
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
context 'when protocol is not set' do
|
|
55
|
+
let(:builder) do
|
|
56
|
+
Builder.new do |b|
|
|
57
|
+
b.host = 'example.com'
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'uses default http protocol' do
|
|
62
|
+
expect(builder.url('/path/to/resource'))
|
|
63
|
+
.to eq('http://example.com/path/to/resource')
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
context 'when domain is not set' do
|
|
68
|
+
let(:builder) { Builder.new }
|
|
69
|
+
|
|
70
|
+
it 'raises an exception' do
|
|
71
|
+
expect { builder.url('/path') }.to raise_error(HostNotSet)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module Hubert
|
|
4
|
+
describe Components do
|
|
5
|
+
let(:components) { Components.new }
|
|
6
|
+
|
|
7
|
+
describe '#protocol=' do
|
|
8
|
+
it 'assigns the protocol' do
|
|
9
|
+
components.protocol = 'HTTP'
|
|
10
|
+
|
|
11
|
+
expect(components.protocol).to eq('http')
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'raises an exception if the protocol is invalid' do
|
|
15
|
+
expect { components.protocol = 'ftp' }.
|
|
16
|
+
to raise_error(InvalidProtocol, /\[ftp\]/)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
context 'when passed as a symbol' do
|
|
20
|
+
it 'converts protocol to a String' do
|
|
21
|
+
components.protocol = :http
|
|
22
|
+
|
|
23
|
+
expect(components.protocol).to eq('http')
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe '#http!' do
|
|
29
|
+
it 'assigns the HTTP protocol' do
|
|
30
|
+
components.http!
|
|
31
|
+
|
|
32
|
+
expect(components.protocol).to eq('http')
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe '#https!' do
|
|
37
|
+
it 'assigns the HTTPS protocol' do
|
|
38
|
+
components.https!
|
|
39
|
+
|
|
40
|
+
expect(components.protocol).to eq('https')
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe '#host=' do
|
|
45
|
+
it 'assigns the host' do
|
|
46
|
+
components.host = 'example.com'
|
|
47
|
+
|
|
48
|
+
expect(components.host).to eq('example.com')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'removes the path component from the host' do
|
|
52
|
+
components.host = 'example.com/path/to/resource'
|
|
53
|
+
|
|
54
|
+
expect(components.host).to eq('example.com')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'removes the protocol component from the host' do
|
|
58
|
+
components.host = 'http://example.com/path/to/resource'
|
|
59
|
+
|
|
60
|
+
expect(components.host).to eq('example.com')
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
describe '#path_prefix' do
|
|
65
|
+
it 'assigns the path prefix' do
|
|
66
|
+
components.path_prefix = 'foo/bar/baz'
|
|
67
|
+
|
|
68
|
+
expect(components.path_prefix).to eq('/foo/bar/baz')
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'removes slashes from the end of the path' do
|
|
72
|
+
components.path_prefix = '/foo/bar/baz/'
|
|
73
|
+
|
|
74
|
+
expect(components.path_prefix).to eq('/foo/bar/baz')
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe '#port' do
|
|
79
|
+
it 'returns default HTTP port (80)' do
|
|
80
|
+
expect(components.port).to eq('80')
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
context 'when using HTTPS' do
|
|
84
|
+
it 'returns the default HTTPS port (443)' do
|
|
85
|
+
components.https!
|
|
86
|
+
|
|
87
|
+
expect(components.port).to eq('443')
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
context 'when using HTTP' do
|
|
92
|
+
it 'returns the default HTTP port (80)' do
|
|
93
|
+
components.http!
|
|
94
|
+
|
|
95
|
+
expect(components.port).to eq('80')
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe '#port=' do
|
|
101
|
+
it 'assings port' do
|
|
102
|
+
components.port = 123
|
|
103
|
+
|
|
104
|
+
expect(components.port).to eq('123')
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module Hubert
|
|
4
|
+
describe DSL do
|
|
5
|
+
let(:example_class) do
|
|
6
|
+
Class.new { extend DSL }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
%w( path url http! https! port host path_prefix ).each do |method|
|
|
10
|
+
it "adds '#{method}' method to the host class" do
|
|
11
|
+
expect(example_class).to respond_to(method)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe '.path' do
|
|
16
|
+
before do
|
|
17
|
+
example_class.class_eval do
|
|
18
|
+
path 'some/:simple/path/:id', as: :get_items
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'defines *_path method on any instance of the class' do
|
|
23
|
+
expect(example_class.new).to respond_to(:get_items_path)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
context 'when calling new method' do
|
|
27
|
+
subject do
|
|
28
|
+
instance.get_items_path(simple: 'foo', id: 1234, sort: 'name', order: 'desc')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
let(:instance) do
|
|
32
|
+
example_class.new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'builds path using passed Hash' do
|
|
36
|
+
expect(subject).to eq('/some/foo/path/1234?sort=name&order=desc')
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
context 'when :as key is not present' do
|
|
41
|
+
it 'raises an exception' do
|
|
42
|
+
expect {
|
|
43
|
+
example_class.class_eval do
|
|
44
|
+
path 'some/:simple/path/:id'
|
|
45
|
+
end
|
|
46
|
+
}.to raise_error(AliasNotSet, /:as/)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
describe '.url' do
|
|
52
|
+
before do
|
|
53
|
+
example_class.class_eval do
|
|
54
|
+
url 'some/:simple/path/:id', as: :get_items
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'defines *_url method on any instance of the class' do
|
|
59
|
+
expect(example_class.new).to respond_to(:get_items_url)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe 'defined method' do
|
|
63
|
+
context 'when host is not specified' do
|
|
64
|
+
it 'raises an exception' do
|
|
65
|
+
expect { example_class.new.get_items_url }
|
|
66
|
+
.to raise_error(HostNotSet)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
context 'when all path segments are specified' do
|
|
71
|
+
before do
|
|
72
|
+
example_class.class_eval do
|
|
73
|
+
https!; host 'example.com'; port 8080; path_prefix 'api'
|
|
74
|
+
|
|
75
|
+
url 'some/:simple/path/:id', as: :get_items
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'returns a url' do
|
|
80
|
+
expect(example_class.new.get_items_url(simple: 'foo', id: 'bar'))
|
|
81
|
+
.to eq('https://example.com:8080/api/some/foo/path/bar')
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module Hubert
|
|
4
|
+
describe Template do
|
|
5
|
+
let(:template) do
|
|
6
|
+
Template.new(path)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
let(:path) do
|
|
10
|
+
'/first/second/third/'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe '#placeholders' do
|
|
14
|
+
subject { template.placeholders }
|
|
15
|
+
|
|
16
|
+
let(:path) do
|
|
17
|
+
'simple/:path/:with/some/:placeholders'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it' returns a list of placeholders' do
|
|
21
|
+
expect(subject).to eq([:path, :with, :placeholders])
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
describe '#render' do
|
|
26
|
+
subject { template.render({}) }
|
|
27
|
+
|
|
28
|
+
it 'returns a string' do
|
|
29
|
+
expect(subject).to be_kind_of(String)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'renders a path' do
|
|
33
|
+
expect(subject).to include('first/second/third')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'returns a path prefixed with "/"' do
|
|
37
|
+
expect(subject).to match(/^\/[^\/]/)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'removes following slash from the path' do
|
|
41
|
+
expect(subject[-1]).not_to eq('/')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
context 'when path contains placeholders' do
|
|
45
|
+
subject do
|
|
46
|
+
template.render(name: 'test', id: 1234)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
let(:path) do
|
|
50
|
+
'a/path/to/:name/followed/by/:id/and/:name/again'
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'substitues all placeholders with provided values' do
|
|
54
|
+
expect(subject).to eq(
|
|
55
|
+
'/a/path/to/test/followed/by/1234/and/test/again'
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
context 'when hash contains additional keys' do
|
|
61
|
+
subject do
|
|
62
|
+
template.render(parent_id: 10, id: 512, sort: 'name', order: 'asc')
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
let(:path) do
|
|
66
|
+
'a/path/:parent_id/:id/'
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'builds a query string' do
|
|
70
|
+
expect(subject).to eq('/a/path/10/512?sort=name&order=asc')
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
context 'when unable to find a placeholder substitution' do
|
|
75
|
+
subject do
|
|
76
|
+
template.render(id: 123)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
let(:path) do
|
|
80
|
+
'a/path/:id/:name'
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'raises an exception' do
|
|
84
|
+
expect { subject }.to raise_error(KeyNotFound, /\[name\]/)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
context 'when query string contains characters that are not allowed' do
|
|
89
|
+
subject do
|
|
90
|
+
template.render(escape: 'all the things', like: 'a boss!!')
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
let(:path) do
|
|
94
|
+
'a/path/without/placeholders'
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'escapes all illegal characters' do
|
|
98
|
+
expect(subject).to eq(
|
|
99
|
+
'/a/path/without/placeholders?escape=all+the+things&like=a+boss%21%21'
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
context 'when segments contain illegal characters' do
|
|
105
|
+
subject do
|
|
106
|
+
template.render(what: 'all the things', who: 'a boss')
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
let(:path) do
|
|
110
|
+
'escape/:what/like/:who'
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'escapes all illegal characters' do
|
|
114
|
+
expect(subject).to eq('/escape/all+the+things/like/a+boss')
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require 'spec_helper'
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: hubert
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Andrzej Kajetanowicz
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2015-04-26 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.7'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.7'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '10.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '10.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: '3.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.0'
|
|
55
|
+
description: Hubert makes it easy to generate URLs using template strings known from
|
|
56
|
+
Ruby On Rails framework.
|
|
57
|
+
email:
|
|
58
|
+
- andrzej.kajetanowicz@gmail.com
|
|
59
|
+
executables: []
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files: []
|
|
62
|
+
files:
|
|
63
|
+
- ".gitignore"
|
|
64
|
+
- ".rspec"
|
|
65
|
+
- Gemfile
|
|
66
|
+
- Gemfile.lock
|
|
67
|
+
- LICENCE
|
|
68
|
+
- README.md
|
|
69
|
+
- Rakefile
|
|
70
|
+
- benchmark/template.rb
|
|
71
|
+
- examples/github.rb
|
|
72
|
+
- hubert.gemspec
|
|
73
|
+
- lib/hubert.rb
|
|
74
|
+
- lib/hubert/builder.rb
|
|
75
|
+
- lib/hubert/components.rb
|
|
76
|
+
- lib/hubert/dsl.rb
|
|
77
|
+
- lib/hubert/errors.rb
|
|
78
|
+
- lib/hubert/template.rb
|
|
79
|
+
- lib/hubert/template/context.rb
|
|
80
|
+
- lib/hubert/template/renderer.rb
|
|
81
|
+
- lib/hubert/version.rb
|
|
82
|
+
- spec/lib/hubert/builder_spec.rb
|
|
83
|
+
- spec/lib/hubert/components_spec.rb
|
|
84
|
+
- spec/lib/hubert/dsl_spec.rb
|
|
85
|
+
- spec/lib/hubert/template_spec.rb
|
|
86
|
+
- spec/lib/hubert_spec.rb
|
|
87
|
+
- spec/spec_helper.rb
|
|
88
|
+
homepage: https://github.com/kajetanowicz/hubert
|
|
89
|
+
licenses:
|
|
90
|
+
- MIT
|
|
91
|
+
metadata: {}
|
|
92
|
+
post_install_message:
|
|
93
|
+
rdoc_options: []
|
|
94
|
+
require_paths:
|
|
95
|
+
- lib
|
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - ">="
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '0'
|
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
102
|
+
requirements:
|
|
103
|
+
- - ">="
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: '0'
|
|
106
|
+
requirements: []
|
|
107
|
+
rubyforge_project:
|
|
108
|
+
rubygems_version: 2.2.2
|
|
109
|
+
signing_key:
|
|
110
|
+
specification_version: 4
|
|
111
|
+
summary: 'Hubert: simple http URL builder tool'
|
|
112
|
+
test_files:
|
|
113
|
+
- spec/lib/hubert/builder_spec.rb
|
|
114
|
+
- spec/lib/hubert/components_spec.rb
|
|
115
|
+
- spec/lib/hubert/dsl_spec.rb
|
|
116
|
+
- spec/lib/hubert/template_spec.rb
|
|
117
|
+
- spec/lib/hubert_spec.rb
|
|
118
|
+
- spec/spec_helper.rb
|
|
119
|
+
has_rdoc:
|