zensana 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,3 @@
1
+ module Zensana
2
+ VERSION = "1.0.0"
3
+ end