apitizer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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