issue-db 1.1.0 → 1.2.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.
@@ -4,218 +4,220 @@ require_relative "cache"
4
4
  require_relative "models/record"
5
5
  require_relative "utils/generate"
6
6
 
7
- class RecordNotFound < StandardError; end
8
-
9
- class Database
10
- include Cache
11
- include Generate
12
-
13
- # :param: log [Logger] a logger object to use for logging
14
- # :param: client [Octokit::Client] an Octokit::Client object to use for interacting with the GitHub API
15
- # :param: repo [Repository] a Repository object that represents the GitHub repository to use as the datastore
16
- # :param: label [String] the label to use for issues managed in the datastore by this library
17
- # :param: cache_expiry [Integer] the number of seconds to cache issues in memory (default: 60)
18
- # :return: A new Database object
19
- def initialize(log, client, repo, label, cache_expiry)
20
- @log = log
21
- @client = client
22
- @repo = repo
23
- @label = label
24
- @cache_expiry = cache_expiry
25
- @issues = nil
26
- @issues_last_updated = nil
27
- end
7
+ module IssueDB
8
+ class RecordNotFound < StandardError; end
9
+
10
+ class Database
11
+ include Cache
12
+ include Generate
13
+
14
+ # :param: log [Logger] a logger object to use for logging
15
+ # :param: client [Octokit::Client] an Octokit::Client object to use for interacting with the GitHub API
16
+ # :param: repo [Repository] a Repository object that represents the GitHub repository to use as the datastore
17
+ # :param: label [String] the label to use for issues managed in the datastore by this library
18
+ # :param: cache_expiry [Integer] the number of seconds to cache issues in memory (default: 60)
19
+ # :return: A new Database object
20
+ def initialize(log, client, repo, label, cache_expiry)
21
+ @log = log
22
+ @client = client
23
+ @repo = repo
24
+ @label = label
25
+ @cache_expiry = cache_expiry
26
+ @issues = nil
27
+ @issues_last_updated = nil
28
+ end
28
29
 
29
- # Create a new issue/record in the database
30
- # This will return the newly created issue as a Record object (parsed)
31
- # :param: key [String] the key (issue title) to create
32
- # :param: data [Hash] the data to use for the issue body
33
- # :param: options [Hash] a hash of options containing extra data such as body_before and body_after
34
- # :return: The newly created issue as a Record object
35
- # usage example:
36
- # data = { color: "blue", cool: true, popularity: 100, tags: ["tag1", "tag2"] }
37
- # options = { body_before: "some text before the data", body_after: "some text after the data", include_closed: true }
38
- # db.create("event123", {cool: true, data: "here"}, options)
39
- def create(key, data, options = {})
40
- @log.debug("attempting to create: #{key}")
41
- issue = find_issue_by_key(key, options, create_mode: true)
42
- if issue
43
- @log.warn("skipping issue creation and returning existing issue - an issue already exists with the key: #{key}")
30
+ # Create a new issue/record in the database
31
+ # This will return the newly created issue as a Record object (parsed)
32
+ # :param: key [String] the key (issue title) to create
33
+ # :param: data [Hash] the data to use for the issue body
34
+ # :param: options [Hash] a hash of options containing extra data such as body_before and body_after
35
+ # :return: The newly created issue as a Record object
36
+ # usage example:
37
+ # data = { color: "blue", cool: true, popularity: 100, tags: ["tag1", "tag2"] }
38
+ # options = { body_before: "some text before the data", body_after: "some text after the data", include_closed: true }
39
+ # db.create("event123", {cool: true, data: "here"}, options)
40
+ def create(key, data, options = {})
41
+ @log.debug("attempting to create: #{key}")
42
+ issue = find_issue_by_key(key, options, create_mode: true)
43
+ if issue
44
+ @log.warn("skipping issue creation and returning existing issue - an issue already exists with the key: #{key}")
45
+ return Record.new(issue)
46
+ end
47
+
48
+ # if we make it here, no existing issues were found so we can safely create one
49
+
50
+ body = generate(data, body_before: options[:body_before], body_after: options[:body_after])
51
+
52
+ # if we make it here, no existing issues were found so we can safely create one
53
+ issue = @client.create_issue(@repo.full_name, key, body, { labels: @label })
54
+
55
+ # ensure the cache is initialized before appending and handle race conditions
56
+ current_issues = issues
57
+ if current_issues && !current_issues.include?(issue)
58
+ @issues << issue
59
+ end
60
+
61
+ @log.debug("issue created: #{key}")
44
62
  return Record.new(issue)
