apitizer 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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +2 -0
  4. data/CHANGELOG.md +1 -0
  5. data/Gemfile +3 -0
  6. data/Guardfile +7 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +91 -0
  9. data/Rakefile +7 -0
  10. data/apitizer.gemspec +32 -0
  11. data/lib/apitizer.rb +14 -0
  12. data/lib/apitizer/base.rb +61 -0
  13. data/lib/apitizer/connection.rb +10 -0
  14. data/lib/apitizer/connection/adaptor.rb +13 -0
  15. data/lib/apitizer/connection/adaptor/standard.rb +34 -0
  16. data/lib/apitizer/connection/dispatcher.rb +24 -0
  17. data/lib/apitizer/connection/request.rb +16 -0
  18. data/lib/apitizer/connection/response.rb +12 -0
  19. data/lib/apitizer/core.rb +24 -0
  20. data/lib/apitizer/helper.rb +46 -0
  21. data/lib/apitizer/processing.rb +8 -0
  22. data/lib/apitizer/processing/parser.rb +14 -0
  23. data/lib/apitizer/processing/parser/json.rb +15 -0
  24. data/lib/apitizer/processing/parser/yaml.rb +15 -0
  25. data/lib/apitizer/processing/translator.rb +13 -0
  26. data/lib/apitizer/result.rb +15 -0
  27. data/lib/apitizer/routing.rb +10 -0
  28. data/lib/apitizer/routing/mapper.rb +47 -0
  29. data/lib/apitizer/routing/node.rb +5 -0
  30. data/lib/apitizer/routing/node/base.rb +44 -0
  31. data/lib/apitizer/routing/node/collection.rb +34 -0
  32. data/lib/apitizer/routing/node/operation.rb +32 -0
  33. data/lib/apitizer/routing/node/root.rb +15 -0
  34. data/lib/apitizer/routing/node/scope.rb +19 -0
  35. data/lib/apitizer/routing/path.rb +26 -0
  36. data/lib/apitizer/routing/proxy.rb +17 -0
  37. data/lib/apitizer/version.rb +3 -0
  38. data/spec/apitizer/base_spec.rb +71 -0
  39. data/spec/apitizer/connection/adaptor_spec.rb +24 -0
  40. data/spec/apitizer/connection/dispatcher_spec.rb +39 -0
  41. data/spec/apitizer/helper_spec.rb +87 -0
  42. data/spec/apitizer/processing/parser_spec.rb +23 -0
  43. data/spec/apitizer/result_spec.rb +19 -0
  44. data/spec/apitizer/routing/mapper_spec.rb +80 -0
  45. data/spec/apitizer/routing/node_spec.rb +63 -0
  46. data/spec/apitizer/routing/path_spec.rb +102 -0
  47. data/spec/spec_helper.rb +11 -0
  48. data/spec/support/factory_helper.rb +23 -0
  49. data/spec/support/resource_helper.rb +18 -0
  50. metadata +203 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 77d5476a8e3c80f3034bd2f27144d7f2fd997095
