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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/.env +1 -0
  3. data/.gitignore +2 -0
  4. data/.rubocop.yml +30 -0
  5. data/Gemfile +18 -0
  6. data/Gemfile.lock +74 -0
  7. data/LICENSE +21 -0
  8. data/README.md +7 -0
  9. data/Rakefile +12 -0
  10. data/fixtures/vcr_cassettes/Mavenlink_CustomFieldValue/_list/a_subject_type_is_provided/should_be_listable.yml +55 -0
  11. data/fixtures/vcr_cassettes/Mavenlink_CustomFieldValue/_retrieve/should_be_retrievable.yml +55 -0
  12. data/fixtures/vcr_cassettes/Mavenlink_Invoice/_list/should_be_listable.yml +89 -0
  13. data/fixtures/vcr_cassettes/Mavenlink_Invoice/_retrieve/should_be_retrievable.yml +55 -0
  14. data/fixtures/vcr_cassettes/Mavenlink_Workspace/_list/should_be_listable.yml +108 -0
  15. data/fixtures/vcr_cassettes/Mavenlink_Workspace/_list_invoices/should_list_the_Invoices_for_the_provided_Workspace_id.yml +57 -0
  16. data/fixtures/vcr_cassettes/Mavenlink_Workspace/_retrieve/should_be_retrievable.yml +57 -0
  17. data/fixtures/vcr_cassettes/Mavenlink_WorkspaceGroup/_list/should_be_listable.yml +74 -0
  18. data/fixtures/vcr_cassettes/Mavenlink_WorkspaceGroup/_list_workspaces/should_list_the_Workspaces_for_the_provided_WorkspaceGroup_id.yml +57 -0
  19. data/fixtures/vcr_cassettes/Mavenlink_WorkspaceGroup/_retrieve/should_be_retrievable.yml +56 -0
  20. data/lib/mavenlink.rb +31 -0
  21. data/lib/mavenlink/api_operations/request.rb +41 -0
  22. data/lib/mavenlink/api_resource.rb +70 -0
  23. data/lib/mavenlink/list.rb +59 -0
  24. data/lib/mavenlink/resources.rb +6 -0
  25. data/lib/mavenlink/resources/custom_field_value.rb +24 -0
  26. data/lib/mavenlink/resources/invoice.rb +16 -0
  27. data/lib/mavenlink/resources/workspace.rb +12 -0
  28. data/lib/mavenlink/resources/workspace_group.rb +25 -0
  29. data/lib/mavenlink/util.rb +23 -0
  30. data/mavenlink-ruby.gemspec +20 -0
  31. data/spec/.rubocop.yml +20 -0
  32. data/spec/lib/mavenlink/list_spec.rb +185 -0
  33. data/spec/lib/mavenlink/resources/custom_field_value_spec.rb +69 -0
  34. data/spec/lib/mavenlink/resources/invoice_spec.rb +38 -0
  35. data/spec/lib/mavenlink/resources/workspace_group_spec.rb +147 -0
  36. data/spec/lib/mavenlink/resources/workspace_spec.rb +35 -0
  37. data/spec/spec_helper.rb +24 -0
  38. 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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mavenlink/resources/custom_field_value"
4
+ require "mavenlink/resources/invoice"
5
+ require "mavenlink/resources/workspace"
6
+ require "mavenlink/resources/workspace_group"
@@ -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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mavenlink
4
+ class Workspace < APIResource
5
+ OBJECT_NAME = "workspace"
6
+
7
+ def self.list_invoices(id)
8
+ response = get("/invoices?workspace_id=#{id}")
9
+ List.new(Invoice, response)
10
+ end
11
+ end
12
+ 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