45
63
  end
46
64
 
47
- # if we make it here, no existing issues were found so we can safely create one
48
-
49
- body = generate(data, body_before: options[:body_before], body_after: options[:body_after])
50
-
51
- # if we make it here, no existing issues were found so we can safely create one
52
- issue = @client.create_issue(@repo.full_name, key, body, { labels: @label })
53
-
54
- # ensure the cache is initialized before appending and handle race conditions
55
- current_issues = issues
56
- if current_issues && !current_issues.include?(issue)
57
- @issues << issue
65
+ # Read an issue/record from the database
66
+ # This will return the issue as a Record object (parsed)
67
+ # :param: key [String] the key (issue title) to read
68
+ # :param: options [Hash] a hash of options to pass through to the search method
69
+ # :return: The issue as a Record object
70
+ def read(key, options = {})
71
+ @log.debug("attempting to read: #{key}")
72
+ issue = find_issue_by_key(key, options)
73
+ @log.debug("issue found: #{key}")
74
+ return Record.new(issue)
58
75
  end
59
76
 
60
- @log.debug("issue created: #{key}")
61
- return Record.new(issue)
62
- end
63
-
64
- # Read an issue/record from the database
65
- # This will return the issue as a Record object (parsed)
66
- # :param: key [String] the key (issue title) to read
67
- # :param: options [Hash] a hash of options to pass through to the search method
68
- # :return: The issue as a Record object
69
- def read(key, options = {})
70
- @log.debug("attempting to read: #{key}")
71
- issue = find_issue_by_key(key, options)
72
- @log.debug("issue found: #{key}")
73
- return Record.new(issue)
74
- end
75
-
76
- # Update an issue/record in the database
77
- # This will return the updated issue as a Record object (parsed)
78
- # :param: key [String] the key (issue title) to update
79
- # :param: data [Hash] the data to use for the issue body
80
- # :param: options [Hash] a hash of options containing extra data such as body_before and body_after
81
- # :return: The updated issue as a Record object
82
- # usage example:
83
- # data = { color: "blue", cool: true, popularity: 100, tags: ["tag1", "tag2"] }
84
- # options = { body_before: "some text before the data", body_after: "some text after the data", include_closed: true }
85
- # db.update("event123", {cool: true, data: "here"}, options)
86
- def update(key, data, options = {})
87
- @log.debug("attempting to update: #{key}")
88
- issue = find_issue_by_key(key, options)
89
-
90
- body = generate(data, body_before: options[:body_before], body_after: options[:body_after])
91
-
92
- updated_issue = @client.update_issue(@repo.full_name, issue.number, key, body)
93
-
94
- # update the issue in the cache using the reference we have
95
- index = @issues.index(issue)
96
- if index
97
- @issues[index] = updated_issue
98
- else
99
- @log.warn("issue not found in cache during update: #{key}")
100
- # Force a cache refresh to ensure consistency
101
- update_issue_cache!
77
+ # Update an issue/record in the database
78
+ # This will return the updated issue as a Record object (parsed)
79
+ # :param: key [String] the key (issue title) to update
80
+ # :param: data [Hash] the data to use for the issue body
81
+ # :param: options [Hash] a hash of options containing extra data such as body_before and body_after
82
+ # :return: The updated issue as a Record object
83
+ # usage example:
84
+ # data = { color: "blue", cool: true, popularity: 100, tags: ["tag1", "tag2"] }
85
+ # options = { body_before: "some text before the data", body_after: "some text after the data", include_closed: true }
86
+ # db.update("event123", {cool: true, data: "here"}, options)
87
+ def update(key, data, options = {})
88
+ @log.debug("attempting to update: #{key}")
89
+ issue = find_issue_by_key(key, options)
90
+
91
+ body = generate(data, body_before: options[:body_before], body_after: options[:body_after])
92
+
93
+ updated_issue = @client.update_issue(@repo.full_name, issue.number, key, body)
94
+
95
+ # update the issue in the cache using the reference we have
96
+ index = @issues.index(issue)
97
+ if index
98
+ @issues[index] = updated_issue
99
+ else
100
+ @log.warn("issue not found in cache during update: #{key}")
101
+ # Force a cache refresh to ensure consistency
102
+ update_issue_cache!
103
+ end
104
+
105
+ @log.debug("issue updated: #{key}")
106
+ return Record.new(updated_issue)
102
107
  end
