zensana 1.0.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 +9 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/Guardfile +55 -0
- data/LICENCE.txt +22 -0
- data/README.md +165 -0
- data/Rakefile +2 -0
- data/bin/zensana +7 -0
- data/lib/zensana/cli.rb +8 -0
- data/lib/zensana/command.rb +7 -0
- data/lib/zensana/commands/project.rb +293 -0
- data/lib/zensana/models/asana/attachment.rb +62 -0
- data/lib/zensana/models/asana/project.rb +84 -0
- data/lib/zensana/models/asana/task.rb +86 -0
- data/lib/zensana/models/asana/user.rb +35 -0
- data/lib/zensana/models/zendesk/attachment.rb +38 -0
- data/lib/zensana/models/zendesk/comment.rb +34 -0
- data/lib/zensana/models/zendesk/ticket.rb +73 -0
- data/lib/zensana/models/zendesk/user.rb +96 -0
- data/lib/zensana/services/asana.rb +48 -0
- data/lib/zensana/services/error.rb +55 -0
- data/lib/zensana/services/response.rb +49 -0
- data/lib/zensana/services/zendesk.rb +54 -0
- data/lib/zensana/validate/key.rb +44 -0
- data/lib/zensana/version.rb +3 -0
- data/lib/zensana.rb +25 -0
- data/zensana.gemspec +31 -0
- metadata +186 -0
@@ -0,0 +1,84 @@
|
|
1
|
+
module Zensana
|
2
|
+
class Asana
|
3
|
+
class Project
|
4
|
+
include Zensana::Asana::Access
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
def list
|
9
|
+
@@list ||= Zensana::Asana.inst.fetch '/projects'
|
10
|
+
end
|
11
|
+
|
12
|
+
def search(spec)
|
13
|
+
list.select { |p| p.to_s =~ %r{#{spec}} }
|
14
|
+
rescue RegexpError
|
15
|
+
raise BadSearch, "'#{spec}' is not a valid regular expression"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :attributes
|
20
|
+
|
21
|
+
def initialize(spec)
|
22
|
+
@attributes = fetch(spec)
|
23
|
+
end
|
24
|
+
|
25
|
+
def task_list
|
26
|
+
@task_list ||= project_tasks
|
27
|
+
end
|
28
|
+
|
29
|
+
def full_tasks
|
30
|
+
@full_tasks ||= fetch_project_tasks
|
31
|
+
end
|
32
|
+
|
33
|
+
def method_missing(name, *args, &block)
|
34
|
+
attributes[name.to_s] || super
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def list
|
40
|
+
self.class.list
|
41
|
+
end
|
42
|
+
|
43
|
+
def id
|
44
|
+
self.id
|
45
|
+
end
|
46
|
+
|
47
|
+
def fetch(spec)
|
48
|
+
if is_integer?(spec)
|
49
|
+
fetch_by_id(spec)
|
50
|
+
else
|
51
|
+
fetch_by_name(spec)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def fetch_by_id(id)
|
56
|
+
asana_service.fetch "/projects/#{id}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def fetch_by_name(name)
|
60
|
+
list.each do |project|
|
61
|
+
return fetch_by_id(project['id']) if project['name'] =~ %r{#{name}}
|
62
|
+
end
|
63
|
+
raise NotFound, "No project matches name '#{name}'"
|
64
|
+
rescue RegexpError
|
65
|
+
raise BadSearch, "'#{name}' is not a valid regular expression"
|
66
|
+
end
|
67
|
+
|
68
|
+
def fetch_project_tasks
|
69
|
+
task_list.map { |t| Zensana::Asana::Task.new(t['id']) }
|
70
|
+
end
|
71
|
+
|
72
|
+
def project_tasks
|
73
|
+
asana_service.fetch "/projects/#{id}/tasks"
|
74
|
+
rescue NotFound
|
75
|
+
[]
|
76
|
+
end
|
77
|
+
|
78
|
+
def is_integer?(id)
|
79
|
+
Integer(id) rescue false
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Zensana
|
2
|
+
class Asana
|
3
|
+
class Task
|
4
|
+
include Zensana::Asana::Access
|
5
|
+
|
6
|
+
FIELDS = [
|
7
|
+
'id', 'name', 'notes', 'created_at', 'created_by.name',
|
8
|
+
'completed', 'assignee.name', 'tags.name', 'followers.name'
|
9
|
+
]
|
10
|
+
|
11
|
+
attr_reader :attributes
|
12
|
+
|
13
|
+
def initialize(id)
|
14
|
+
@attributes = fetch(id)
|
15
|
+
end
|
16
|
+
|
17
|
+
def tags
|
18
|
+
attributes['tags'].map { |t| snake_case t['name'] } if attributes['tags']
|
19
|
+
end
|
20
|
+
|
21
|
+
def is_section?
|
22
|
+
self.name.end_with?(':') rescue false
|
23
|
+
end
|
24
|
+
|
25
|
+
def section_name
|
26
|
+
self.name.chop if is_section?
|
27
|
+
end
|
28
|
+
|
29
|
+
def subtasks
|
30
|
+
@subtasks ||= fetch_subtasks(self.id)
|
31
|
+
end
|
32
|
+
|
33
|
+
def stories
|
34
|
+
@stories ||= story_list(self.id)
|
35
|
+
end
|
36
|
+
|
37
|
+
def attachments
|
38
|
+
@attachments ||= fetch_attachments(self.id)
|
39
|
+
end
|
40
|
+
|
41
|
+
def method_missing(name, *args, &block)
|
42
|
+
attributes[name.to_s] || super
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def fetch(id)
|
48
|
+
asana_service.fetch("/tasks/#{id}?opt_fields=#{opt_fields}")
|
49
|
+
end
|
50
|
+
|
51
|
+
def fetch_subtasks(id)
|
52
|
+
subtask_list(id).map { |s| Zensana::Asana::Task.new(s['id']) }
|
53
|
+
end
|
54
|
+
|
55
|
+
def subtask_list(id)
|
56
|
+
asana_service.fetch "/tasks/#{id}/subtasks"
|
57
|
+
rescue NotFound
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
|
61
|
+
def fetch_attachments(id)
|
62
|
+
attachment_list(id).map { |s| Zensana::Asana::Attachment.new(s['id']) }
|
63
|
+
end
|
64
|
+
|
65
|
+
def attachment_list(id)
|
66
|
+
asana_service.fetch "/tasks/#{id}/attachments"
|
67
|
+
rescue NotFound
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
|
71
|
+
def story_list(id)
|
72
|
+
asana_service.fetch "/tasks/#{id}/stories"
|
73
|
+
rescue NotFound
|
74
|
+
nil
|
75
|
+
end
|
76
|
+
|
77
|
+
def opt_fields
|
78
|
+
FIELDS.map { |f| f.to_s }.join(',')
|
79
|
+
end
|
80
|
+
|
81
|
+
def snake_case(string)
|
82
|
+
string.gsub(/(.)([A-Z])/,'\1_\2').downcase
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Zensana
|
2
|
+
class Asana
|
3
|
+
class User
|
4
|
+
include Zensana::Asana::Access
|
5
|
+
|
6
|
+
def self.list
|
7
|
+
@@list ||= Zensana::Asana.inst.fetch '/users'
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :attributes
|
11
|
+
|
12
|
+
def initialize(id)
|
13
|
+
@attributes = fetch(id)
|
14
|
+
end
|
15
|
+
|
16
|
+
def method_missing(name, *args, &block)
|
17
|
+
attributes[name.to_s] || super
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def fetch(id)
|
23
|
+
if cache[id] then
|
24
|
+
cache[id]
|
25
|
+
else
|
26
|
+
cache[id] = asana_service.fetch("/users/#{id}")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def cache
|
31
|
+
@@cache ||= {}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Zensana
|
2
|
+
class Zendesk
|
3
|
+
class Attachment
|
4
|
+
include Zensana::Zendesk::Access
|
5
|
+
|
6
|
+
attr_reader :attributes
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@attributes = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def upload(filename)
|
13
|
+
@attributes = upload_file(filename)
|
14
|
+
end
|
15
|
+
|
16
|
+
def method_missing(name, *args, &block)
|
17
|
+
attributes[name.to_s] || super
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def upload_file(file)
|
23
|
+
raise NotFound, "#{file} does not exist" unless File.exist?(file)
|
24
|
+
zendesk_service.create(
|
25
|
+
"/uploads.json",
|
26
|
+
:headers => {
|
27
|
+
"Content-Type" => "application/binary"
|
28
|
+
},
|
29
|
+
:detect_mime_type => true,
|
30
|
+
:body => {
|
31
|
+
"filename" => "#{File.basename(file)}",
|
32
|
+
"uploaded_data" => File.new(file)
|
33
|
+
},
|
34
|
+
)['upload']
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Zensana
|
2
|
+
class Zendesk
|
3
|
+
class Comment
|
4
|
+
include Zensana::Validate::Key
|
5
|
+
|
6
|
+
REQUIRED_KEYS = [ :author_id, :value ]
|
7
|
+
OPTIONAL_KEYS = [ :created_at, :public, :uploads ]
|
8
|
+
|
9
|
+
# Class validates the comment attributes
|
10
|
+
# added during the Ticket Import call.
|
11
|
+
# Comments cannot be created this way
|
12
|
+
# for existing tickets.
|
13
|
+
|
14
|
+
attr_reader :attributes
|
15
|
+
|
16
|
+
def initialize(attributes)
|
17
|
+
validate_keys attributes
|
18
|
+
#id = attributes['author_id']
|
19
|
+
#raise NotFound, "Author #{id} does not exist" unless author_exists?(id)
|
20
|
+
@attributes = attributes
|
21
|
+
end
|
22
|
+
|
23
|
+
def author_exists?(id)
|
24
|
+
!! Zendesk::User.new.find(id)
|
25
|
+
rescue NotFound
|
26
|
+
false
|
27
|
+
end
|
28
|
+
|
29
|
+
def method_missing(name, *args, &block)
|
30
|
+
attributes[name.to_s] || super
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Zensana
|
4
|
+
class Zendesk
|
5
|
+
class Ticket
|
6
|
+
include Zensana::Zendesk::Access
|
7
|
+
include Zensana::Validate::Key
|
8
|
+
|
9
|
+
# if external_id is present for a ticket in
|
10
|
+
# ZenDesk then we can say that it was already created
|
11
|
+
def self.external_id_exists?(external_id)
|
12
|
+
query = "/search.json?query=type:ticket,,external_id:#{external_id}"
|
13
|
+
external_id && (result = Zensana::Zendesk.inst.fetch(query)['results']) &&
|
14
|
+
! (result.nil? || result.empty?)
|
15
|
+
end
|
16
|
+
|
17
|
+
REQUIRED_KEYS = [:requester_id ]
|
18
|
+
OPTIONAL_KEYS = [
|
19
|
+
:external_id, :type, :subject, :description, :priority, :status,
|
20
|
+
:submitter_id, :assignee_id, :group_id, :collaborator_ids, :tags,
|
21
|
+
:created_at, :updated_id, :comments, :solved_at, :updated_at
|
22
|
+
]
|
23
|
+
|
24
|
+
attr_reader :attributes
|
25
|
+
|
26
|
+
def initialize(attributes)
|
27
|
+
@attributes = attributes || {}
|
28
|
+
end
|
29
|
+
|
30
|
+
def find(id)
|
31
|
+
@attributes = fetch(id)
|
32
|
+
end
|
33
|
+
|
34
|
+
def import(attributes=@attributes)
|
35
|
+
validate_keys attributes
|
36
|
+
raise AlreadyExists, "This ticket has already been imported with id #{self.id}" if imported?
|
37
|
+
import_ticket(attributes)
|
38
|
+
end
|
39
|
+
|
40
|
+
def imported?
|
41
|
+
!! id
|
42
|
+
end
|
43
|
+
|
44
|
+
def id
|
45
|
+
attributes['id']
|
46
|
+
end
|
47
|
+
|
48
|
+
def external_id
|
49
|
+
attributes['external_id']
|
50
|
+
end
|
51
|
+
|
52
|
+
def method_missing(name, *args, &block)
|
53
|
+
attributes[name.to_s] || super
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def import_ticket(attributes)
|
59
|
+
unless attributes['ticket']
|
60
|
+
attributes = { 'ticket' => attributes }
|
61
|
+
end
|
62
|
+
zendesk_service.create(
|
63
|
+
"/imports/tickets.json",
|
64
|
+
:body => JSON.generate(attributes)
|
65
|
+
)['ticket']
|
66
|
+
end
|
67
|
+
|
68
|
+
def fetch(id)
|
69
|
+
zendesk_service.fetch("/tickets/#{id}.json")['ticket']
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Zensana
|
4
|
+
class Zendesk
|
5
|
+
class User
|
6
|
+
include Zensana::Zendesk::Access
|
7
|
+
include Zensana::Validate::Key
|
8
|
+
|
9
|
+
REQUIRED_KEYS = [ :name, :email ]
|
10
|
+
OPTIONAL_KEYS = [ :time_zone, :locale_id, :organization_id, :role, :verified, :phone, :photo ]
|
11
|
+
|
12
|
+
attr_reader :attributes
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@attributes = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def find(spec)
|
19
|
+
@attributes = lookup(spec)
|
20
|
+
end
|
21
|
+
|
22
|
+
def create(attributes)
|
23
|
+
validate_keys attributes
|
24
|
+
email = attributes['email'] || attributes['user']['email']
|
25
|
+
raise AlreadyExists, "User '#{email}' already exists" if lookup_by_email(email)
|
26
|
+
rescue NotFound
|
27
|
+
user = create_user(attributes)
|
28
|
+
update_cache user
|
29
|
+
@attributes = user
|
30
|
+
end
|
31
|
+
|
32
|
+
def method_missing(name, *args, &block)
|
33
|
+
attributes[name.to_s] || super
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def lookup(spec)
|
39
|
+
if spec.is_a?(Fixnum)
|
40
|
+
lookup_by_id spec
|
41
|
+
else
|
42
|
+
lookup_by_email spec
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def lookup_by_id(id)
|
47
|
+
unless (user = read_cache(id))
|
48
|
+
user = fetch(id)
|
49
|
+
update_cache user
|
50
|
+
end
|
51
|
+
user
|
52
|
+
end
|
53
|
+
|
54
|
+
def lookup_by_email(email)
|
55
|
+
unless (user = read_cache(email))
|
56
|
+
user = search("email:#{email}")
|
57
|
+
update_cache user
|
58
|
+
end
|
59
|
+
user
|
60
|
+
end
|
61
|
+
|
62
|
+
def create_user(attributes)
|
63
|
+
unless attributes['user'].is_a?(Hash)
|
64
|
+
attributes = { 'user' => attributes }
|
65
|
+
end
|
66
|
+
zendesk_service.create(
|
67
|
+
"/users.json",
|
68
|
+
:body => JSON.generate(attributes)
|
69
|
+
)['user']
|
70
|
+
end
|
71
|
+
|
72
|
+
def cache
|
73
|
+
@@cache ||= {}
|
74
|
+
end
|
75
|
+
|
76
|
+
def read_cache(key)
|
77
|
+
cache[key.to_s]
|
78
|
+
end
|
79
|
+
|
80
|
+
def update_cache(user)
|
81
|
+
[ :id, :email ].each do |attr|
|
82
|
+
key = user[attr].to_s
|
83
|
+
cache[key] = user
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def fetch(id)
|
88
|
+
zendesk_service.fetch("/users/#{id}.json")['user']
|
89
|
+
end
|
90
|
+
|
91
|
+
def search(query)
|
92
|
+
zendesk_service.fetch("/users/search.json?query=#{query}")['users'].first
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'httmultiparty'
|
2
|
+
|
3
|
+
module Zensana
|
4
|
+
class Asana
|
5
|
+
include HTTMultiParty
|
6
|
+
base_uri 'https://app.asana.com/api/1.0'
|
7
|
+
headers 'Content-Type' => 'application/json; charset=utf-8'
|
8
|
+
default_timeout 10
|
9
|
+
#debug_output
|
10
|
+
|
11
|
+
def self.inst
|
12
|
+
@inst ||= new
|
13
|
+
end
|
14
|
+
|
15
|
+
module Access
|
16
|
+
def asana_service
|
17
|
+
@asana_service ||= Zensana::Asana.inst
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
user, pword = if ENV['ASANA_USERNAME']
|
23
|
+
[ ENV['ASANA_USERNAME'], ENV['ASANA_PASSWORD'] ]
|
24
|
+
else
|
25
|
+
[ ENV['ASANA_API_KEY'], nil ]
|
26
|
+
end
|
27
|
+
self.class.basic_auth user, pword
|
28
|
+
end
|
29
|
+
|
30
|
+
def fetch(path, params={}, &block)
|
31
|
+
request :get, path, params, &block
|
32
|
+
end
|
33
|
+
|
34
|
+
def request(method, path, params={}, &block)
|
35
|
+
result = self.class.send(method, path, params)
|
36
|
+
|
37
|
+
Zensana::Error.handle_http_errors result
|
38
|
+
|
39
|
+
Zensana::Response.new(result).tap do |response|
|
40
|
+
block.call(response) if block_given?
|
41
|
+
end
|
42
|
+
|
43
|
+
rescue Net::OpenTimeout
|
44
|
+
raise Unprocessable, "Connection timed out"
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Zensana
|
4
|
+
class Error < StandardError
|
5
|
+
def self.msg=(msg)
|
6
|
+
@msg = msg
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.msg
|
10
|
+
@msg
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(msg=nil)
|
14
|
+
super msg || self.class.msg
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.handle_http_errors(http_response)
|
18
|
+
body = JSON.parse(http_response.body)
|
19
|
+
message = if body['errors']
|
20
|
+
body['errors'].first['message']
|
21
|
+
else
|
22
|
+
body
|
23
|
+
end
|
24
|
+
rescue
|
25
|
+
nil
|
26
|
+
ensure
|
27
|
+
case http_response.code
|
28
|
+
when 200, 201 then return
|
29
|
+
when 404 then raise NotFound, message
|
30
|
+
when 401..403 then raise AccessDenied, message
|
31
|
+
else raise Unprocessable, message
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class AccessDenied < Error
|
37
|
+
self.msg = 'Access denied - check credentials'
|
38
|
+
end
|
39
|
+
|
40
|
+
class AlreadyExists < Error
|
41
|
+
self.msg = 'That item already exists'
|
42
|
+
end
|
43
|
+
|
44
|
+
class BadSearch < Error
|
45
|
+
self.msg = 'That is an invalid regular expression'
|
46
|
+
end
|
47
|
+
|
48
|
+
class NotFound < Error
|
49
|
+
self.msg = 'That item does not exist'
|
50
|
+
end
|
51
|
+
|
52
|
+
class Unprocessable < Error
|
53
|
+
self.msg = 'Something went wrong in the external service'
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'awesome_print'
|
2
|
+
|
3
|
+
module Zensana
|
4
|
+
class Response
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def each(&block)
|
8
|
+
@data.each(&block)
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(http_response)
|
12
|
+
@ok = http_response.success?
|
13
|
+
body = JSON.parse(http_response.body) rescue {}
|
14
|
+
@data = body['data'] || body
|
15
|
+
end
|
16
|
+
|
17
|
+
def ok?
|
18
|
+
@ok
|
19
|
+
end
|
20
|
+
|
21
|
+
def has_more_pages?
|
22
|
+
!! next_page
|
23
|
+
end
|
24
|
+
|
25
|
+
def next_page
|
26
|
+
@data['next_page']
|
27
|
+
end
|
28
|
+
|
29
|
+
def [](key)
|
30
|
+
@data.is_a?(Hash) ? @data[key.to_s] : key.is_a?(Integer) ? @data[key] : nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_h
|
34
|
+
@data.respond_to?('to_h') ? @data.to_h : @data
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_a
|
38
|
+
@data.respond_to?('to_a') ? @data.to_a : @data
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_s
|
42
|
+
@data.respond_to?('to_s') ? @data.to_s : @data
|
43
|
+
end
|
44
|
+
|
45
|
+
def pretty
|
46
|
+
ap @data
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'httmultiparty'
|
2
|
+
|
3
|
+
module Zensana
|
4
|
+
class Zendesk
|
5
|
+
include HTTMultiParty
|
6
|
+
default_timeout 10
|
7
|
+
#debug_output
|
8
|
+
|
9
|
+
def self.inst
|
10
|
+
@inst ||= new
|
11
|
+
end
|
12
|
+
|
13
|
+
module Access
|
14
|
+
def zendesk_service
|
15
|
+
@zendesk_service ||= Zensana::Zendesk.inst
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
self.class.base_uri "https://#{ENV['ZENDESK_DOMAIN']}.zendesk.com/api/v2"
|
21
|
+
self.class.basic_auth ENV['ZENDESK_USERNAME'], ENV['ZENDESK_PASSWORD']
|
22
|
+
end
|
23
|
+
|
24
|
+
def fetch(path, params={}, &block)
|
25
|
+
request :get, path, params, &block
|
26
|
+
end
|
27
|
+
|
28
|
+
def create(path, params={}, &block)
|
29
|
+
request :post, path, params, &block
|
30
|
+
end
|
31
|
+
|
32
|
+
def request(method, path, params={}, &block)
|
33
|
+
unless params[:headers]
|
34
|
+
params[:headers] = {
|
35
|
+
"Content-Type" => "application/json"
|
36
|
+
}
|
37
|
+
end
|
38
|
+
path = relative_path(path)
|
39
|
+
result = self.class.send(method, path, params)
|
40
|
+
|
41
|
+
Zensana::Error.handle_http_errors result
|
42
|
+
|
43
|
+
Zensana::Response.new(result).tap do |response|
|
44
|
+
block.call(response) if block_given?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def relative_path(path)
|
51
|
+
path.sub(self.class.base_uri, '')
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Zensana
|
2
|
+
class MissingKey < Zensana::Error; end
|
3
|
+
class UnknownKey < Zensana::Error; end
|
4
|
+
class UndefinedKeys < Zensana::Error; end
|
5
|
+
|
6
|
+
module Validate
|
7
|
+
module Key
|
8
|
+
def validate_keys(hash)
|
9
|
+
raise MissingKey, "Mandatory keys are: #{required_keys}" unless has_required_keys?(hash)
|
10
|
+
raise UnknownKey, "Valid keys are: #{valid_keys}" if has_unknown_keys?(hash)
|
11
|
+
end
|
12
|
+
|
13
|
+
def has_required_keys?(hash)
|
14
|
+
required_keys.all? { |k| hash.key? k }
|
15
|
+
end
|
16
|
+
|
17
|
+
def has_unknown_keys?(hash)
|
18
|
+
! (hash.keys - valid_keys).empty?
|
19
|
+
end
|
20
|
+
|
21
|
+
def valid_keys
|
22
|
+
@valid_keys ||= required_keys + optional_keys
|
23
|
+
end
|
24
|
+
|
25
|
+
def required_keys
|
26
|
+
@required_keys ||= begin
|
27
|
+
const = "#{self.class}::REQUIRED_KEYS"
|
28
|
+
Object.const_get const
|
29
|
+
rescue NameError
|
30
|
+
raise UndefinedKeys, "You must define #{const}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def optional_keys
|
35
|
+
@optional_keys ||= begin
|
36
|
+
const = "#{self.class}::OPTIONAL_KEYS"
|
37
|
+
Object.const_get const
|
38
|
+
rescue NameError
|
39
|
+
raise UndefinedKeys, "You must define #{const}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|