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