103
108
 
104
- @log.debug("issue updated: #{key}")
105
- return Record.new(updated_issue)
106
- end
107
-
108
- # Delete an issue/record from the database - in this context, "delete" means to close the issue as "completed"
109
- # :param: key [String] the key (issue title) to delete
110
- # :param: options [Hash] a hash of options to pass through to the search method
111
- # :return: The deleted issue as a Record object (parsed) - it may contain useful data
112
- def delete(key, options = {})
113
- @log.debug("attempting to delete: #{key}")
114
- issue = find_issue_by_key(key, options)
115
-
116
- deleted_issue = @client.close_issue(@repo.full_name, issue.number)
117
-
118
- # update the issue in the cache using the reference we have
119
- index = @issues.index(issue)
120
- if index
121
- @issues[index] = deleted_issue
122
- else
123
- @log.warn("issue not found in cache during delete: #{key}")
124
- # Force a cache refresh to ensure consistency
125
- update_issue_cache!
109
+ # Delete an issue/record from the database - in this context, "delete" means to close the issue as "completed"
110
+ # :param: key [String] the key (issue title) to delete
111
+ # :param: options [Hash] a hash of options to pass through to the search method
112
+ # :return: The deleted issue as a Record object (parsed) - it may contain useful data
113
+ def delete(key, options = {})
114
+ @log.debug("attempting to delete: #{key}")
115
+ issue = find_issue_by_key(key, options)
116
+
117
+ deleted_issue = @client.close_issue(@repo.full_name, issue.number)
118
+
119
+ # update the issue in the cache using the reference we have
120
+ index = @issues.index(issue)
121
+ if index
122
+ @issues[index] = deleted_issue
123
+ else
124
+ @log.warn("issue not found in cache during delete: #{key}")
125
+ # Force a cache refresh to ensure consistency
126
+ update_issue_cache!
127
+ end
128
+
129
+ # return the deleted issue as a Record object as it may contain useful data
130
+ return Record.new(deleted_issue)
126
131
  end
127
132
 
128
- # return the deleted issue as a Record object as it may contain useful data
129
- return Record.new(deleted_issue)
130
- end
131
-
132
- # List all keys in the database
133
- # This will return an array of strings that represent the issue titles that are "keys" in the database
134
- # :param: options [Hash] a hash of options to pass through to the search method
135
- # :return: An array of strings that represent the issue titles that are "keys" in the database
136
- # usage example:
137
- # options = {include_closed: true}
138
- # keys = db.list_keys(options)
139
- def list_keys(options = {})
140
- current_issues = issues
141
- return [] if current_issues.nil?
142
-
143
- keys = current_issues.select do |issue|
144
- options[:include_closed] || issue[:state] == "open"
145
- end.map do |issue|
146
- issue[:title]
133
+ # List all keys in the database
134
+ # This will return an array of strings that represent the issue titles that are "keys" in the database
135
+ # :param: options [Hash] a hash of options to pass through to the search method
136
+ # :return: An array of strings that represent the issue titles that are "keys" in the database
137
+ # usage example:
138
+ # options = {include_closed: true}
139
+ # keys = db.list_keys(options)
140
+ def list_keys(options = {})
141
+ current_issues = issues
142
+ return [] if current_issues.nil?
143
+
144
+ keys = current_issues.select do |issue|
145
+ options[:include_closed] || issue[:state] == "open"
146
+ end.map do |issue|
147
+ issue[:title]
148
+ end
149
+
150
+ return keys
147
151
  end
