typekitable 0.1.0
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 +22 -0
- data/.ruby-version +1 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +38 -0
- data/Rakefile +13 -0
- data/bin/typekitable +6 -0
- data/features/authentication.feature +13 -0
- data/features/kits.feature +22 -0
- data/features/step_definitions/kit_steps.rb +5 -0
- data/features/support/env.rb +15 -0
- data/lib/typekitable.rb +11 -0
- data/lib/typekitable/authenticator.rb +22 -0
- data/lib/typekitable/commander.rb +51 -0
- data/lib/typekitable/request.rb +66 -0
- data/lib/typekitable/request_fetcher.rb +72 -0
- data/lib/typekitable/response_formatter.rb +120 -0
- data/lib/typekitable/tokenizer.rb +23 -0
- data/lib/typekitable/version.rb +3 -0
- data/spec/fixtures/vcr_cassettes/add_kit.yml +63 -0
- data/spec/fixtures/vcr_cassettes/add_kit_maximum_error.yml +61 -0
- data/spec/fixtures/vcr_cassettes/add_kit_unauthorized_error.yml +61 -0
- data/spec/fixtures/vcr_cassettes/kits.yml +61 -0
- data/spec/fixtures/vcr_cassettes/kits_error.yml +59 -0
- data/spec/fixtures/vcr_cassettes/libraries.yml +61 -0
- data/spec/lib/typekitable/commander_spec.rb +17 -0
- data/spec/lib/typekitable/request_fetcher_spec.rb +67 -0
- data/spec/lib/typekitable/request_spec.rb +151 -0
- data/spec/lib/typekitable/response_formatter_spec.rb +199 -0
- data/spec/lib/typekitable/tokenizer_spec.rb +48 -0
- data/spec/spec_helper.rb +23 -0
- data/typekitable.gemspec +24 -0
- metadata +149 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 07bb313e5d918cc2e3eb1315fd657baa888d4368
|
4
|
+
data.tar.gz: 9862a8897cc8be29054bb2b05d4509198df538fe
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a5022b0861150976b4afa6a2a74659521f5bdba0c9194fc3a575dea2db1ac535b69bf4eb9ff45455c14743ff63581d55466b4aa45cc557b956fbfd3a902c1cf7
|
7
|
+
data.tar.gz: 828707e0d58dcb70564740b6637c5782a80e8ec5a73a3a0ef267b21f7987cf70803f978dacbf2f39e9603ab2eb3a4b2064b2c67a3e92e194f9e85279e3b3e826
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
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
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.2.0
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Persa Zula
|
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,38 @@
|
|
1
|
+
# Typekitable
|
2
|
+
|
3
|
+
A CLI for interacting with your Typekit kits.
|
4
|
+
|
5
|
+
Get an API token [here](https://typekit.com/account/tokens) and get started!
|
6
|
+
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Install it yourself as:
|
11
|
+
|
12
|
+
$ gem install typekitable
|
13
|
+
|
14
|
+
## Usage
|
15
|
+
|
16
|
+
Command | Description
|
17
|
+
------------------------------- |-------------------------------------
|
18
|
+
typekitable kit_add NAME DOMAINS| # Adds a new kit
|
19
|
+
typekitable kit_info KIT_ID | # Get information on a specific kit
|
20
|
+
typekitable kit_list | # Get a list of all of your kits
|
21
|
+
typekitable kit_publish KIT_ID | # Publish a draft kit
|
22
|
+
typekitable re-authenticate | # Reset your Typekit API token
|
23
|
+
|
24
|
+
## Tests
|
25
|
+
|
26
|
+
Unit tests are written with [rspec](https://github.com/rspec/rspec) and can be run with the command `rake spec`
|
27
|
+
|
28
|
+
Feature tests are written with [cucumber](https://github.com/cucumber/cucumber) and [aruba](https://github.com/cucumber/aruba) and can be run with the command `rake features`
|
29
|
+
|
30
|
+
|
31
|
+
## Contributing
|
32
|
+
|
33
|
+
1. Fork it ( https://github.com/pzula/typekitable/fork )
|
34
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
35
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
36
|
+
4. Add spec coverage
|
37
|
+
5. Push to the branch (`git push origin my-new-feature`)
|
38
|
+
6. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'cucumber'
|
4
|
+
require 'cucumber/rake/task'
|
5
|
+
require 'rspec/core/rake_task'
|
6
|
+
|
7
|
+
Cucumber::Rake::Task.new(:features) do |t|
|
8
|
+
t.cucumber_opts = "features --format pretty"
|
9
|
+
end
|
10
|
+
|
11
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
12
|
+
spec.rspec_opts = "--format documentation --color"
|
13
|
+
end
|
data/bin/typekitable
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
@mock_home_directory
|
2
|
+
Feature: As a user, I can enter my API token credentials
|
3
|
+
|
4
|
+
Scenario: Invoking the CLI to re-authenticate with a token results in success
|
5
|
+
Given I run `bundle exec ruby bin/typekitable re-authenticate` interactively
|
6
|
+
And I type "y"
|
7
|
+
And I type "4d6141e7c82cb30affebcc392abc2ce3ab0ea4c1"
|
8
|
+
Then the output should contain "Thank you, your Typekit API token has been stored"
|
9
|
+
|
10
|
+
Scenario: Invoking the CLI to re-authenticate without a token gives me API token generation information
|
11
|
+
Given I run `bundle exec ruby bin/typekitable re-authenticate` interactively
|
12
|
+
And I type "n"
|
13
|
+
Then the output should contain "Generate an API token"
|
@@ -0,0 +1,22 @@
|
|
1
|
+
@mock_home_directory
|
2
|
+
Feature: As a user with a stored token, I can interact with the commands
|
3
|
+
|
4
|
+
Background:
|
5
|
+
* I have a stored token
|
6
|
+
|
7
|
+
Scenario: I can get a list of all of my commands
|
8
|
+
Given I run `bundle exec ruby bin/typekitable help`
|
9
|
+
Then the output should contain "Commands"
|
10
|
+
|
11
|
+
Scenario: I can get a list of my kits
|
12
|
+
Given I run `bundle exec ruby bin/typekitable kit_list`
|
13
|
+
Then the output should contain "rmc4kry"
|
14
|
+
|
15
|
+
Scenario: I can get information about a kit
|
16
|
+
Given I run `bundle exec ruby bin/typekitable kit_info "rmc4kry"`
|
17
|
+
Then the output should contain "droid kit"
|
18
|
+
|
19
|
+
Scenario: I can publish a draft kit
|
20
|
+
Given I run `bundle exec ruby bin/typekitable kit_publish "rmc4kry"`
|
21
|
+
Then the output should contain "published"
|
22
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'cucumber'
|
2
|
+
require 'aruba/cucumber'
|
3
|
+
|
4
|
+
Before do
|
5
|
+
@dirs = [File.expand_path(File.join(File.dirname(__FILE__), '../..'))]
|
6
|
+
end
|
7
|
+
|
8
|
+
Before '@mocked_home_directory' do
|
9
|
+
set_env 'HOME', File.expand_path(File.join(current_dir, 'home'))
|
10
|
+
FileUtils.mkdir_p ENV['HOME']
|
11
|
+
end
|
12
|
+
|
13
|
+
After '@mocked_home_directory' do
|
14
|
+
FileUtils.rm_f ENV['HOME']
|
15
|
+
end
|
data/lib/typekitable.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require "thor"
|
2
|
+
require "net/http"
|
3
|
+
require "formatador"
|
4
|
+
require "json"
|
5
|
+
require "typekitable/version"
|
6
|
+
require "typekitable/commander"
|
7
|
+
require "typekitable/tokenizer"
|
8
|
+
require "typekitable/authenticator"
|
9
|
+
require "typekitable/request_fetcher"
|
10
|
+
require "typekitable/request"
|
11
|
+
require "typekitable/response_formatter"
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Typekitable
|
2
|
+
class Authenticator < Thor
|
3
|
+
include Thor::Actions
|
4
|
+
|
5
|
+
no_commands do
|
6
|
+
def get_authentication
|
7
|
+
if no?("Do you have a Typekit API token? [y/N]", :yellow)
|
8
|
+
say("Generate an API token at https://typekit.com/account/tokens and come back once you have it, we will only ask for it once", :red)
|
9
|
+
exit
|
10
|
+
else
|
11
|
+
request_token
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def request_token
|
16
|
+
token = ask("What is your Typekit API token?:", :red)
|
17
|
+
Tokenizer.store(token)
|
18
|
+
say("Thank you, your Typekit API token has been stored.", :green)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Typekitable
|
2
|
+
class Commander < Thor
|
3
|
+
include Thor::Actions
|
4
|
+
|
5
|
+
desc "re-authenticate", "Reset your Typekit API token"
|
6
|
+
def re_authenticate
|
7
|
+
Authenticator.new.get_authentication
|
8
|
+
invoke :help
|
9
|
+
end
|
10
|
+
|
11
|
+
desc "kit_list", "Get a list of all of your kits"
|
12
|
+
def kit_list
|
13
|
+
result = formatted_response(response_for_command(:kit_list))
|
14
|
+
result.output_heading
|
15
|
+
result.output_body
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "kit_add NAME DOMAINS", "Adds a new kit"
|
19
|
+
def kit_add(name, domains)
|
20
|
+
response = response_for_command(:kit_add, { :name => name, :domains => [domains] })
|
21
|
+
result = formatted_response(response)
|
22
|
+
result.output_heading
|
23
|
+
result.output_body
|
24
|
+
end
|
25
|
+
|
26
|
+
desc "kit_info KIT_ID", "Get information on a specific kit"
|
27
|
+
def kit_info(kit_id)
|
28
|
+
result = formatted_response(response_for_command(:kit_info, {}, kit_id))
|
29
|
+
result.output_heading
|
30
|
+
result.output_body
|
31
|
+
end
|
32
|
+
|
33
|
+
desc "kit_publish KIT_ID", "Publish a draft kit"
|
34
|
+
def kit_publish(kit_id)
|
35
|
+
result = formatted_response(response_for_command(:kit_publish, {}, kit_id))
|
36
|
+
result.output_heading
|
37
|
+
result.output_body
|
38
|
+
end
|
39
|
+
|
40
|
+
no_commands do
|
41
|
+
def response_for_command(command_type, options = {}, resource_id = nil)
|
42
|
+
RequestFetcher.new(command_type, options, resource_id).response
|
43
|
+
end
|
44
|
+
|
45
|
+
def formatted_response(response)
|
46
|
+
ResponseFormatter.new(response)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Typekitable
|
2
|
+
class Request
|
3
|
+
attr_reader :path, :verb, :parameters
|
4
|
+
|
5
|
+
BASE_URL = "https://typekit.com/api/v1/json/"
|
6
|
+
|
7
|
+
def initialize(path, verb, parameters)
|
8
|
+
@path = path
|
9
|
+
@verb = verb
|
10
|
+
@parameters = parameters
|
11
|
+
end
|
12
|
+
|
13
|
+
def token
|
14
|
+
Tokenizer.get_token
|
15
|
+
end
|
16
|
+
|
17
|
+
def response
|
18
|
+
case verb
|
19
|
+
when "GET" then get_request_response
|
20
|
+
when "POST" then post_request_response
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def uri
|
25
|
+
URI.parse(BASE_URL + path)
|
26
|
+
end
|
27
|
+
|
28
|
+
def headers
|
29
|
+
{ "X-Typekit-Token" => token }
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def http_request
|
35
|
+
Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') do |https|
|
36
|
+
yield(https, uri)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def get_request_response
|
41
|
+
http_request do |https, uri|
|
42
|
+
response = https.get(uri.path, headers)
|
43
|
+
build_response(response.code, response.message, response.body)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def post_request_response
|
48
|
+
req = Net::HTTP::Post.new(uri.path, headers)
|
49
|
+
req.set_form_data(parameters)
|
50
|
+
http_request do |https, uri|
|
51
|
+
response = https.request(req)
|
52
|
+
build_response(response.code, response.message, response.body)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def build_response(code, message, body)
|
57
|
+
Response.new(:code => code, :message => message, :body => body)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class Response < Struct.new(:code, :message, :body)
|
62
|
+
def initialize(from_hash)
|
63
|
+
super(*from_hash.values_at(*members))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Typekitable
|
2
|
+
class RequestFetcher
|
3
|
+
attr_reader :type, :options, :resource_id
|
4
|
+
|
5
|
+
VALID_TYPES = [:kit_list, :kit_add, :kit_info, :kit_publish]
|
6
|
+
VALID_OPTIONS = {
|
7
|
+
:kit_add => [:name, :domains]
|
8
|
+
}
|
9
|
+
|
10
|
+
def initialize(type, options = {}, resource_id = nil)
|
11
|
+
@type = validate_type(type)
|
12
|
+
@options = validate_options(options)
|
13
|
+
@resource_id = resource_id
|
14
|
+
end
|
15
|
+
|
16
|
+
def request_path
|
17
|
+
request_config[type][:request_path]
|
18
|
+
end
|
19
|
+
|
20
|
+
def verb
|
21
|
+
request_config[type][:verb]
|
22
|
+
end
|
23
|
+
|
24
|
+
def parameters
|
25
|
+
options
|
26
|
+
end
|
27
|
+
|
28
|
+
def response
|
29
|
+
Request.new(request_path, verb, parameters).response
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def request_config
|
35
|
+
{
|
36
|
+
:kit_list => {
|
37
|
+
:request_path => "kits",
|
38
|
+
:verb => "GET"
|
39
|
+
},
|
40
|
+
:kit_add => {
|
41
|
+
:request_path => "kits",
|
42
|
+
:verb => "POST"
|
43
|
+
},
|
44
|
+
:kit_info => {
|
45
|
+
:request_path => "kits/#{resource_id}",
|
46
|
+
:verb => "GET"
|
47
|
+
},
|
48
|
+
:kit_publish => {
|
49
|
+
:request_path => "kits/#{resource_id}/publish",
|
50
|
+
:verb => "POST"
|
51
|
+
}
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def validate_type(type)
|
56
|
+
raise InvalidTypeError unless VALID_TYPES.include?(type)
|
57
|
+
|
58
|
+
return type
|
59
|
+
end
|
60
|
+
|
61
|
+
def validate_options(options_list)
|
62
|
+
if options_list.empty? || VALID_OPTIONS[:kit_add].all? {|element| options_list.keys.include?(element) }
|
63
|
+
return options_list
|
64
|
+
else
|
65
|
+
raise InvalidOptionsError
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class InvalidTypeError < StandardError; end
|
71
|
+
class InvalidOptionsError < StandardError; end
|
72
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Typekitable
|
2
|
+
class ResponseFormatter
|
3
|
+
attr_reader :response
|
4
|
+
|
5
|
+
ERRORS = {
|
6
|
+
"400" => "Error in data sent",
|
7
|
+
"401" => "Not authorized",
|
8
|
+
"403" => "Rate limit reached",
|
9
|
+
"404" => "Not found",
|
10
|
+
"500" => "Unable to process request",
|
11
|
+
"503" => "Service is offline"
|
12
|
+
}
|
13
|
+
|
14
|
+
def initialize(response)
|
15
|
+
@response = response
|
16
|
+
end
|
17
|
+
|
18
|
+
def error?
|
19
|
+
ERRORS.keys.include?(response.code)
|
20
|
+
end
|
21
|
+
|
22
|
+
def parsed_body
|
23
|
+
JSON.parse(response.body, :symbolize_names => true)
|
24
|
+
end
|
25
|
+
|
26
|
+
def table_headers
|
27
|
+
if error?
|
28
|
+
error_key
|
29
|
+
elsif collection?
|
30
|
+
collection_headers
|
31
|
+
elsif singular_resource?
|
32
|
+
singular_resource_headers
|
33
|
+
else
|
34
|
+
[main_key]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def table_body
|
39
|
+
if error?
|
40
|
+
error_message
|
41
|
+
elsif collection?
|
42
|
+
collection_data
|
43
|
+
elsif singular_resource?
|
44
|
+
singular_resource_data
|
45
|
+
else
|
46
|
+
[parsed_body]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def data_heading
|
51
|
+
main_key.to_s.capitalize
|
52
|
+
end
|
53
|
+
|
54
|
+
def output_heading
|
55
|
+
display_line(data_heading)
|
56
|
+
end
|
57
|
+
|
58
|
+
def output_body
|
59
|
+
display_table(table_body, table_headers)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def data_element
|
65
|
+
parsed_body[main_key]
|
66
|
+
end
|
67
|
+
|
68
|
+
def singular_resource?
|
69
|
+
!error? && data_element.is_a?(Hash)
|
70
|
+
end
|
71
|
+
|
72
|
+
def collection?
|
73
|
+
!error? && data_element.is_a?(Array)
|
74
|
+
end
|
75
|
+
|
76
|
+
def empty_response?
|
77
|
+
data_element.any?
|
78
|
+
end
|
79
|
+
|
80
|
+
def main_key
|
81
|
+
parsed_body.keys.first
|
82
|
+
end
|
83
|
+
|
84
|
+
def collection_headers
|
85
|
+
data_element.map do |data|
|
86
|
+
data.keys
|
87
|
+
end.flatten.uniq
|
88
|
+
end
|
89
|
+
|
90
|
+
def collection_data
|
91
|
+
data_element.map do |line|
|
92
|
+
line
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def singular_resource_headers
|
97
|
+
data_element.keys
|
98
|
+
end
|
99
|
+
|
100
|
+
def singular_resource_data
|
101
|
+
[data_element]
|
102
|
+
end
|
103
|
+
|
104
|
+
def error_key
|
105
|
+
[response.code]
|
106
|
+
end
|
107
|
+
|
108
|
+
def error_message
|
109
|
+
[{response.code => data_element.pop}]
|
110
|
+
end
|
111
|
+
|
112
|
+
def display_line(line)
|
113
|
+
Formatador.display_line(line)
|
114
|
+
end
|
115
|
+
|
116
|
+
def display_table(table_data, headers)
|
117
|
+
Formatador.display_table(table_data, headers)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|