typekit-client 0.0.2

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 (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +2 -0
  4. data/CHANGELOG.md +7 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +350 -0
  8. data/Rakefile +7 -0
  9. data/bin/typekit +146 -0
  10. data/lib/typekit.rb +13 -0
  11. data/lib/typekit/client.rb +38 -0
  12. data/lib/typekit/configuration.rb +16 -0
  13. data/lib/typekit/configuration/base.rb +21 -0
  14. data/lib/typekit/configuration/default.rb +34 -0
  15. data/lib/typekit/connection.rb +10 -0
  16. data/lib/typekit/connection/adaptor.rb +13 -0
  17. data/lib/typekit/connection/adaptor/standard.rb +32 -0
  18. data/lib/typekit/connection/dispatcher.rb +17 -0
  19. data/lib/typekit/connection/request.rb +23 -0
  20. data/lib/typekit/connection/response.rb +20 -0
  21. data/lib/typekit/core.rb +16 -0
  22. data/lib/typekit/helper.rb +43 -0
  23. data/lib/typekit/parser.rb +14 -0
  24. data/lib/typekit/parser/json.rb +13 -0
  25. data/lib/typekit/parser/yaml.rb +13 -0
  26. data/lib/typekit/processor.rb +38 -0
  27. data/lib/typekit/routing.rb +9 -0
  28. data/lib/typekit/routing/map.rb +43 -0
  29. data/lib/typekit/routing/node.rb +5 -0
  30. data/lib/typekit/routing/node/base.rb +45 -0
  31. data/lib/typekit/routing/node/collection.rb +34 -0
  32. data/lib/typekit/routing/node/operation.rb +32 -0
  33. data/lib/typekit/routing/node/root.rb +8 -0
  34. data/lib/typekit/routing/node/scope.rb +19 -0
  35. data/lib/typekit/routing/proxy.rb +17 -0
  36. data/lib/typekit/version.rb +3 -0
  37. data/spec/cassettes/index_kits_ok.yml +16 -0
  38. data/spec/cassettes/index_kits_unauthorized.yml +16 -0
  39. data/spec/cassettes/show_families_calluna_found.yml +16 -0
  40. data/spec/spec_helper.rb +20 -0
  41. data/spec/support/rest_helper.rb +22 -0
  42. data/spec/typekit/client_spec.rb +36 -0
  43. data/spec/typekit/configuration_spec.rb +34 -0
  44. data/spec/typekit/connection/adaptor_spec.rb +24 -0
  45. data/spec/typekit/connection/dispatcher_spec.rb +36 -0
  46. data/spec/typekit/connection/request_spec.rb +13 -0
  47. data/spec/typekit/helper_spec.rb +93 -0
  48. data/spec/typekit/processor_spec.rb +34 -0
  49. data/spec/typekit/routing/map_spec.rb +106 -0
  50. data/spec/typekit/routing/node_spec.rb +40 -0
  51. data/typekit-client.gemspec +33 -0
  52. metadata +208 -0
@@ -0,0 +1,5 @@
1
+ require_relative 'node/base'
2
+ require_relative 'node/root'
3
+ require_relative 'node/scope'
4
+ require_relative 'node/collection'
5
+ require_relative 'node/operation'
@@ -0,0 +1,45 @@
1
+ module Typekit
2
+ module Routing
3
+ module Node
4
+ class Base
5
+ def append(child)
6
+ children << child
7
+ end
8
+
9
+ def assemble(request, path)
10
+ process(request, path)
11
+ return authorize(request) if path.empty?
12
+ lookup!(path.first).assemble(request, path)
13
+ end
14
+
15
+ def match(name)
16
+ end
17
+
18
+ def process(request, path)
19
+ end
20
+
21
+ def permitted?(request)
22
+ end
23
+
24
+ protected
25
+
26
+ def children
27
+ @children ||= []
28
+ end
29
+
30
+ def lookup(name)
31
+ children.find { |c| c.match(name) }
32
+ end
33
+
34
+ def lookup!(name)
35
+ lookup(name) or raise Error, 'Not found'
36
+ end
37
+
38
+ def authorize(request)
39
+ raise Error, 'Not permitted' unless permitted?(request)
40
+ request
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,34 @@
1
+ module Typekit
2
+ module Routing
3
+ module Node
4
+ class Collection < Base
5
+ def initialize(name, only: nil)
6
+ @name = name
7
+ @actions = only && Array(only) || Typekit.actions
8
+ unless (@actions - Typekit.actions).empty?
9
+ raise Error, 'Not supported'
10
+ end
11
+ end
12
+
13
+ def match(name)
14
+ @name == name
15
+ end
16
+
17
+ def process(request, path)
18
+ request << path.shift # @name
19
+ return request if path.empty?
20
+ request << path.shift # id
21
+ end
22
+
23
+ def permitted?(request)
24
+ return false unless @actions.include?(request.action)
25
+
26
+ id_present = request.path.last != @name
27
+ member_action = Helper.member_action?(request.action)
28
+
29
+ id_present == member_action
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,32 @@
1
+ module Typekit
2
+ module Routing
3
+ module Node
4
+ class Operation < Base
5
+ def initialize(name, action:, on:, **options)
6
+ # TODO: how about on == :collection?
7
+ unless Typekit.actions.include?(action) && on == :member
8
+ raise Error, 'Not supported'
9
+ end
10
+ @name = name
11
+ @action = action
12
+ end
13
+
14
+ def match(name)
15
+ if @name.is_a?(String) && @name =~ /^:/
16
+ true
17
+ else
18
+ @name == name
19
+ end
20
+ end
21
+
22
+ def process(request, path)
23
+ request << path.shift # @name
24
+ end
25
+
26
+ def permitted?(request)
27
+ @action == request.action
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,8 @@
1
+ module Typekit
2
+ module Routing
3
+ module Node
4
+ class Root < Base
5
+ end
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,19 @@
1
+ module Typekit
2
+ module Routing
3
+ module Node
4
+ class Scope < Base
5
+ def initialize(path)
6
+ @path = Array(path)
7
+ end
8
+
9
+ def match(name)
10
+ !lookup(name).nil?
11
+ end
12
+
13
+ def process(request, path)
14
+ @path.each { |chunk| request << chunk }
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ module Typekit
2
+ module Routing
3
+ class Proxy
4
+ def initialize(owner, **options)
5
+ @owner = owner
6
+ @options = options
7
+ end
8
+
9
+ def method_missing(name, *arguments, **options, &block)
10
+ name = :"define_#{ name }"
11
+ return super unless @owner.respond_to?(name)
12
+ # NOTE: https://bugs.ruby-lang.org/issues/9776
13
+ @owner.send(name, *arguments, **options, **@options, &block)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module Typekit
2
+ VERSION = '0.0.2'
3
+ end
@@ -0,0 +1,16 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: get
5
+ uri: https://typekit.com/api/v1/json/kits
6
+ response:
7
+ status:
8
+ code: 200
9
+ message: OK
10
+ headers:
11
+ Status:
12
+ - 200 OK
13
+ body:
14
+ encoding: UTF-8
15
+ string: '{"kits":[{"id":"bas4cfe","link":"/api/v1/json/kits/bas4cfe"},{"id":"sfh6bkj","link":"/api/v1/json/kits/sfh6bkj"},{"id":"kof8zcn","link":"/api/v1/json/kits/kof8zcn"},{"id":"muq6mxx","link":"/api/v1/json/kits/muq6mxx"}]}'
16
+ recorded_at: Fri, 23 May 2014 00:00:00 GMT
@@ -0,0 +1,16 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: get
5
+ uri: https://typekit.com/api/v1/json/kits
6
+ response:
7
+ status:
8
+ code: 401
9
+ message: Unauthorized
10
+ headers:
11
+ Status:
12
+ - 401 Unauthorized
13
+ body:
14
+ encoding: UTF-8
15
+ string: '{"errors":["Not authorized"]}'
16
+ recorded_at: Fri, 23 May 2014 00:00:00 GMT
@@ -0,0 +1,16 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: get
5
+ uri: https://typekit.com/api/v1/json/families/calluna
6
+ response:
7
+ status:
8
+ code: 302
9
+ message: Found
10
+ headers:
11
+ Status:
12
+ - 302 Found
13
+ body:
14
+ encoding: UTF-8
15
+ string: '{"family":{"id":"vybr","link":"/api/v1/json/families/vybr"}}'
16
+ recorded_at: Tue, 27 May 2014 00:00:00 GMT
@@ -0,0 +1,20 @@
1
+ require_relative 'support/rest_helper'
2
+ require 'webmock/rspec'
3
+ require 'vcr'
4
+
5
+ RSpec.configure do |config|
6
+ config.treat_symbols_as_metadata_keys_with_true_values = true
7
+ config.run_all_when_everything_filtered = true
8
+ config.filter_run :focus
9
+ config.order = 'random'
10
+ end
11
+
12
+ VCR.configure do |config|
13
+ config.cassette_library_dir = 'spec/cassettes'
14
+ config.hook_into :webmock
15
+ config.configure_rspec_metadata!
16
+ config.default_cassette_options = {
17
+ allow_unused_http_interactions: false
18
+ }
19
+ config.allow_http_connections_when_no_cassette = false
20
+ end
@@ -0,0 +1,22 @@
1
+ module RESTHelper
2
+ def restful_actions
3
+ # TODO: keep in sync with Typekit.actions?
4
+ [ :index, :show, :create, :update, :delete ]
5
+ end
6
+
7
+ def restful_collection_actions
8
+ # TODO: keep in sync with Typekit.collection_actions?
9
+ [ :index, :create ]
10
+ end
11
+
12
+ def restful_member_actions
13
+ # TODO: keep in sync with Typekit.member_actions?
14
+ [ :show, :update, :delete ]
15
+ end
16
+
17
+ def rest_http_dictionary
18
+ # TODO: keep in sync with Typekit.action_dictionary?
19
+ { :index => :get, :show => :get, :create => :post,
20
+ :update => :post, :delete => :delete }
21
+ end
22
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+ require 'typekit'
3
+
4
+ describe Typekit::Client do
5
+ let(:token) { 'arbitrary' }
6
+ let(:subject) { Typekit::Client.new(token: token) }
7
+
8
+ describe '#index' do
9
+ context 'when successful' do
10
+ options = { vcr: { cassette_name: 'index_kits_ok' } }
11
+
12
+ it 'returns hashes', options do
13
+ result = subject.index(:kits)
14
+ expect(result).to be_kind_of(Hash)
15
+ end
16
+ end
17
+
18
+ context 'when unauthorized' do
19
+ options = { vcr: { cassette_name: 'index_kits_unauthorized' } }
20
+
21
+ it 'raises exceptions', options do
22
+ expect { subject.index(:kits) }.to \
23
+ raise_error(Typekit::Error, /(authentication|authorized)/i)
24
+ end
25
+ end
26
+
27
+ context 'when found' do
28
+ options = { vcr: { cassette_name: 'show_families_calluna_found' } }
29
+
30
+ it 'returns the Response as is', options do
31
+ expect { subject.show(:families, 'calluna') }.not_to \
32
+ raise_error
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+ require 'typekit'
3
+
4
+ describe Typekit::Configuration do
5
+ let(:subject_module) { Typekit::Configuration }
6
+
7
+ def build(name, **options)
8
+ subject_module.build(name, **options)
9
+ end
10
+
11
+ describe '.build' do
12
+ it 'requies an API token to be given' do
13
+ expect { build(:default) }.to \
14
+ raise_error(subject_module::Error, /Not enough arguments/i)
15
+ end
16
+
17
+ it 'returns registeries of settings' do
18
+ expect(build(:default, token: 'nekot')).to \
19
+ be_kind_of(subject_module::Base)
20
+ end
21
+
22
+ it 'raises exceptions when encouters unknown registeries' do
23
+ expect { build(:awesome, token: 'nekot') }.to \
24
+ raise_error(subject_module::Error, /Unknown configuration/i)
25
+ end
26
+ end
27
+
28
+ describe 'Base#map' do
29
+ it 'returns a Map' do
30
+ expect(build(:default, token: 'nekot').map).to \
31
+ be_kind_of(Typekit::Routing::Map)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+ require 'typekit'
3
+
4
+ describe Typekit::Connection::Adaptor do
5
+ let(:address) { 'https://typekit.com/api/v1/json/kits' }
6
+
7
+ [ 'Standard' ].each do |adaptor|
8
+ subject { Typekit::Connection::Adaptor.const_get(adaptor).new }
9
+
10
+ describe "#{ adaptor }.process" do
11
+ it 'returns the code, headers, and body of the response' do
12
+ stub_http_request(:get, address).to_return(
13
+ code: '200', body: 'Hej!', headers: { 'a' => 'b' } )
14
+ response = subject.process(:get, address)
15
+ expect(response).to eq([ '200', { 'a' => [ 'b' ] }, 'Hej!' ])
16
+ end
17
+
18
+ it 'raises exceptions when encounters unknown methods' do
19
+ expect { subject.process(:smile, address) }.to \
20
+ raise_error(Typekit::Connection::Error, /Invalid method/i)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+ require 'typekit'
3
+
4
+ describe Typekit::Connection::Dispatcher do
5
+ extend RESTHelper
6
+
7
+ let(:token) { 'arbitrary' }
8
+ let(:address) { 'https://typekit.com/api/v1/json/kits' }
9
+ let(:subject) { Typekit::Connection::Dispatcher.new(token: token) }
10
+
11
+ def create_request(action)
12
+ double('Request', action: action, address: address, parameters: {})
13
+ end
14
+
15
+ describe '#deliver' do
16
+ restful_actions.each do |action|
17
+ method = rest_http_dictionary[action]
18
+
19
+ context "when sending #{ action } Requests" do
20
+ it 'sets the token header' do
21
+ stub = stub_http_request(method, address)
22
+ response = subject.deliver(create_request(action))
23
+ expect(stub).to have_requested(method, address).
24
+ with(:headers => { 'X-Typekit-Token' => token })
25
+ end
26
+
27
+ it 'returns Responses' do
28
+ stub_http_request(method, address).
29
+ to_return(code: '200', body: 'Hej!')
30
+ response = subject.deliver(create_request(action))
31
+ expect([ response.code, response.content ]).to eq([ 200, 'Hej!' ])
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+ require 'typekit'
3
+
4
+ describe Typekit::Connection::Request do
5
+ subject { Typekit::Connection::Request.new(action: :show) }
6
+
7
+ describe '#address' do
8
+ it 'assembles the address' do
9
+ [ :kits, 'xxx', :families, 'yyy' ].each { |chunk| subject << chunk }
10
+ expect(subject.address).to eq('kits/xxx/families/yyy')
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,93 @@
1
+ require 'spec_helper'
2
+ require 'typekit'
3
+
4
+ describe Typekit::Helper do
5
+ extend RESTHelper
6
+
7
+ let(:subject_module) { Typekit::Helper }
8
+
9
+ describe '.member_action?' do
10
+ restful_member_actions.each do |action|
11
+ it "returns true for the #{ action } member action" do
12
+ expect(subject_module.member_action?(action)).to be_true
13
+ end
14
+ end
15
+
16
+ restful_collection_actions.each do |action|
17
+ it "returns false for the #{ action } collection action" do
18
+ expect(subject_module.member_action?(action)).to be_false
19
+ end
20
+ end
21
+
22
+ it 'raises exceptions when encounters unknown actions' do
23
+ expect { subject_module.member_action?(:rock) }.to \
24
+ raise_error(Typekit::Helper::Error, /Unknown action/i)
25
+ end
26
+ end
27
+
28
+ describe '.translate_action' do
29
+ rest_http_dictionary.each do |action, method|
30
+ it "returns the #{ method } verb for the #{ action } action" do
31
+ expect(subject_module.translate_action(action)).to eq(method)
32
+ end
33
+ end
34
+
35
+ it 'raises exceptions when encounters unknown actions' do
36
+ expect { subject_module.translate_action(:rock) }.to \
37
+ raise_error(Typekit::Helper::Error, /Unknown action/i)
38
+ end
39
+ end
40
+
41
+ describe '.build_query' do
42
+ it 'handels ordinary parameters' do
43
+ queries = [
44
+ 'name=Megakit&domains=localhost',
45
+ 'domains=localhost&name=Megakit'
46
+ ]
47
+ query = subject_module.build_query(
48
+ name: 'Megakit', domains: 'localhost')
49
+ expect(queries).to include(query)
50
+ end
51
+
52
+ it 'handles parameters whose values are ordinary lists' do
53
+ query = subject_module.build_query(
54
+ domains: [ 'example.com', 'example.net' ])
55
+ expect(query).to eq('domains[]=example.com&domains[]=example.net')
56
+ end
57
+
58
+ it 'handles parameters whose values are object lists' do
59
+ queries = [
60
+ 'families[0][id]=gkmg&families[1][id]=asdf',
61
+ 'families[1][id]=asdf&families[0][id]=gkmg'
62
+ ]
63
+ query = subject_module.build_query(
64
+ families: { 0 => { id: 'gkmg' }, 1 => { id: 'asdf' } })
65
+ expect(queries).to include(query)
66
+ end
67
+
68
+ it 'converts integers to decimal strings' do
69
+ query = subject_module.build_query(page: 42)
70
+ expect(query).to eq('page=42')
71
+ end
72
+
73
+ it 'converts integers in object lists to decimal strings' do
74
+ queries = [
75
+ 'primes[0][value]=2&primes[1][value]=3',
76
+ 'primes[1][value]=3&primes[0][value]=2'
77
+ ]
78
+ query = subject_module.build_query(
79
+ primes: { 0 => { value: 2 }, 1 => { value: 3 } })
80
+ expect(queries).to include(query)
81
+ end
82
+
83
+ it 'converts the logical true to the string true' do
84
+ query = subject_module.build_query(badge: true)
85
+ expect(query).to eq('badge=true')
86
+ end
87
+
88
+ it 'converts the logical false to the string false' do
89
+ query = subject_module.build_query(badge: false)
90
+ expect(query).to eq('badge=false')
91
+ end
92
+ end
93
+ end