148
152
 
149
- return keys
150
- end
151
-
152
- # List all issues/record in the database as Record objects (parsed)
153
- # This will return an array of Record objects that represent the issues in the database
154
- # :param: options [Hash] a hash of options to pass through to the search method
155
- # :return: An array of Record objects that represent the issues in the database
156
- # usage example:
157
- # options = {include_closed: true}
158
- # records = db.list(options)
159
- def list(options = {})
160
- current_issues = issues
161
- return [] if current_issues.nil?
162
-
163
- records = current_issues.select do |issue|
164
- options[:include_closed] || issue[:state] == "open"
165
- end.map do |issue|
166
- Record.new(issue)
153
+ # List all issues/record in the database as Record objects (parsed)
154
+ # This will return an array of Record objects that represent the issues in the database
155
+ # :param: options [Hash] a hash of options to pass through to the search method
156
+ # :return: An array of Record objects that represent the issues in the database
157
+ # usage example:
158
+ # options = {include_closed: true}
159
+ # records = db.list(options)
160
+ def list(options = {})
161
+ current_issues = issues
162
+ return [] if current_issues.nil?
163
+
164
+ records = current_issues.select do |issue|
165
+ options[:include_closed] || issue[:state] == "open"
166
+ end.map do |issue|
167
+ Record.new(issue)
168
+ end
169
+
170
+ return records
167
171
  end
168
172
 
169
- return records
170
- end
171
-
172
- # Force a refresh of the issues cache
173
- # This will update the issues cache with the latest issues from the repo
174
- # :return: The updated issue cache as a list of issues (Hash objects not parsed)
175
- def refresh!
176
- update_issue_cache!
177
- end
173
+ # Force a refresh of the issues cache
174
+ # This will update the issues cache with the latest issues from the repo
175
+ # :return: The updated issue cache as a list of issues (Hash objects not parsed)
176
+ def refresh!
177
+ update_issue_cache!
178
+ end
178
179
 
179
180
  protected
180
181
 
181
- def not_found!(key)
182
- raise RecordNotFound, "no record found for key: #{key}"
183
- end
184
-
185
- # A helper method to search through the issues cache and return the first issue that matches the given key
186
- # :param: key [String] the key (issue title) to search for
187
- # :param: options [Hash] a hash of options to pass through to the search method
188
- # :param: create_mode [Boolean] a flag to indicate whether or not we are in create mode
189
- # :return: A direct reference to the issue as a Hash object if found, otherwise throws a RecordNotFound error
190
- # ... unless create_mode is true, in which case it returns nil as a signal to proceed with creating the issue
191
- def find_issue_by_key(key, options = {}, create_mode: false)
192
- issue = issues.find do |issue|
193
- issue[:title] == key && (options[:include_closed] || issue[:state] == "open")
182
+ def not_found!(key)
183
+ raise RecordNotFound, "no record found for key: #{key}"
194
184
  end
195
185
 
196
- if issue.nil?
197
- @log.debug("no issue found in cache for: #{key}")
198
- return nil if create_mode
199
-
200
- not_found!(key)
186
+ # A helper method to search through the issues cache and return the first issue that matches the given key
187
+ # :param: key [String] the key (issue title) to search for
188
+ # :param: options [Hash] a hash of options to pass through to the search method
189
+ # :param: create_mode [Boolean] a flag to indicate whether or not we are in create mode
190
+ # :return: A direct reference to the issue as a Hash object if found, otherwise throws a RecordNotFound error
191
+ # ... unless create_mode is true, in which case it returns nil as a signal to proceed with creating the issue
192
+ def find_issue_by_key(key, options = {}, create_mode: false)
193
+ issue = issues.find do |issue|
194
+ issue[:title] == key && (options[:include_closed] || issue[:state] == "open")
195
+ end
196
+
197
+ if issue.nil?
198
+ @log.debug("no issue found in cache for: #{key}")
199
+ return nil if create_mode
200
+
201
+ not_found!(key)
202
+ end
203
+
204
+ @log.debug("issue found in cache for: #{key}")
205
+ return issue
201
206
  end
