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.
@@ -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