bootic_client 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4311719bc6b53b09e5026c67183f7313c491f48f
4
+ data.tar.gz: 7f85b7946a404ae8b32019cb05df811ce6a60709
5
+ SHA512:
6
+ metadata.gz: 125c0102f1890b005ef8e4ff9ca74f4f597d16c8c542c097edb1e5541ee17c05edc3cd64b0200e7dc61282bd1d5b72dcf8a32e8f3b5f1ce2e0f11ee4907e86f9
7
+ data.tar.gz: f9b42eeb52e5e031b35d3b2d3c25a20408e0753348c60e358b3fd8fa8f198a21918c8958377a809484f65304ff2b60a1209f1081eb8cc36c75da6ab34e45d187
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in bootic_client.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'byebug'
8
+ gem 'vcr', '~> 2.4'
9
+ gem 'webmock', '>= 1.9'
10
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Ismael Celis
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,115 @@
1
+ [![Build Status](https://travis-ci.org/bootic/bootic_client.rb.svg?branch=master)](https://travis-ci.org/bootic/bootic_client.rb)
2
+
3
+ ## WORK IN PROGRESS
4
+
5
+ # BooticClient
6
+
7
+ Official Ruby client for the [Bootic API](https://developers.bootic.net)
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'bootic_client'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install bootic_client
22
+
23
+ ## Usage
24
+
25
+ ### Configure with you app's credentials
26
+
27
+ ```ruby
28
+ BooticClient.configure do |c|
29
+ c.client_id = ENV['BOOTIC_CLIENT_ID']
30
+ c.client_secret = ENV['BOOTIC_CLIENT_SECRET']
31
+ c.logger = Logger.new(STDOUT)
32
+ c.logging = true
33
+ c.cache_store = Rails.cache
34
+ end
35
+ ```
36
+
37
+ ### Using with an existing access token
38
+
39
+ ```ruby
40
+ bootic = BooticClient.client(:authorized, access_token: 'beidjbewjdiedue...', logging: true)
41
+
42
+ root = bootic.root
43
+
44
+ if root.has?(:products)
45
+ # All products
46
+ all_products = root.products(q: 'xmas presents')
47
+ all_products.total_items # => 23443
48
+ all_products.each do |product|
49
+ puts product.title
50
+ puts product.price
51
+ end
52
+
53
+ if all_product.has?(:next)
54
+ next_page = all_products.next
55
+ next_page.each{...}
56
+ end
57
+ end
58
+ ```
59
+
60
+ ## 1. Refresh token flow (web apps)
61
+
62
+ In this flow you first get a token by authorizing an app. ie. using [omniauth-bootic](https://github.com/bootic/omniauth-bootic)
63
+
64
+ ```ruby
65
+ def client
66
+ @client ||= BooticClient.client(:authorized, access_token: session[:access_token]) do |new_token|
67
+ session[:access_token] = new_token
68
+ end
69
+ end
70
+ ```
71
+
72
+
73
+ ## 2. User-less flow (client credentials - automated scripts)
74
+
75
+ ```ruby
76
+ client = BooticClient.client(:client_credentials, scope: 'admin', access_token: some_store[:access_token]) do |new_token|
77
+ some_store[:access_token] = new_token
78
+ end
79
+ ```
80
+
81
+
82
+ ## Cache storage
83
+
84
+ `BooticClient` honours HTTP caching headers included in API responses (such as `ETag` and `Last-Modified`).
85
+
86
+ By default a simple memory store is used. It is recommended that you use a distributed store in production, such as Memcache. In Rails applications you can use the `Rails.cache` interface.
87
+
88
+ ```ruby
89
+ BooticClient.configure do |c|
90
+ ...
91
+ c.cache_store = Rails.cache
92
+ end
93
+ ```
94
+
95
+ Outside of Rails, BooticClient ships with a wrapper around the [Dalli](https://github.com/mperham/dalli) memcache client.
96
+ You must include Dalli in your Gemfile and require the wrapper explicitely.
97
+
98
+ ```ruby
99
+ require 'bootic_client/stores/memcache'
100
+ CACHE_STORE = BooticClient::Stores::Memcache.new(ENV['MEMCACHE_SERVER'])
101
+
102
+ BooticClient.configure do |c|
103
+ ...
104
+ c.cache_store = CACHE_STORE
105
+ end
106
+ ```
107
+
108
+ ## Contributing
109
+
110
+ 1. Fork it
111
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
112
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
113
+ 4. Push to the branch (`git push origin my-new-feature`)
114
+ 5. Create new Pull Request
115
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'bootic_client/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "bootic_client"
8
+ spec.version = BooticClient::VERSION
9
+ spec.authors = ["Ismael Celis"]
10
+ spec.email = ["ismaelct@gmail.com"]
11
+ spec.description = %q{Official Ruby client for the Bootic API}
12
+ spec.summary = %q{Official Ruby client for the Bootic API}
13
+ spec.homepage = "https://developers.bootic.net"
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_dependency "faraday", '~> 0.9'
22
+ spec.add_dependency "uri_template", '~> 0.7'
23
+ spec.add_dependency "faraday_middleware", '~> 0.9'
24
+ spec.add_dependency "faraday-http-cache", '~> 0.4'
25
+ spec.add_dependency "net-http-persistent", '~> 2.9'
26
+ spec.add_dependency "oauth2"
27
+
28
+ spec.add_development_dependency "bundler", "~> 1.3"
29
+ spec.add_development_dependency "rake"
30
+ spec.add_development_dependency "rspec"
31
+ spec.add_development_dependency "jwt"
32
+ spec.add_development_dependency "dalli"
33
+ end
@@ -0,0 +1,73 @@
1
+ require 'faraday'
2
+ require 'faraday_middleware'
3
+ require 'faraday-http-cache'
4
+ require "bootic_client/errors"
5
+ require 'faraday/adapter/net_http_persistent'
6
+
7
+ module BooticClient
8
+
9
+ class Client
10
+
11
+ USER_AGENT = "[BooticClient v#{VERSION}] Ruby-#{RUBY_VERSION} - #{RUBY_PLATFORM}".freeze
12
+
13
+ attr_reader :options, :api_root
14
+
15
+ def initialize(api_root, options = {}, &block)
16
+ @api_root = api_root
17
+ @options = {
18
+ access_token: nil,
19
+ logging: false
20
+ }.merge(options.dup)
21
+
22
+ @options[:cache_store] = @options[:cache_store] || Faraday::HttpCache::MemoryStore.new
23
+
24
+ conn &block if block_given?
25
+ end
26
+
27
+ def get_and_wrap(href, wrapper_class, query = {})
28
+ wrapper_class.new get(href, query).body, self
29
+ end
30
+
31
+ def get(href, query = {})
32
+ validate_request!
33
+
34
+ resp = conn.get do |req|
35
+ req.url href
36
+ req.params.update(query)
37
+ req.headers['Authorization'] = "Bearer #{options[:access_token]}"
38
+ req.headers['User-Agent'] = USER_AGENT
39
+ end
40
+
41
+ raise_if_invalid! resp
42
+
43
+ resp
44
+ end
45
+
46
+ protected
47
+
48
+ def conn(&block)
49
+ @conn ||= Faraday.new(url: api_root) do |f|
50
+ cache_options = {shared_cache: false, store: options[:cache_store]}
51
+ cache_options[:logger] = options[:logger] if options[:logging]
52
+
53
+ f.use :http_cache, cache_options
54
+ f.response :logger, options[:logger] if options[:logging]
55
+ f.response :json
56
+ yield f if block_given?
57
+ f.adapter :net_http_persistent
58
+ end
59
+ end
60
+
61
+ def validate_request!
62
+ raise NoAccessTokenError, "Missing access token" unless options[:access_token]
63
+ end
64
+
65
+ def raise_if_invalid!(resp)
66
+ raise ServerError, "Server Error" if resp.status > 499
67
+ raise NotFoundError, "Not Found" if resp.status == 404
68
+ raise UnauthorizedError, "Unauthorized request" if resp.status == 401
69
+ raise AccessForbiddenError, "Access Forbidden" if resp.status == 403
70
+ end
71
+ end
72
+
73
+ end
@@ -0,0 +1,125 @@
1
+ require "bootic_client/relation"
2
+ require 'ostruct'
3
+
4
+ module BooticClient
5
+ class Entity
6
+
7
+ CURIE_EXP = /(.+):(.+)/.freeze
8
+ CURIES_REL = 'curies'.freeze
9
+ SPECIAL_PROP_EXP = /^_.+/.freeze
10
+
11
+ attr_reader :curies, :entities
12
+
13
+ def initialize(attrs, client, top = self)
14
+ @attrs, @client, @top = attrs, client, top
15
+ build!
16
+ end
17
+
18
+ def [](key)
19
+ properties[key.to_sym]
20
+ end
21
+
22
+ def has?(prop_name)
23
+ has_property?(prop_name) || has_entity?(prop_name) || has_rel?(prop_name)
24
+ end
25
+
26
+ def inspect
27
+ %(#<#{self.class.name} props: [#{properties.keys.join(', ')}] rels: [#{rels.keys.join(', ')}] entities: [#{entities.keys.join(', ')}]>)
28
+ end
29
+
30
+ def properties
31
+ @properties ||= attrs.select{|k,v| !(k =~ SPECIAL_PROP_EXP)}.each_with_object({}) do |(k,v),memo|
32
+ memo[k.to_sym] = Entity.wrap(v)
33
+ end
34
+ end
35
+
36
+ def links
37
+ @links ||= attrs.fetch('_links', {})
38
+ end
39
+
40
+ def self.wrap(obj)
41
+ case obj
42
+ when Hash
43
+ OpenStruct.new(obj)
44
+ when Array
45
+ obj.map{|e| wrap(e)}
46
+ else
47
+ obj
48
+ end
49
+ end
50
+
51
+ def method_missing(name, *args, &block)
52
+ if !block_given?
53
+ if has_property?(name)
54
+ self[name]
55
+ elsif has_entity?(name)
56
+ entities[name]
57
+ elsif has_rel?(name)
58
+ rels[name].get(*args)
59
+ else
60
+ super
61
+ end
62
+ else
63
+ super
64
+ end
65
+ end
66
+
67
+ def respond_to_missing?(method_name, include_private = false)
68
+ has?(method_name)
69
+ end
70
+
71
+ def has_property?(prop_name)
72
+ properties.has_key? prop_name.to_sym
73
+ end
74
+
75
+ def has_entity?(prop_name)
76
+ entities.has_key? prop_name.to_sym
77
+ end
78
+
79
+ def has_rel?(prop_name)
80
+ rels.has_key? prop_name.to_sym
81
+ end
82
+
83
+ def each(&block)
84
+ iterable? ? entities[:items].each(&block) : [self].each(&block)
85
+ end
86
+
87
+ def rels
88
+ @rels ||= (
89
+ links = attrs.fetch('_links', {})
90
+ links.each_with_object({}) do |(rel,rel_attrs),memo|
91
+ if rel =~ CURIE_EXP
92
+ _, curie_namespace, rel = rel.split(CURIE_EXP)
93
+ if curie = curies.find{|c| c['name'] == curie_namespace}
94
+ rel_attrs['docs'] = Relation.expand(curie['href'], rel: rel)
95
+ end
96
+ end
97
+ if rel != CURIES_REL
98
+ rel_attrs['name'] = rel
99
+ memo[rel.to_sym] = Relation.new(rel_attrs, client, Entity)
100
+ end
101
+ end
102
+ )
103
+ end
104
+
105
+ protected
106
+
107
+ attr_reader :client, :top, :attrs
108
+
109
+ def iterable?
110
+ has_entity?(:items) && entities[:items].respond_to?(:each)
111
+ end
112
+
113
+ def build!
114
+ @curies = top.links.fetch('curies', [])
115
+
116
+ @entities = attrs.fetch('_embedded', {}).each_with_object({}) do |(k,v),memo|
117
+ memo[k.to_sym] = if v.kind_of?(Array)
118
+ v.map{|ent_attrs| Entity.new(ent_attrs, client, top)}
119
+ else
120
+ Entity.new(v, client, top)
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,9 @@
1
+ module BooticClient
2
+ class TransportError < StandardError; end
3
+ class ServerError < TransportError; end
4
+ class NotFoundError < ServerError; end
5
+ class TokenError < ServerError; end
6
+ class UnauthorizedError < TokenError; end
7
+ class AccessForbiddenError < TokenError; end
8
+ class NoAccessTokenError < TokenError; end
9
+ end
@@ -0,0 +1,60 @@
1
+ require 'uri_template'
2
+ require "bootic_client/entity"
3
+
4
+ module BooticClient
5
+
6
+ class Relation
7
+
8
+ def initialize(attrs, client, wrapper_class = Entity)
9
+ @attrs, @client, @wrapper_class = attrs, client, wrapper_class
10
+ end
11
+
12
+ def inspect
13
+ %(#<#{self.class.name} #{attrs.inspect}>)
14
+ end
15
+
16
+ def href
17
+ attrs['href']
18
+ end
19
+
20
+ def templated?
21
+ !!attrs['templated']
22
+ end
23
+
24
+ def name
25
+ attrs['name']
26
+ end
27
+
28
+ def title
29
+ attrs['title']
30
+ end
31
+
32
+ def type
33
+ attrs['type']
34
+ end
35
+
36
+ def docs
37
+ attrs['docs']
38
+ end
39
+
40
+ def get(opts = {})
41
+ if templated?
42
+ client.get_and_wrap uri.expand(opts), wrapper_class
43
+ else
44
+ client.get_and_wrap href, wrapper_class, opts
45
+ end
46
+ end
47
+
48
+ def self.expand(href, opts = {})
49
+ URITemplate.new(href).expand(opts)
50
+ end
51
+
52
+ protected
53
+ attr_reader :wrapper_class, :client, :attrs
54
+
55
+ def uri
56
+ @uri ||= URITemplate.new(href)
57
+ end
58
+ end
59
+
60
+ end
@@ -0,0 +1,33 @@
1
+ require 'dalli'
2
+
3
+ module BooticClient
4
+ module Stores
5
+ class Memcache
6
+ attr_reader :client
7
+
8
+ def initialize(server_hosts, dalli_options = {})
9
+ @client = Dalli::Client.new(Array(server_hosts), dalli_options)
10
+ end
11
+
12
+ def read(key)
13
+ @client.get key.to_s
14
+ end
15
+
16
+ def write(key, data, ttl = nil)
17
+ @client.set key.to_s, data, ttl
18
+ end
19
+
20
+ def get(key)
21
+ @client.get key
22
+ end
23
+
24
+ def set(key, data, ttl = nil)
25
+ @client.set key, data, ttl
26
+ end
27
+
28
+ def stats
29
+ @client.stats
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,38 @@
1
+ require 'bootic_client/strategies/strategy'
2
+
3
+ module BooticClient
4
+ module Strategies
5
+
6
+ class Authorized < Strategy
7
+ protected
8
+
9
+ def validate!(options)
10
+ raise "options MUST include access_token" unless options[:access_token]
11
+ end
12
+
13
+ def get_token
14
+ # The JWT grant must have an expiration date, in seconds since the epoch.
15
+ # For most cases a few seconds should be enough.
16
+ exp = Time.now.utc.to_i + 5
17
+
18
+ # Use the "assertion" flow to exchange the JWT grant for an access token
19
+ access_token = auth.assertion.get_token(
20
+ hmac_secret: config.client_secret,
21
+ iss: config.client_id,
22
+ prn: client.options[:access_token],
23
+ aud: 'api',
24
+ exp: exp
25
+ )
26
+
27
+ access_token.token
28
+ end
29
+
30
+ def auth
31
+ @auth ||= OAuth2::Client.new(nil, nil, site: config.auth_host)
32
+ end
33
+ end
34
+
35
+ end
36
+
37
+ strategies[:authorized] = Strategies::Authorized
38
+ end
@@ -0,0 +1,19 @@
1
+ require 'bootic_client/strategies/strategy'
2
+
3
+ module BooticClient
4
+ module Strategies
5
+
6
+ class ClientCredentials < Strategy
7
+ protected
8
+ def get_token
9
+ opts = {}
10
+ opts['scope'] = options.delete(:scope) if options[:scope]
11
+ token = auth.client_credentials.get_token(opts, 'auth_scheme' => 'basic')
12
+ token.token
13
+ end
14
+ end
15
+
16
+ end
17
+
18
+ strategies[:client_credentials] = Strategies::ClientCredentials
19
+ end
@@ -0,0 +1,59 @@
1
+ require 'oauth2'
2
+
3
+ module BooticClient
4
+ module Strategies
5
+ class Strategy
6
+
7
+ def initialize(config, client_opts = {}, &on_new_token)
8
+ @config, @options, @on_new_token = config, client_opts, (on_new_token || Proc.new)
9
+ raise "MUST include client_id" unless config.client_id
10
+ raise "MUST include client_secret" unless config.client_secret
11
+ raise "MUST include api_root" unless config.api_root
12
+ validate! @options
13
+ end
14
+
15
+ def root
16
+ get config.api_root
17
+ end
18
+
19
+ def get(href, query = {})
20
+ begin
21
+ client.get_and_wrap(href, Entity, query)
22
+ rescue TokenError => e
23
+ new_token = get_token
24
+ client.options[:access_token] = new_token
25
+ on_new_token.call new_token
26
+ client.get_and_wrap(href, Entity, query)
27
+ end
28
+ end
29
+
30
+ def inspect
31
+ %(#<#{self.class.name} cid: #{config.client_id} root: #{config.api_root} auth: #{config.auth_host}>)
32
+ end
33
+
34
+ protected
35
+
36
+ attr_reader :config, :options, :on_new_token
37
+
38
+ def validate!(options)
39
+
40
+ end
41
+
42
+ def get_token
43
+ raise "Implement this in subclasses"
44
+ end
45
+
46
+ def auth
47
+ @auth ||= OAuth2::Client.new(
48
+ config.client_id,
49
+ config.client_secret,
50
+ site: config.auth_host
51
+ )
52
+ end
53
+
54
+ def client
55
+ @client ||= Client.new(config.api_root, options)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,3 @@
1
+ module BooticClient
2
+ VERSION = "0.0.1"
3
+ end