202
207
 
203
- @log.debug("issue found in cache for: #{key}")
204
- return issue
205
- end
208
+ # A helper method to fetch all issues from the repo and update the issues cache
209
+ # It is cache aware
210
+ def issues
211
+ # update the issues cache if it is nil
212
+ update_issue_cache! if @issues.nil?
206
213
 
207
- # A helper method to fetch all issues from the repo and update the issues cache
208
- # It is cache aware
209
- def issues
210
- # update the issues cache if it is nil
211
- update_issue_cache! if @issues.nil?
214
+ # update the cache if it has expired (with nil safety)
215
+ if !@issues_last_updated.nil? && (Time.now - @issues_last_updated) > @cache_expiry
216
+ @log.debug("issue cache expired - last updated: #{@issues_last_updated} - refreshing now")
217
+ update_issue_cache!
218
+ end
212
219
 
213
- # update the cache if it has expired (with nil safety)
214
- if !@issues_last_updated.nil? && (Time.now - @issues_last_updated) > @cache_expiry
215
- @log.debug("issue cache expired - last updated: #{@issues_last_updated} - refreshing now")
216
- update_issue_cache!
220
+ return @issues
217
221
  end
218
-
219
- return @issues
220
222
  end
221
223
  end
@@ -2,34 +2,36 @@
2
2
 
3
3
  require_relative "../utils/parse"
4
4
 
5
- class IssueParseError < StandardError; end
5
+ module IssueDB
6
+ class IssueParseError < StandardError; end
6
7
 
7
- class Record
8
- include Parse
8
+ class Record
9
+ include Parse
9
10
 
10
- attr_reader :body_before, :data, :body_after, :source_data, :key
11
- def initialize(data)
12
- @key = data.title
13
- @source_data = data
14
- parse!
15
- end
11
+ attr_reader :body_before, :data, :body_after, :source_data, :key
12
+ def initialize(data)
13
+ @key = data.title
14
+ @source_data = data
15
+ parse!
16
+ end
16
17
 
17
18
  protected
18
19
 
19
- def parse!
20
- if @source_data.body.nil? || @source_data.body.strip == ""
21
- raise IssueParseError, "issue body is empty for issue number #{@source_data.number}"
20
+ def parse!
21
+ if @source_data.body.nil? || @source_data.body.strip == ""
22
+ raise IssueParseError, "issue body is empty for issue number #{@source_data.number}"
23
+ end
24
+
25
+ begin
26
+ parsed = parse(@source_data.body)
27
+ rescue JSON::ParserError => e
28
+ message = "failed to parse issue body data contents for issue number: #{@source_data.number} - #{e.message}"
29
+ raise IssueParseError, message
30
+ end
31
+
32
+ @body_before = parsed[:body_before]
33
+ @data = parsed[:data]
34
+ @body_after = parsed[:body_after]
22
35
  end
23
-
24
- begin
25
- parsed = parse(@source_data.body)
26
- rescue JSON::ParserError => e
27
- message = "failed to parse issue body data contents for issue number: #{@source_data.number} - #{e.message}"
28
- raise IssueParseError, message
29
- end
30
-
31
- @body_before = parsed[:body_before]
32
- @data = parsed[:data]
33
- @body_after = parsed[:body_after]
34
36
  end
35
37
  end
@@ -1,22 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class RepoFormatError < StandardError; end
3
+ module IssueDB
4
+ class RepoFormatError < StandardError; end
4
5
 
5
- class Repository
6
- attr_reader :owner, :repo, :full_name
7
- def initialize(repo)
8
- @repo = repo
9
- validate!
10
- end
6
+ class Repository
7
+ attr_reader :owner, :repo, :full_name
8
+ def initialize(repo)
9
+ @repo = repo
10
+ validate!
11
+ end
11
12
 