4
+ data.tar.gz: 483fda35af1d8363671a0aad567ce9a55334058a
5
+ SHA512:
6
+ metadata.gz: ecb1d3880c1c46112e6ad3233ad60a49b112cc1d4c978200a6a69aa1923b217a95b25630503802ea3ae586ee3da8b6529d2ef61a075540ab0fc2f89efb06b3e3
7
+ data.tar.gz: 5503cfb3bbdb45bd32da4b028432a12f68d948da8caca486c559677fda7953631695867084963f2776ac9ba608da0bd8538b993eb4d540d56d3b5c93ff12ddfb
@@ -0,0 +1,5 @@
1
+ .DS_Store
2
+ *.swo
3
+ *.swp
4
+ *.gem
5
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
@@ -0,0 +1 @@
1
+ ## Apitizer 0.0.1 (June 1, 2014)
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,7 @@
1
+ guard :rspec do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch('spec/spec_helper.rb') { 'spec' }
4
+ watch(%r{^spec/support/(.+)\.rb$}) { 'spec' }
5
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{ m[1] }_spec.rb" }
6
+ watch(%r{^lib/(.*\.rb)$}) { |m| "spec/#{ File.dirname(m[1]) }" }
7
+ end
@@ -0,0 +1,22 @@
1
+ Copyright 2014 Ivan Ukhov
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.
@@ -0,0 +1,91 @@
1
+ # Apitizer
2
+ The main ingredient of a RESTful API client.
3
+
4
+ ## Installation
5
+ Add the following line to your `Gemfile`:
6
+ ```ruby
7
+ gem 'apitizer'
8
+ ```
9
+
10
+ Then execute:
11
+ ```bash
12
+ $ bundle
13
+ ```
14
+
15
+ Alternatively, you can install the gem manually:
16
+ ```bash
17
+ $ gem install apitizer
18
+ ```
19
+
20
+ ## Usage
21
+ Here is an example for the [Typekit API](https://typekit.com/docs/api).
22
+ Check out [Typekit Client](https://github.com/IvanUkhov/typekit-client)
23
+ as well.
24
+
25
+ Code:
26
+ ```ruby
27
+ require 'apitizer'
28
+
29
+ options = {
30
+ # Format
31
+ format: :json,
32
+ # Authorization
33
+ headers: { 'X-Typekit-Token' => ENV['tk_token'] },
34
+ # Non-standard REST-HTTP mapping
35
+ dictionary: { update: :post }
36
+ }
37
+
38
+ apitizer = Apitizer::Base.new(options) do
39
+ address 'https://typekit.com/api/v1/json'
40
+
41
+ resources :families, only: :show do
42
+ show ':variant', on: :member
43
+ end
44
+
45
+ resources :kits do
46
+ resources :families, only: [ :show, :update, :delete ]
47
+ show :published, on: :member
48
+ update :publish, on: :member
49
+ end
50
+
51
+ resources :libraries, only: [ :index, :show ]
52
+ end
53
+
54
+ puts JSON.pretty_generate(apitizer.index(:kits))
55
+ ```
56
+
57
+ Output:
58
+ ```json
59
+ {
60
+ "kits": [
61
+ {
62
+ "id": "bas4cfe",
63
+ "link": "/api/v1/json/kits/bas4cfe"
64
+ },
65
+ {
66
+ "id": "sfh6bkj",
67
+ "link": "/api/v1/json/kits/sfh6bkj"
68
+ },
69
+ {
70
+ "id": "kof8zcn",
71
+ "link": "/api/v1/json/kits/kof8zcn"
72
+ },
73
+ {
74
+ "id": "zyx4wop",
75
+ "link": "/api/v1/json/kits/zyx4wop"
76
+ }
77
+ ]
78
+ }
79
+ ```
80
+
81
+ ## History
82
+ Apitizer was a part of
83
+ [Typekit Client](https://github.com/IvanUkhov/typekit-client).
84
+
85
+ ## Contributing
86
+
87
+ 1. Fork it ( https://github.com/IvanUkhov/apitizer/fork )
88
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
89
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
90
+ 4. Push to the branch (`git push origin my-new-feature`)
91
+ 5. Create a new Pull Request
@@ -0,0 +1,7 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+ task :test => :spec
@@ -0,0 +1,32 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ require 'apitizer/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'apitizer'
8
+ spec.version = Apitizer::VERSION
9
+ spec.authors = [ 'Ivan Ukhov' ]
10
+ spec.email = [ 'ivan.ukhov@gmail.com' ]
11
+ spec.summary = 'The main ingredient of a RESTful API client'
12
+ spec.description = 'A Ruby library that provides a flexible engine ' \
13
+ 'for developing RESTful API clients.'
14
+ spec.homepage = 'https://github.com/IvanUkhov/apitizer'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0")
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^spec/})
20
+ spec.require_paths = [ 'lib' ]
21
+
22
+ spec.required_ruby_version = '>= 2.1'
23
+
24
+ spec.add_dependency 'rack', '~> 1.5'
25
+ spec.add_dependency 'json', '~> 1.8'
26
+
27
+ spec.add_development_dependency 'bundler', '~> 1.6'
28
+ spec.add_development_dependency 'rake'
29
+ spec.add_development_dependency 'rspec', '~> 2.14'
30
+ spec.add_development_dependency 'guard-rspec', '~> 4.2'
31
+ spec.add_development_dependency 'webmock', '~> 1.18'
32
+ end
@@ -0,0 +1,14 @@
1
+ require 'delegate'
2
+ require 'forwardable'
3
+
4
+ require_relative 'apitizer/core'
5
+ require_relative 'apitizer/helper'
6
+
7
+ require_relative 'apitizer/routing'
8
+ require_relative 'apitizer/connection'
9
+ require_relative 'apitizer/processing'
10
+
11
+ require_relative 'apitizer/result'
12
+ require_relative 'apitizer/base'
13
+
14
+ require_relative 'apitizer/version'
@@ -0,0 +1,61 @@
1
+ module Apitizer
2
+ class Base
3
+ def initialize(**options, &block)
4
+ @options = Helper.deep_merge(Apitizer.defaults, options)
5
+ @block = block
6
+ end
7
+
8
+ def process(*arguments)
9
+ request = build_request(*arguments)
10
+ response = dispatcher.process(request)
11
+ content = translator.process(response)
12
+ Result.new(request: request, response: response, content: content)
13
+ end
14
+
15
+ Apitizer.actions.each do |action|
16
+ define_method(action) do |*arguments|
17
+ process(action, *arguments)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ [ :mapper, :dispatcher, :translator ].each do |component|
24
+ class_eval <<-METHOD, __FILE__, __LINE__ + 1
25
+ def #{ component }
26
+ @#{ component } ||= build_#{ component }
27
+ end
28
+ METHOD
29
+ end
30
+
31
+ def build_mapper
32
+ Routing::Mapper.new(&@block)
33
+ end
34
+
35
+ def build_dispatcher
36
+ Connection::Dispatcher.new(adaptor: self.adaptor,
37
+ dictionary: self.dictionary, headers: self.headers)
38
+ end
39
+
40
+ def build_translator
41
+ Processing::Translator.new(format: self.format)
42
+ end
43
+
44
+ def build_request(*arguments)
45
+ action, steps, parameters = prepare(*arguments)
46
+ path = mapper.trace(action, steps)
47
+ Connection::Request.new(action: action, path: path,
48
+ parameters: parameters)
49
+ end
50
+
51
+ def prepare(action, *path)
52
+ parameters = path.last.is_a?(Hash) ? path.pop : {}
53
+ [ action.to_sym, path.flatten.map(&:to_sym), parameters ]
54
+ end
55
+
56
+ def method_missing(name, *arguments, &block)
57
+ return @options[name] if @options.key?(name)
58
+ super
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,10 @@
1
+ require_relative 'connection/request'
2
+ require_relative 'connection/adaptor'
3
+ require_relative 'connection/dispatcher'
4
+ require_relative 'connection/response'
5
+
6
+ module Apitizer
7
+ module Connection
8
+ Error = Class.new(Apitizer::Error)
9
+ end
10
+ end
@@ -0,0 +1,13 @@
1
+ require_relative 'adaptor/standard'
2
+
3
+ module Apitizer
4
+ module Connection
5
+ module Adaptor
6
+ def self.build(name)
7
+ self.const_get(name.to_s.capitalize).new
8
+ rescue NameError
9
+ raise Error, 'Unknown connection adaptor'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,34 @@
1
+ require 'net/https'
2
+ require 'uri'
3
+
4
+ module Apitizer
5
+ module Connection
6
+ module Adaptor
7
+ class Standard
8
+ def process(method, address, parameters = {}, headers = {})
9
+ klass = Net::HTTP.const_get(method.to_s.capitalize)
10
+ request = klass.new(build_uri(address, parameters))
11
+ headers.each { |k, v| request[k] = v }
12
+ http = Net::HTTP.new(request.uri.host, request.uri.port)
13
+ http.use_ssl = true if address =~ /^https:/
14
+ response = http.request(request)
15
+ [ response.code, response.to_hash, response.body ]
16
+ rescue NoMethodError
17
+ raise
18
+ rescue NameError
19
+ raise Error, 'Invalid method'
20
+ rescue SocketError
21
+ raise Error, 'Connection failed'
22
+ end
23
+
24
+ private
25
+
26
+ def build_uri(address, parameters)
27
+ chunks = [ address ]
28
+ chunks << Helper.build_query(parameters) unless parameters.empty?
29
+ URI(chunks.join('?'))
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,24 @@
1
+ module Apitizer
2
+ module Connection
3
+ class Dispatcher
4
+ def initialize(adaptor: :standard, dictionary:, headers: {})
5
+ @adaptor = Adaptor.build(adaptor)
6
+ @dictionary = dictionary
7
+ @headers = headers
8
+ end
9
+
10
+ def process(request)
11
+ method = translate(request.action)
12
+ code, _, body = @adaptor.process(method, request.address,
13
+ request.parameters, @headers)
14
+ Response.new(code: code.to_i, body: body)
15
+ end
16
+
17
+ private
18
+
19
+ def translate(action)
20
+ @dictionary[action] or raise Error, 'Unknown action'
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,16 @@
1
+ module Apitizer
2
+ module Connection
3
+ class Request
4
+ extend Forwardable
5
+
6
+ attr_reader :action, :path, :parameters
7
+ def_delegator :path, :address
8
+
9
+ def initialize(action:, path:, parameters: {})
10
+ @action = action
11
+ @path = path
12
+ @parameters = parameters
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ module Apitizer
2
+ module Connection
3
+ class Response
4
+ attr_reader :code, :body
5
+
6
+ def initialize(code:, body:)
7
+ @code = code
8
+ @body = body
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,24 @@
1
+ module Apitizer
2
+ Error = Class.new(StandardError)
3
+
4
+ @defaults = {
5
+ format: :json,
6
+ adaptor: :standard,
7
+ dictionary: {
8
+ :index => :get,
9
+ :show => :get,
10
+ :create => :post,
11
+ :update => :put,
12
+ :delete => :delete
13
+ },
14
+ headers: {}
15
+ }.freeze
16
+
17
+ @actions = [ :index, :show, :create, :update, :delete ].freeze
18
+ @collection_actions = [ :index, :create ].freeze
19
+ @member_actions = [ :show, :update, :delete ].freeze
20
+
21
+ singleton_class.class_eval do
22
+ attr_reader :defaults, :actions, :collection_actions, :member_actions
23
+ end
24
+ end
@@ -0,0 +1,46 @@
1
+ require 'rack/utils'
2
+
3
+ module Apitizer
4
+ module Helper
5
+ Error = Class.new(Apitizer::Error)
6
+
7
+ def self.member_action?(action)
8
+ if Apitizer.member_actions.include?(action)
9
+ true
10
+ elsif Apitizer.collection_actions.include?(action)
11
+ false
12
+ else
13
+ raise Error, 'Unknown action'
14
+ end
15
+ end
16
+
17
+ def self.deep_merge(one, two)
18
+ merger = Proc.new do |key, v1, v2|
19
+ Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2
20
+ end
21
+ one.merge(two, &merger)
22
+ end
23
+
24
+ def self.build_query(parameters)
25
+ Rack::Utils.build_nested_query(prepare_parameters(parameters))
26
+ end
27
+
28
+ private
29
+
30
+ def self.prepare_parameters(parameters)
31
+ # PATCH: https://github.com/rack/rack/issues/557
32
+ Hash[
33
+ parameters.map do |key, value|
34
+ case value
35
+ when Integer, TrueClass, FalseClass
36
+ [ key, value.to_s ]
37
+ when Hash
38
+ [ key, prepare_parameters(value) ]
39
+ else
40
+ [ key, value ]
41
+ end
42
+ end
43
+ ]
44
+ end
45
+ end
46
+ end