mavenlink-ruby 0.0.2
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/.env +1 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +30 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +74 -0
- data/LICENSE +21 -0
- data/README.md +7 -0
- data/Rakefile +12 -0
- data/fixtures/vcr_cassettes/Mavenlink_CustomFieldValue/_list/a_subject_type_is_provided/should_be_listable.yml +55 -0
- data/fixtures/vcr_cassettes/Mavenlink_CustomFieldValue/_retrieve/should_be_retrievable.yml +55 -0
- data/fixtures/vcr_cassettes/Mavenlink_Invoice/_list/should_be_listable.yml +89 -0
- data/fixtures/vcr_cassettes/Mavenlink_Invoice/_retrieve/should_be_retrievable.yml +55 -0
- data/fixtures/vcr_cassettes/Mavenlink_Workspace/_list/should_be_listable.yml +108 -0
- data/fixtures/vcr_cassettes/Mavenlink_Workspace/_list_invoices/should_list_the_Invoices_for_the_provided_Workspace_id.yml +57 -0
- data/fixtures/vcr_cassettes/Mavenlink_Workspace/_retrieve/should_be_retrievable.yml +57 -0
- data/fixtures/vcr_cassettes/Mavenlink_WorkspaceGroup/_list/should_be_listable.yml +74 -0
- data/fixtures/vcr_cassettes/Mavenlink_WorkspaceGroup/_list_workspaces/should_list_the_Workspaces_for_the_provided_WorkspaceGroup_id.yml +57 -0
- data/fixtures/vcr_cassettes/Mavenlink_WorkspaceGroup/_retrieve/should_be_retrievable.yml +56 -0
- data/lib/mavenlink.rb +31 -0
- data/lib/mavenlink/api_operations/request.rb +41 -0
- data/lib/mavenlink/api_resource.rb +70 -0
- data/lib/mavenlink/list.rb +59 -0
- data/lib/mavenlink/resources.rb +6 -0
- data/lib/mavenlink/resources/custom_field_value.rb +24 -0
- data/lib/mavenlink/resources/invoice.rb +16 -0
- data/lib/mavenlink/resources/workspace.rb +12 -0
- data/lib/mavenlink/resources/workspace_group.rb +25 -0
- data/lib/mavenlink/util.rb +23 -0
- data/mavenlink-ruby.gemspec +20 -0
- data/spec/.rubocop.yml +20 -0
- data/spec/lib/mavenlink/list_spec.rb +185 -0
- data/spec/lib/mavenlink/resources/custom_field_value_spec.rb +69 -0
- data/spec/lib/mavenlink/resources/invoice_spec.rb +38 -0
- data/spec/lib/mavenlink/resources/workspace_group_spec.rb +147 -0
- data/spec/lib/mavenlink/resources/workspace_spec.rb +35 -0
- data/spec/spec_helper.rb +24 -0
- metadata +78 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mavenlink
|
4
|
+
module APIOperations
|
5
|
+
module Request
|
6
|
+
module ClassMethods
|
7
|
+
def get(path, params = {})
|
8
|
+
api_base = Mavenlink.api_base
|
9
|
+
api_key = Mavenlink.api_key
|
10
|
+
options = {
|
11
|
+
query: params,
|
12
|
+
headers: {
|
13
|
+
Authorization: "Bearer #{api_key}",
|
14
|
+
},
|
15
|
+
}
|
16
|
+
HTTParty.get("#{api_base}#{path}", options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def post(path, params = {})
|
20
|
+
api_base = Mavenlink.api_base
|
21
|
+
api_key = Mavenlink.api_key
|
22
|
+
options = {
|
23
|
+
body: params,
|
24
|
+
headers: {
|
25
|
+
Authorization: "Bearer #{api_key}",
|
26
|
+
},
|
27
|
+
}
|
28
|
+
HTTParty.post("#{api_base}#{path}", options)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.included(base)
|
33
|
+
base.extend(ClassMethods)
|
34
|
+
end
|
35
|
+
|
36
|
+
protected def get(url, params = {})
|
37
|
+
self.class.get(url, params)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mavenlink
|
4
|
+
class APIResource
|
5
|
+
include Mavenlink::APIOperations::Request
|
6
|
+
|
7
|
+
def self.resource_url
|
8
|
+
if self == APIResource
|
9
|
+
raise NotImplementedError,
|
10
|
+
"APIResource is an abstract class, you should before actions "\
|
11
|
+
"on its subclasses (Workspace, Invoice, etc.)"
|
12
|
+
end
|
13
|
+
|
14
|
+
"/#{plural_name}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.plural_name
|
18
|
+
"#{self::OBJECT_NAME.downcase}s"
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.retrieve(id)
|
22
|
+
instance = new(id: id)
|
23
|
+
instance.refresh
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.list(params = {}, options = {})
|
27
|
+
response = get(resource_url, params)
|
28
|
+
List.new(self, response, options)
|
29
|
+
end
|
30
|
+
|
31
|
+
def refresh
|
32
|
+
response = get(resource_url)
|
33
|
+
data = Util.results(response).first
|
34
|
+
self.class.new(data)
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(data)
|
38
|
+
@values = data
|
39
|
+
|
40
|
+
update_attributes(@values)
|
41
|
+
end
|
42
|
+
|
43
|
+
def resource_url
|
44
|
+
unless (id = self.id)
|
45
|
+
raise InvalidRequestError.new(
|
46
|
+
"Could not determine which URL to request: #{self.class} instance " \
|
47
|
+
"has invalid ID: #{id.inspect}",
|
48
|
+
"id"
|
49
|
+
)
|
50
|
+
end
|
51
|
+
"#{self.class.resource_url}/#{CGI.escape(id)}"
|
52
|
+
end
|
53
|
+
|
54
|
+
protected def metaclass
|
55
|
+
class << self; self; end
|
56
|
+
end
|
57
|
+
|
58
|
+
protected def update_attributes(values)
|
59
|
+
values.each do |k, v|
|
60
|
+
add_accessor(k, v) unless metaclass.method_defined? k.to_sym
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
protected def add_accessor(key, value)
|
65
|
+
metaclass.instance_eval do
|
66
|
+
define_method(key) { value }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mavenlink
|
4
|
+
class List
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
attr_accessor :data
|
8
|
+
|
9
|
+
attr_reader :page_number
|
10
|
+
attr_reader :page_count
|
11
|
+
|
12
|
+
def initialize(klass, response, options = {})
|
13
|
+
@meta = response["meta"]
|
14
|
+
@klass = klass
|
15
|
+
@page_number = @meta["page_number"]
|
16
|
+
@page_count = @meta["page_count"]
|
17
|
+
@options = options
|
18
|
+
results = setup_results(response)
|
19
|
+
@data = results.map { |thing| klass.new(thing) }
|
20
|
+
end
|
21
|
+
|
22
|
+
def each(&blk)
|
23
|
+
@data.each(&blk)
|
24
|
+
end
|
25
|
+
|
26
|
+
def auto_paging_each(&blk)
|
27
|
+
return enum_for(:auto_paging_each) unless block_given?
|
28
|
+
|
29
|
+
page = self
|
30
|
+
loop do
|
31
|
+
page.each(&blk)
|
32
|
+
|
33
|
+
break if page.last_page?
|
34
|
+
|
35
|
+
page = page.next_page
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def last_page?
|
40
|
+
@page_number == @page_count
|
41
|
+
end
|
42
|
+
|
43
|
+
def next_page
|
44
|
+
@klass.list({ page: @page_number + 1 }, @options)
|
45
|
+
end
|
46
|
+
|
47
|
+
private def setup_results(response)
|
48
|
+
results = Util.results(response)
|
49
|
+
|
50
|
+
if (filters = Util.stringify_keys(@options[:filters]))
|
51
|
+
results.select! do |res|
|
52
|
+
filters.map { |key, value| res[key] == value }.all?
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
results
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mavenlink
|
4
|
+
class CustomFieldValue < APIResource
|
5
|
+
OBJECT_NAME = "custom_field_value"
|
6
|
+
|
7
|
+
def self.list(subject_type = nil)
|
8
|
+
if subject_type.nil?
|
9
|
+
raise ArgumentError,
|
10
|
+
"CustomFieldValues can only be listed with a subject, " \
|
11
|
+
"e.g. CustomFieldValue.list('user')"
|
12
|
+
end
|
13
|
+
|
14
|
+
super(subject_type: subject_type)
|
15
|
+
end
|
16
|
+
|
17
|
+
def subject
|
18
|
+
case subject_type
|
19
|
+
when "workspace_group"
|
20
|
+
WorkspaceGroup.retrieve(subject_id)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mavenlink
|
4
|
+
class Invoice < APIResource
|
5
|
+
include Mavenlink::APIOperations::Request
|
6
|
+
|
7
|
+
|
8
|
+
OBJECT_NAME = "invoice"
|
9
|
+
|
10
|
+
def self.create(params = {})
|
11
|
+
response = post(resource_url, params)
|
12
|
+
data = Util.results(response).first
|
13
|
+
self.class.new(data)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mavenlink
|
4
|
+
class WorkspaceGroup < APIResource
|
5
|
+
OBJECT_NAME = "workspace_group"
|
6
|
+
|
7
|
+
def self.list_workspaces(id)
|
8
|
+
response = get("/workspaces?workspace_groups=#{id}")
|
9
|
+
List.new(Workspace, response)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.list_custom_field_values(id)
|
13
|
+
response = get("/custom_field_values", subject_type: OBJECT_NAME)
|
14
|
+
List.new(CustomFieldValue, response, filters: { subject_id: id.to_i })
|
15
|
+
end
|
16
|
+
|
17
|
+
def workspaces
|
18
|
+
@workspaces ||= self.class.list_workspaces(id)
|
19
|
+
end
|
20
|
+
|
21
|
+
def custom_field_values
|
22
|
+
@custom_field_values ||= self.class.list_custom_field_values(id)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Util
|
4
|
+
def self.results(response)
|
5
|
+
pp response
|
6
|
+
response["results"].map do |result|
|
7
|
+
key = result["key"]
|
8
|
+
id = result["id"]
|
9
|
+
response[key][id]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.stringify_keys(hash)
|
14
|
+
new_hash = {}
|
15
|
+
return new_hash unless hash.is_a? Hash
|
16
|
+
|
17
|
+
hash.each do |k, v|
|
18
|
+
new_hash[k.to_s] = v
|
19
|
+
end
|
20
|
+
|
21
|
+
new_hash
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(::File.join(::File.dirname(__FILE__), "lib"))
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "mavenlink-ruby"
|
7
|
+
s.version = "0.0.2"
|
8
|
+
s.date = "2020-03-15"
|
9
|
+
s.summary = "Ruby Mavenlink API Wrapper"
|
10
|
+
s.description = "Inspired by Stripe's stripe-ruby"
|
11
|
+
s.authors = ["Adam Dawkins"]
|
12
|
+
s.email = "adam@dragondrop.uk"
|
13
|
+
s.homepage = "https://rubygems.org/gems/mavenlink-ruby"
|
14
|
+
s.license = "MIT"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- test/*`.split("\n")
|
18
|
+
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
end
|
data/spec/.rubocop.yml
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
inherit_from:
|
2
|
+
- ../.rubocop.yml
|
3
|
+
|
4
|
+
Layout/LineLength:
|
5
|
+
Max: 100
|
6
|
+
|
7
|
+
Metrics/BlockLength:
|
8
|
+
# `context` in specs are blocks and get quite large, so exclude the spec
|
9
|
+
# directory from having to adhere to this rule.
|
10
|
+
Enabled: false
|
11
|
+
|
12
|
+
Metrics/ClassLength:
|
13
|
+
# spec classes get quite large, so exclude the spec directory from having
|
14
|
+
# to adhere to this rule.
|
15
|
+
Enabled: false
|
16
|
+
|
17
|
+
Style/NumericLiterals:
|
18
|
+
# We have a lot of numeric ids in our specs and it looks weird splitting
|
19
|
+
# them into thousand separators
|
20
|
+
Enabled: false
|
@@ -0,0 +1,185 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe Mavenlink::List do
|
6
|
+
let(:response) do
|
7
|
+
{
|
8
|
+
"count" => 3,
|
9
|
+
"results" => [
|
10
|
+
{ "key" => "invoices", "id" => "1" },
|
11
|
+
{ "key" => "invoices", "id" => "2" },
|
12
|
+
{ "key" => "invoices", "id" => "3" },
|
13
|
+
],
|
14
|
+
"invoices" => {
|
15
|
+
"1" => { "id" => 1 },
|
16
|
+
"2" => { "id" => 2 },
|
17
|
+
"3" => { "id" => 3 },
|
18
|
+
},
|
19
|
+
"meta" => {
|
20
|
+
"count" => 3,
|
21
|
+
"page_count" => 1,
|
22
|
+
"page_number" => 1,
|
23
|
+
},
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
class MockResource
|
28
|
+
attr_reader :id
|
29
|
+
def initialize(args)
|
30
|
+
@id = args[:id]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should provide #count via Enumerable" do
|
35
|
+
list = Mavenlink::List.new(MockResource, response)
|
36
|
+
|
37
|
+
expect(list.count).to eq 3
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should provide #each" do
|
41
|
+
list = Mavenlink::List.new(MockResource, response)
|
42
|
+
expect(list.each.to_a.count).to eq 3
|
43
|
+
expect(list.each.to_a.first).to be_a MockResource
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "filtering" do
|
47
|
+
let(:response) do
|
48
|
+
{
|
49
|
+
"count" => 4,
|
50
|
+
"results" => [
|
51
|
+
{ "key" => "things", "id" => "1" },
|
52
|
+
{ "key" => "things", "id" => "2" },
|
53
|
+
{ "key" => "things", "id" => "3" },
|
54
|
+
{ "key" => "things", "id" => "4" },
|
55
|
+
],
|
56
|
+
"things" => {
|
57
|
+
"1" => { "id" => 1, "subject_id" => "A" },
|
58
|
+
"2" => { "id" => 2, "subject_id" => "A" },
|
59
|
+
"3" => { "id" => 3, "subject_id" => "B" },
|
60
|
+
"4" => { "id" => 4, "subject_id" => "C" },
|
61
|
+
},
|
62
|
+
"meta" => {
|
63
|
+
"count" => 4,
|
64
|
+
"page_count" => 1,
|
65
|
+
"page_number" => 1,
|
66
|
+
},
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
it "applies a given filter to the results of the list" do
|
71
|
+
list = Mavenlink::List.new(Mavenlink::Thing, response, filters: { "subject_id" => "A" })
|
72
|
+
expect(list.each.to_a.count).to eq 2
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "#auto_paging_each" do
|
77
|
+
# We do a lot of setup here to mimic three pages worth of results.
|
78
|
+
# A `foo` flag is added to each result to allow us to test filtering
|
79
|
+
let(:initial_response) do
|
80
|
+
{
|
81
|
+
"count" => 5,
|
82
|
+
"results" => [
|
83
|
+
{ "key" => "things", "id" => "1" },
|
84
|
+
{ "key" => "things", "id" => "2" },
|
85
|
+
],
|
86
|
+
"things" => {
|
87
|
+
"1" => { "id" => 1, "foo" => true },
|
88
|
+
"2" => { "id" => 2, "foo" => false },
|
89
|
+
},
|
90
|
+
"meta" => {
|
91
|
+
"count" => 5,
|
92
|
+
"page_count" => 3,
|
93
|
+
"page_number" => 1,
|
94
|
+
},
|
95
|
+
}
|
96
|
+
end
|
97
|
+
|
98
|
+
before do
|
99
|
+
stub_request(:get, "#{Mavenlink.api_base}/things")
|
100
|
+
.with(query: { page: 2 })
|
101
|
+
.to_return(
|
102
|
+
headers: {
|
103
|
+
"Content-Type": "application/json",
|
104
|
+
},
|
105
|
+
body: JSON.generate(
|
106
|
+
count: 5,
|
107
|
+
results: [
|
108
|
+
{ key: "things", id: "3" },
|
109
|
+
{ key: "things", id: "4" },
|
110
|
+
],
|
111
|
+
things: {
|
112
|
+
"3": { id: 3, foo: true },
|
113
|
+
"4": { id: 4, foo: false },
|
114
|
+
},
|
115
|
+
meta: {
|
116
|
+
count: 5,
|
117
|
+
page_count: 3,
|
118
|
+
page_number: 2,
|
119
|
+
page_size: 2,
|
120
|
+
}
|
121
|
+
)
|
122
|
+
)
|
123
|
+
stub_request(:get, "#{Mavenlink.api_base}/things")
|
124
|
+
.with(query: { page: 3 })
|
125
|
+
.to_return(
|
126
|
+
headers: {
|
127
|
+
"Content-Type": "application/json",
|
128
|
+
},
|
129
|
+
body: JSON.generate(
|
130
|
+
count: 5,
|
131
|
+
results: [
|
132
|
+
{ key: "things", id: "5" },
|
133
|
+
],
|
134
|
+
things: {
|
135
|
+
"5": { id: 5, foo: false },
|
136
|
+
},
|
137
|
+
meta: {
|
138
|
+
count: 5,
|
139
|
+
page_count: 3,
|
140
|
+
page_number: 3,
|
141
|
+
page_size: 1,
|
142
|
+
}
|
143
|
+
)
|
144
|
+
)
|
145
|
+
end
|
146
|
+
|
147
|
+
let(:list) { Mavenlink::List.new(Mavenlink::Thing, initial_response) }
|
148
|
+
let(:url) { "#{Mavenlink.api_base}/things" }
|
149
|
+
it "requests all the pages from the API" do
|
150
|
+
list.auto_paging_each(&:id)
|
151
|
+
expect(a_request(:get, url).with(query: { page: 2 })).to have_been_made
|
152
|
+
expect(a_request(:get, url).with(query: { page: 3 })).to have_been_made
|
153
|
+
expect(a_request(:get, url).with(query: { page: 4 })).to_not have_been_made
|
154
|
+
end
|
155
|
+
|
156
|
+
it "returns an Enumerable" do
|
157
|
+
expect(list.auto_paging_each).to be_an Enumerable
|
158
|
+
end
|
159
|
+
|
160
|
+
it "implements #count" do
|
161
|
+
expect(list.auto_paging_each.count).to eq 5
|
162
|
+
end
|
163
|
+
|
164
|
+
it "iterates all the results" do
|
165
|
+
class Foo
|
166
|
+
def self.bar; end
|
167
|
+
end
|
168
|
+
allow(Foo).to receive(:bar)
|
169
|
+
list.auto_paging_each { Foo.bar }
|
170
|
+
expect(Foo).to have_received(:bar).exactly(5).times
|
171
|
+
end
|
172
|
+
describe "filtering" do
|
173
|
+
let(:list) { Mavenlink::List.new(Mavenlink::Thing, initial_response, filters: { foo: true }) }
|
174
|
+
it "passes filters through each page" do
|
175
|
+
expect(list.auto_paging_each.map(&:foo).all?).to eq true
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
module Mavenlink
|
182
|
+
class Thing < Mavenlink::APIResource
|
183
|
+
OBJECT_NAME = "thing"
|
184
|
+
end
|
185
|
+
end
|