12
13
  protected
13
14
 
14
- def validate!
15
- if @repo.nil? || !@repo.include?("/")
16
- raise RepoFormatError, "repository #{@repo} is invalid - valid format: <owner>/<repo>"
17
- end
15
+ def validate!
16
+ if @repo.nil? || !@repo.include?("/")
17
+ raise RepoFormatError, "repository #{@repo} is invalid - valid format: <owner>/<repo>"
18
+ end
18
19
 
19
- @full_name = @repo.strip
20
- @owner, @repo = @full_name.split("/")
20
+ @full_name = @repo.strip
21
+ @owner, @repo = @full_name.split("/")
22
+ end
21
23
  end
22
24
  end
@@ -1,45 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class GenerateError < StandardError; end
3
+ module IssueDB
4
+ class GenerateError < StandardError; end
4
5
 
5
- module Generate
6
- # Generates the issue body with embedded data
7
- # :param data [Hash] the data to embed in the issue body
8
- # :param body_before [String] the body of the issue before the data (optional)
9
- # :param body_after [String] the body of the issue after the data (optional)
10
- # :param guard_start [String] the guard start string which is used to identify the start of the data
11
- # :param guard_end [String] the guard end string which is used to identify the end of the data
12
- # :return [String] the issue body with the embedded data
13
- def generate(
14
- data,
15
- body_before: nil,
16
- body_after: nil,
17
- guard_start: "<!--- issue-db-start -->",
18
- guard_end: "<!--- issue-db-end -->"
19
- )
6
+ module Generate
7
+ # Generates the issue body with embedded data
8
+ # :param data [Hash] the data to embed in the issue body
9
+ # :param body_before [String] the body of the issue before the data (optional)
10
+ # :param body_after [String] the body of the issue after the data (optional)
11
+ # :param guard_start [String] the guard start string which is used to identify the start of the data
12
+ # :param guard_end [String] the guard end string which is used to identify the end of the data
13
+ # :return [String] the issue body with the embedded data
14
+ def generate(
15
+ data,
16
+ body_before: nil,
17
+ body_after: nil,
18
+ guard_start: "<!--- issue-db-start -->",
19
+ guard_end: "<!--- issue-db-end -->"
20
+ )
20
21
 
21
- # json formatting options
22
- opts = {
23
- indent: " ",
24
- space: " ",
25
- object_nl: "\n",
26
- array_nl: "\n",
27
- allow_nan: true,
28
- max_nesting: false
29
- }
22
+ # json formatting options
23
+ opts = {
24
+ indent: " ",
25
+ space: " ",
26
+ object_nl: "\n",
27
+ array_nl: "\n",
28
+ allow_nan: true,
29
+ max_nesting: false
30
+ }
30
31
 
31
- json_data = JSON.pretty_generate(data, opts)
32
+ json_data = JSON.pretty_generate(data, opts)
32
33
 
33
- # construct the body
34
- body = ""
35
- body += "#{body_before}\n" unless body_before.nil? # the first part of the body
36
- body += "#{guard_start}\n" # the start of the data
37
- body += "```json\n" # the start of the json codeblock
38
- body += "#{json_data}\n" # the data
39
- body += "```\n" # the end of the json codeblock
40
- body += "#{guard_end}\n" # the end of the data
41
- body += body_after unless body_after.nil? # the last part of the body
34
+ # construct the body
35
+ body = ""
36
+ body += "#{body_before}\n" unless body_before.nil? # the first part of the body
37
+ body += "#{guard_start}\n" # the start of the data
38
+ body += "```json\n" # the start of the json codeblock
39
+ body += "#{json_data}\n" # the data
40
+ body += "```\n" # the end of the json codeblock
41
+ body += "#{guard_end}\n" # the end of the data
42
+ body += body_after unless body_after.nil? # the last part of the body
42
43
 
43
- return body
44
+ return body
45
+ end
44
46
  end
45
47
  end