fbe 0.14.1 → 0.15.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c645873d437ec40589dca8b3bd3789b3dd859d5c5b365e25dae38b82bd4d5feb
4
- data.tar.gz: d586ef4096f29632302503f72f24f39511567760e179b0ca4373626b8541eb8e
3
+ metadata.gz: db8d48febaa0fde74386158a1487da848c28058c4fadf85838b110105d70ada8
4
+ data.tar.gz: 46eec95f52085235848a464c2770887e5336f7b818644e61955fa3fbb026e84c
5
5
  SHA512:
6
- metadata.gz: a52fb8eaeab99279664d91e6b1bb50492f40f4c2583937c64e412822f84428863caaf6a33f330dc7706d1c0aad809761337b0a241163d4f914235b66c3fb59fc
7
- data.tar.gz: 88d9ddc164b53f357515b223a69ac9622f1860258ff36fa94ce2ff6d7b3b6f996c81f495734c41f9a3fd5f5581bbf8085358a06c516f50b0466d7a2abe7eea7f
6
+ metadata.gz: 90435fd9b0555636891b524d69835835096b69e3673ed21fe9a49ee2a5b8792040bbdad2fa303f05f7a86c0cc06e35c595b7968ce32e8a7c32e6a94d5f839c9e
7
+ data.tar.gz: ca87fd6888148a43e582ab14cc110189aa741bad4e57c44aca4755db004de9bbef539e3b245dfe7330461b74eb39545f005a632eb0c8ae5ed7801c84f9b2b980
@@ -12,4 +12,4 @@ jobs:
12
12
  runs-on: ubuntu-24.04
13
13
  steps:
14
14
  - uses: actions/checkout@v4
15
- - uses: yegor256/copyrights-action@0.0.8
15
+ - uses: yegor256/copyrights-action@0.0.12
data/Gemfile.lock CHANGED
@@ -110,7 +110,7 @@ GEM
110
110
  i18n (1.14.7)
111
111
  concurrent-ruby (~> 1.0)
112
112
  iri (0.10.0)
113
- json (2.12.0)
113
+ json (2.12.2)
114
114
  judges (0.42.1)
115
115
  backtrace (~> 0)
116
116
  baza.rb (~> 0)
@@ -172,11 +172,11 @@ GEM
172
172
  tago (> 0)
173
173
  racc (1.8.1)
174
174
  rainbow (3.1.1)
175
- rake (13.2.1)
175
+ rake (13.3.0)
176
176
  regexp_parser (2.10.0)
177
177
  retries (0.0.5)
178
178
  rexml (3.4.1)
179
- rubocop (1.75.6)
179
+ rubocop (1.75.8)
180
180
  json (~> 2.3)
181
181
  language_server-protocol (~> 3.17.0.2)
182
182
  lint_roller (~> 1.1.0)
@@ -190,7 +190,7 @@ GEM
190
190
  rubocop-ast (1.44.1)
191
191
  parser (>= 3.3.7.2)
192
192
  prism (~> 1.4)
193
- rubocop-minitest (0.38.0)
193
+ rubocop-minitest (0.38.1)
194
194
  lint_roller (~> 1.1)
195
195
  rubocop (>= 1.75.0, < 2.0)
196
196
  rubocop-ast (>= 1.38.0, < 2.0)
data/lib/fbe/bylaws.rb CHANGED
@@ -6,15 +6,35 @@
6
6
  require 'liquid'
7
7
  require_relative '../fbe'
8
8
 
9
- # Generates policies/bylaws.
9
+ # Generates policies/bylaws from Liquid templates.
10
10
  #
11
11
  # Using the templates stored in the +assets/bylaws+ directory, this function
12
- # creates a hash, where keys are names and values are formulas of bylaws.
12
+ # creates a hash where keys are bylaw names (derived from filenames) and values
13
+ # are the rendered formulas. Templates can use three parameters to control
14
+ # the strictness and generosity of the bylaws.
13
15
  #
14
- # @param [Integer] anger How strict must be the bylaws, giving punishments
15
- # @param [Integer] love How big should be the volume of rewards
16
- # @param [Integer] paranoia How much should be required to reward love
17
- # @return [Hash<String, String>] Names of bylaws and their formulas
16
+ # @param [Integer] anger Strictness level for punishments (0-4, default: 2)
17
+ # - 0: Very lenient, minimal punishments
18
+ # - 2: Balanced approach (default)
19
+ # - 4: Very strict, maximum punishments
20
+ # @param [Integer] love Generosity level for rewards (0-4, default: 2)
21
+ # - 0: Minimal rewards
22
+ # - 2: Balanced rewards (default)
23
+ # - 4: Maximum rewards
24
+ # @param [Integer] paranoia Requirements threshold for rewards (1-4, default: 2)
25
+ # - 1: Easy to earn rewards
26
+ # - 2: Balanced requirements (default)
27
+ # - 4: Very difficult to earn rewards
28
+ # @return [Hash<String, String>] Hash mapping bylaw names to their formulas
29
+ # @raise [RuntimeError] If parameters are out of valid ranges
30
+ # @example Generate balanced bylaws
31
+ # bylaws = Fbe.bylaws(anger: 2, love: 2, paranoia: 2)
32
+ # bylaws['bug-report-was-rewarded']
33
+ # # => "award { 2 * love * paranoia }"
34
+ # @example Generate strict bylaws with minimal rewards
35
+ # bylaws = Fbe.bylaws(anger: 4, love: 1, paranoia: 3)
36
+ # bylaws['dud-was-punished']
37
+ # # => "award { -16 * anger }"
18
38
  def Fbe.bylaws(anger: 2, love: 2, paranoia: 2)
19
39
  raise "The 'anger' must be in the [0..4] interval: #{anger.inspect}" unless !anger.negative? && anger < 5
20
40
  raise "The 'love' must be in the [0..4] interval: #{love.inspect}" unless !love.negative? && love < 5
data/lib/fbe/copy.rb CHANGED
@@ -9,12 +9,20 @@ require_relative 'fb'
9
9
  # Makes a copy of a fact, moving all properties to a new fact.
10
10
  #
11
11
  # All properties from the +source+ will be copied to the +target+, except those
12
- # listed in the +except+.
12
+ # listed in the +except+ array. Only copies properties that don't already exist
13
+ # in the target. Multi-valued properties are copied with all their values.
13
14
  #
14
- # @param [Factbase::Fact] source The source
15
- # @param [Factbase::Fact] target The target
16
- # @param [Array<String>] except List of properties to NOT copy
17
- # @return [Integer] How many properties were copied
15
+ # @param [Factbase::Fact] source The source fact to copy from
16
+ # @param [Factbase::Fact] target The target fact to copy to
17
+ # @param [Array<String>] except List of property names to NOT copy (defaults to empty)
18
+ # @return [Integer] The number of property values that were copied
19
+ # @raise [RuntimeError] If source, target, or except is nil
20
+ # @note Existing properties in target are preserved (not overwritten)
21
+ # @example Copy all properties except timestamps
22
+ # source = fb.query('(eq type "user")').first
23
+ # target = fb.insert
24
+ # count = Fbe.copy(source, target, except: ['_time', '_id'])
25
+ # puts "Copied #{count} property values"
18
26
  def Fbe.copy(source, target, except: [])
19
27
  raise 'The source is nil' if source.nil?
20
28
  raise 'The target is nil' if target.nil?
data/lib/fbe/delete.rb CHANGED
@@ -6,13 +6,22 @@
6
6
  require_relative '../fbe'
7
7
  require_relative 'fb'
8
8
 
9
- # Delete a few properties from the fact.
9
+ # Delete properties from a fact by creating a new fact without them.
10
10
  #
11
- # @param [Factbase::Fact] source The source
12
- # @param [Array<String>] props List of properties to delete
13
- # @param [Factbase] fb The factbase
14
- # @param [String] id The unique ID of the fact
15
- # @return [Factbase::Fact] New fact
11
+ # This method doesn't modify the original fact. Instead, it deletes the existing
12
+ # fact from the factbase and creates a new one with all properties except those
13
+ # specified for deletion.
14
+ #
15
+ # @param [Factbase::Fact] fact The fact to delete properties from (must have an ID)
16
+ # @param [Array<String>] props List of property names to delete
17
+ # @param [Factbase] fb The factbase to use (defaults to Fbe.fb)
18
+ # @param [String] id The property name used as unique identifier (defaults to '_id')
19
+ # @return [Factbase::Fact] New fact without the deleted properties
20
+ # @raise [RuntimeError] If fact is nil, has no ID, or ID property doesn't exist
21
+ # @example Delete multiple properties from a fact
22
+ # fact = fb.query('(eq type "user")').first
23
+ # new_fact = Fbe.delete(fact, 'age', 'city')
24
+ # # new_fact will have all properties except 'age' and 'city'
16
25
  def Fbe.delete(fact, *props, fb: Fbe.fb, id: '_id')
17
26
  raise 'The fact is nil' if fact.nil?
18
27
  i = fact[id]
data/lib/fbe/enter.rb CHANGED
@@ -6,13 +6,25 @@
6
6
  require 'baza-rb'
7
7
  require_relative '../fbe'
8
8
 
9
- # Enter a new valve.
9
+ # Enter a new valve in the Zerocracy system.
10
10
  #
11
- # @param [String] badge Unique badge of the valve
12
- # @param [String] why The reason
13
- # @param [Judges::Options] options The options coming from the +judges+ tool
14
- # @param [Loog] loog The logging facility
15
- # @return [String] Full name of the user
11
+ # A valve is a checkpoint or gate in the processing pipeline. This method
12
+ # records the entry into a valve with a reason, unless in testing mode.
13
+ #
14
+ # @param [String] badge Unique badge identifier for the valve
15
+ # @param [String] why The reason for entering this valve
16
+ # @param [Judges::Options] options The options from judges tool (uses $options if not provided)
17
+ # @param [Loog] loog The logging facility (uses $loog if not provided)
18
+ # @yield Block to execute within the valve context
19
+ # @return [Object] The result of the yielded block
20
+ # @raise [RuntimeError] If badge, why, or required globals are nil
21
+ # @note Requires $options and $loog global variables to be set
22
+ # @note In testing mode (options.testing != nil), bypasses valve recording
23
+ # @example Enter a valve for processing
24
+ # Fbe.enter('payment-check', 'Validating payment data') do
25
+ # # Process payment validation
26
+ # validate_payment(data)
27
+ # end
16
28
  def Fbe.enter(badge, why, options: $options, loog: $loog, &)
17
29
  raise 'The badge is nil' if badge.nil?
18
30
  raise 'The why is nil' if why.nil?
@@ -271,12 +271,34 @@ class Fbe::Graph
271
271
  end
272
272
  end
273
273
 
274
- # Fake GitHub GraphQL client, for tests.
274
+ # Fake GitHub GraphQL client for testing.
275
+ #
276
+ # This class mocks the GraphQL client interface and returns predictable
277
+ # test data without making actual API calls. It's used when the application
278
+ # is in testing mode.
279
+ #
280
+ # @example Using the fake client in tests
281
+ # fake = Fbe::Graph::Fake.new
282
+ # result = fake.total_commits('owner', 'repo', 'main')
283
+ # # => 1484 (always returns the same value)
275
284
  class Fake
285
+ # Executes a GraphQL query (mock implementation).
286
+ #
287
+ # @param [String] _query The GraphQL query (ignored)
288
+ # @return [Hash] Empty hash
276
289
  def query(_query)
277
290
  {}
278
291
  end
279
292
 
293
+ # Returns mock resolved conversation threads.
294
+ #
295
+ # @param [String] owner Repository owner
296
+ # @param [String] name Repository name
297
+ # @param [Integer] _number Pull request number (ignored)
298
+ # @return [Array<Hash>] Array of conversation threads
299
+ # @example
300
+ # fake.resolved_conversations('zerocracy', 'baza', 42)
301
+ # # => [conversation data for zerocracy_baza]
280
302
  def resolved_conversations(owner, name, _number)
281
303
  data = {
282
304
  zerocracy_baza: [
@@ -286,6 +308,14 @@ class Fbe::Graph
286
308
  data[:"#{owner}_#{name}"] || []
287
309
  end
288
310
 
311
+ # Returns mock issue and pull request counts.
312
+ #
313
+ # @param [String] _owner Repository owner (ignored)
314
+ # @param [String] _name Repository name (ignored)
315
+ # @return [Hash] Hash with 'issues' and 'pulls' counts
316
+ # @example
317
+ # fake.total_issues_and_pulls('owner', 'repo')
318
+ # # => {"issues"=>23, "pulls"=>19}
289
319
  def total_issues_and_pulls(_owner, _name)
290
320
  {
291
321
  'issues' => 23,
@@ -293,10 +323,23 @@ class Fbe::Graph
293
323
  }
294
324
  end
295
325
 
326
+ # Returns mock total commit count.
327
+ #
328
+ # @param [String] _owner Repository owner (ignored)
329
+ # @param [String] _name Repository name (ignored)
330
+ # @param [String] _branch Branch name (ignored)
331
+ # @return [Integer] Always returns 1484
296
332
  def total_commits(_owner, _name, _branch)
297
333
  1484
298
334
  end
299
335
 
336
+ # Returns mock issue type event data.
337
+ #
338
+ # @param [String] node_id The event node ID
339
+ # @return [Hash, nil] Event data for known IDs, nil otherwise
340
+ # @example
341
+ # fake.issue_type_event('ITAE_examplevq862Ga8lzwAAAAQZanzv')
342
+ # # => {'type'=>'IssueTypeAddedEvent', ...}
300
343
  def issue_type_event(node_id)
301
344
  case node_id
302
345
  when 'ITAE_examplevq862Ga8lzwAAAAQZanzv'
@@ -362,6 +405,10 @@ class Fbe::Graph
362
405
 
363
406
  private
364
407
 
408
+ # Generates mock conversation thread data.
409
+ #
410
+ # @param [String] id The conversation thread ID
411
+ # @return [Hash] Mock conversation data with comments
365
412
  def conversation(id)
366
413
  {
367
414
  'id' => id,
data/lib/fbe/if_absent.rb CHANGED
@@ -8,28 +8,43 @@ require 'time'
8
8
  require_relative '../fbe'
9
9
  require_relative 'fb'
10
10
 
11
- # Injects a fact if it's absent in the factbase, otherwise (if it is already
12
- # there) returns NIL.
11
+ # Injects a fact if it's absent in the factbase, otherwise returns nil.
12
+ #
13
+ # Checks if a fact with the same property values already exists. If not,
14
+ # creates a new fact. System properties (_id, _time, _version) are excluded
15
+ # from the uniqueness check.
13
16
  #
14
17
  # Here is what you do when you want to add a fact to the factbase, but
15
18
  # don't want to make a duplicate of an existing one:
16
19
  #
17
20
  # require 'fbe/if_absent'
18
- # n =
19
- # Fbe.if_absent do |f|
20
- # f.what = 'something'
21
- # f.details = 'important'
22
- # end
23
- # return if n.nil?
24
- # n.when = Time.now
21
+ # n = Fbe.if_absent do |f|
22
+ # f.what = 'something'
23
+ # f.details = 'important'
24
+ # end
25
+ # return if n.nil? # Fact already existed
26
+ # n.when = Time.now # Add additional properties to the new fact
25
27
  #
26
28
  # This code will definitely create one fact with +what+ equals to +something+
27
29
  # and +details+ equals to +important+, while the +when+ will be equal to the
28
30
  # time of its first creation.
29
31
  #
30
- # @param [Factbase] fb The global factbase
31
- # @yield [Factbase::Fact] The fact just created
32
- # @return [nil|Factbase::Fact] Either +nil+ if it's already there or a new fact
32
+ # @param [Factbase] fb The factbase to check and insert into (defaults to Fbe.fb)
33
+ # @yield [Factbase::Fact] A proxy fact object to set properties on
34
+ # @return [nil, Factbase::Fact] nil if fact exists, otherwise the newly created fact
35
+ # @note String values are properly escaped in queries
36
+ # @note Time values are converted to UTC ISO8601 format for comparison
37
+ # @example Ensure unique user registration
38
+ # user = Fbe.if_absent do |f|
39
+ # f.type = 'user'
40
+ # f.email = 'john@example.com'
41
+ # end
42
+ # if user
43
+ # user.registered_at = Time.now
44
+ # puts "New user created"
45
+ # else
46
+ # puts "User already exists"
47
+ # end
33
48
  def Fbe.if_absent(fb: Fbe.fb)
34
49
  attrs = {}
35
50
  f =
data/lib/fbe/issue.rb CHANGED
@@ -6,18 +6,27 @@
6
6
  require_relative '../fbe'
7
7
  require_relative 'octo'
8
8
 
9
- # Converts an ID of GitHub issue into a nicely formatted string.
9
+ # Converts GitHub repository and issue IDs into a formatted issue reference.
10
10
  #
11
- # The function takes the +repository+ property of the provided +fact+,
12
- # goes to the GitHub API in order to find the full name of the repository,
13
- # and then creates a string with the full name of repository + issue, for
14
- # example +"zerocracy/fbe#42"+.
11
+ # Takes the +repository+ and +issue+ properties from the provided +fact+,
12
+ # queries the GitHub API to get the repository's full name, and formats it
13
+ # as a standard GitHub issue reference (e.g., "zerocracy/fbe#42").
14
+ # Results are cached globally to minimize API calls.
15
15
  #
16
- # @param [Factbase::Fact] fact The fact, where to get the ID of GitHub issue
17
- # @param [Judges::Options] options The options coming from the +judges+ tool
18
- # @param [Hash] global The hash for global caching
19
- # @param [Loog] loog The logging facility
20
- # @return [String] Textual representation of GitHub issue number
16
+ # @param [Factbase::Fact] fact The fact containing repository and issue properties
17
+ # @param [Judges::Options] options The options from judges tool (uses $options global)
18
+ # @param [Hash] global The hash for global caching (uses $global)
19
+ # @param [Loog] loog The logging facility (uses $loog global)
20
+ # @return [String] Formatted issue reference (e.g., "owner/repo#123")
21
+ # @raise [RuntimeError] If fact is nil or required properties are missing
22
+ # @raise [RuntimeError] If required global variables are not set
23
+ # @note Requires 'repository' and 'issue' properties in the fact
24
+ # @note Repository names are cached to reduce GitHub API calls
25
+ # @example Format an issue reference
26
+ # issue_fact = fb.query('(eq type "issue")').first
27
+ # issue_fact.repository = 549866411 # Repository ID
28
+ # issue_fact.issue = 42 # Issue number
29
+ # puts Fbe.issue(issue_fact) # => "zerocracy/fbe#42"
21
30
  def Fbe.issue(fact, options: $options, global: $global, loog: $loog)
22
31
  raise 'The fact is nil' if fact.nil?
23
32
  raise 'The $global is not set' if global.nil?
data/lib/fbe/iterate.rb CHANGED
@@ -10,13 +10,30 @@ require_relative 'fb'
10
10
  require_relative 'octo'
11
11
  require_relative 'unmask_repos'
12
12
 
13
- # Creates an instance of {Fbe::Iterate} and evals it with the block provided.
13
+ # Creates an instance of {Fbe::Iterate} and evaluates it with the provided block.
14
14
  #
15
- # @param [Factbase] fb The global factbase provided by the +judges+ tool
16
- # @param [Judges::Options] options The options coming from the +judges+ tool
17
- # @param [Hash] global The hash for global caching
18
- # @param [Loog] loog The logging facility
19
- # @yield [Factbase::Fact] The fact
15
+ # This is a convenience method that creates an iterator instance and evaluates
16
+ # the DSL block within its context. The iterator processes repositories defined
17
+ # in options.repositories, executing queries and managing state for each.
18
+ #
19
+ # @param [Factbase] fb The global factbase provided by the +judges+ tool (defaults to Fbe.fb)
20
+ # @param [Judges::Options] options The options from judges tool (uses $options global)
21
+ # @param [Hash] global The hash for global caching (uses $global)
22
+ # @param [Loog] loog The logging facility (uses $loog global)
23
+ # @yield Block containing DSL methods (as, by, over, etc.) to configure iteration
24
+ # @return [Object] Result of the block evaluation
25
+ # @raise [RuntimeError] If required globals are not set
26
+ # @example Iterate through repositories processing issues
27
+ # Fbe.iterate do
28
+ # as 'issues-iterator'
29
+ # by '(and (eq what "issue") (gt created_at $before))'
30
+ # repeats 5
31
+ # quota_aware
32
+ # over(timeout: 300) do |repository_id, issue_id|
33
+ # process_issue(repository_id, issue_id)
34
+ # issue_id + 1
35
+ # end
36
+ # end
20
37
  def Fbe.iterate(fb: Fbe.fb, loog: $loog, options: $options, global: $global, &)
21
38
  raise 'The fb is nil' if fb.nil?
22
39
  raise 'The $global is not set' if global.nil?
@@ -26,24 +43,44 @@ def Fbe.iterate(fb: Fbe.fb, loog: $loog, options: $options, global: $global, &)
26
43
  c.instance_eval(&)
27
44
  end
28
45
 
29
- # An iterator.
46
+ # Repository iterator with stateful query execution.
47
+ #
48
+ # This class provides a DSL for iterating through repositories and executing
49
+ # queries while maintaining state between iterations. It tracks progress using
50
+ # "marker" facts in the factbase and supports features like:
51
+ #
52
+ # - Stateful iteration with automatic restart capability
53
+ # - GitHub API quota awareness to prevent rate limit issues
54
+ # - Configurable repeat counts per repository
55
+ # - Timeout controls for long-running operations
30
56
  #
31
- # Here, you go through all repositories defined by the +repositories+ option
32
- # in the +$options+, trying to run the provided query for each of them. If the
33
- # query returns an integer that is different from the previously seen, the
34
- # function keeps repeating the cycle. Otherwise, it will restart from the
35
- # beginning.
57
+ # The iterator executes a query for each repository, passing the previous
58
+ # result as context. If the query returns nil, it restarts from the beginning
59
+ # for that repository. Progress is persisted in the factbase to support
60
+ # resuming after interruptions.
61
+ #
62
+ # @example Processing pull requests with state management
63
+ # iterator = Fbe::Iterate.new(fb: fb, loog: loog, options: options, global: global)
64
+ # iterator.as('pull-requests')
65
+ # iterator.by('(and (eq what "pull_request") (gt number $before))')
66
+ # iterator.repeats(10)
67
+ # iterator.quota_aware
68
+ # iterator.over(timeout: 600) do |repo_id, pr_number|
69
+ # # Process pull request
70
+ # fetch_and_store_pr(repo_id, pr_number)
71
+ # pr_number # Return next PR number to process
72
+ # end
36
73
  #
37
74
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
38
75
  # Copyright:: Copyright (c) 2024-2025 Zerocracy
39
76
  # License:: MIT
40
77
  class Fbe::Iterate
41
- # Ctor.
78
+ # Creates a new iterator instance.
42
79
  #
43
- # @param [Factbase] fb The factbase
44
- # @param [Loog] loog The logging facility
45
- # @param [Judges::Options] options The options coming from the +judges+ tool
46
- # @param [Hash] global The hash for global caching
80
+ # @param [Factbase] fb The factbase for storing iteration state
81
+ # @param [Loog] loog The logging facility for debug output
82
+ # @param [Judges::Options] options The options containing repository configuration
83
+ # @param [Hash] global The hash for global caching of API responses
47
84
  def initialize(fb:, loog:, options:, global:)
48
85
  @fb = fb
49
86
  @loog = loog
@@ -56,56 +93,107 @@ class Fbe::Iterate
56
93
  @quota_aware = false
57
94
  end
58
95
 
59
- # Make this block aware of GitHub API quota.
96
+ # Makes the iterator aware of GitHub API quota limits.
60
97
  #
61
- # When the quota is reached, the loop will gracefully stop to avoid
62
- # hitting GitHub API rate limits. This helps prevent interruptions
63
- # in long-running operations.
98
+ # When enabled, the iterator will check quota status before processing
99
+ # each repository and gracefully stop when the quota is exhausted.
100
+ # This prevents API errors and allows for resuming later.
64
101
  #
65
102
  # @return [nil] Nothing is returned
103
+ # @example Enable quota awareness
104
+ # iterator.quota_aware
105
+ # iterator.over { |repo, item| ... } # Will stop if quota exhausted
66
106
  def quota_aware
67
107
  @quota_aware = true
68
108
  end
69
109
 
70
- # Sets the total counter of repeats to make.
110
+ # Sets the maximum number of iterations per repository.
111
+ #
112
+ # Controls how many times the query will be executed for each repository
113
+ # before moving to the next one. Useful for limiting processing scope.
71
114
  #
72
- # @param [Integer] repeats The total count of repeats to execute
115
+ # @param [Integer] repeats The maximum iterations per repository
73
116
  # @return [nil] Nothing is returned
117
+ # @raise [RuntimeError] If repeats is nil or not positive
118
+ # @example Process up to 100 items per repository
119
+ # iterator.repeats(100)
74
120
  def repeats(repeats)
75
121
  raise 'Cannot set "repeats" to nil' if repeats.nil?
76
122
  raise 'The "repeats" must be a positive integer' unless repeats.positive?
77
123
  @repeats = repeats
78
124
  end
79
125
 
80
- # Sets the query to run.
126
+ # Sets the query to execute for each iteration.
127
+ #
128
+ # The query can use two special variables:
129
+ # - $before: The value from the previous iteration (or initial value)
130
+ # - $repository: The current repository ID
81
131
  #
82
- # @param [String] query The query to execute
132
+ # @param [String] query The Factbase query to execute
83
133
  # @return [nil] Nothing is returned
134
+ # @raise [RuntimeError] If query is already set or nil
135
+ # @example Query for issues after a certain ID
136
+ # iterator.by('(and (eq what "issue") (gt id $before) (eq repo $repository))')
84
137
  def by(query)
85
138
  raise 'Query is already set' unless @query.nil?
86
139
  raise 'Cannot set query to nil' if query.nil?
87
140
  @query = query
88
141
  end
89
142
 
90
- # Sets the label to use in the "marker" fact.
143
+ # Sets the label for tracking iteration state.
91
144
  #
92
- # @param [String] label The label identifier
145
+ # The label is used to create marker facts in the factbase that track
146
+ # the last processed item for each repository. This enables resuming
147
+ # iteration after interruptions.
148
+ #
149
+ # @param [String] label Unique identifier for this iteration type
93
150
  # @return [nil] Nothing is returned
151
+ # @raise [RuntimeError] If label is already set or nil
152
+ # @example Set label for issue processing
153
+ # iterator.as('issue-processor')
94
154
  def as(label)
95
155
  raise 'Label is already set' unless @label.nil?
96
156
  raise 'Cannot set "label" to nil' if label.nil?
97
157
  @label = label
98
158
  end
99
159
 
100
- # It makes a number of repeats of going through all repositories
101
- # provided by the +repositories+ configuration option. In each "repeat"
102
- # it yields the repository ID and a number that is retrieved by the
103
- # +query+. The query is supplied with two parameters:
104
- # +$before+ (the value from the previous repeat) and +$repository+ (GitHub repo ID).
160
+ # Executes the iteration over all configured repositories.
161
+ #
162
+ # For each repository, retrieves the last processed value (or uses the initial
163
+ # value from +since+) and executes the configured query with it. The query
164
+ # receives two parameters: $before (the last processed value) and $repository
165
+ # (GitHub repository ID).
166
+ #
167
+ # When the query returns a non-nil result, the block is called with the
168
+ # repository ID and query result. The block must return an Integer that will
169
+ # be stored as the new "latest" value for the next iteration.
170
+ #
171
+ # When the query returns nil, the iteration for that repository restarts
172
+ # from the initial value (set by +since+), and the block is NOT called.
173
+ #
174
+ # The method tracks progress using marker facts and supports:
175
+ # - Automatic restart when query returns nil
176
+ # - Timeout to prevent infinite loops
177
+ # - GitHub API quota checking (if enabled)
178
+ # - State persistence for resuming after interruptions
179
+ #
180
+ # Processing flow for each repository:
181
+ # 1. Read the "latest" value from factbase (or use +since+ if not found)
182
+ # 2. Execute the query with $before=latest and $repository=repo_id
183
+ # 3. If query returns nil: restart from +since+ value, skip to next repo
184
+ # 4. If query returns a value: call the block with (repo_id, query_result)
185
+ # 5. Store the block's return value as the new "latest" for next iteration
105
186
  #
106
- # @param [Float] timeout How many seconds to spend as a maximum
107
- # @yield [Integer, Integer] Repository ID and the next number to be considered
187
+ # @param [Float] timeout Maximum seconds to run (default: 120)
188
+ # @yield [Integer, Object] Repository ID and the result from query execution
189
+ # @yieldreturn [Integer] The value to store as "latest" for next iteration
108
190
  # @return [nil] Nothing is returned
191
+ # @raise [RuntimeError] If block doesn't return an Integer
192
+ # @example Process issues incrementally
193
+ # iterator.over(timeout: 300) do |repo_id, issue_number|
194
+ # fetch_and_process_issue(repo_id, issue_number)
195
+ # issue_number + 1 # Return next issue number to process
196
+ # end
109
197
  def over(timeout: 2 * 60, &)
110
198
  raise 'Use "as" first' if @label.nil?
111
199
  raise 'Use "by" first' if @query.nil?
@@ -129,7 +217,7 @@ class Fbe::Iterate
129
217
  break
130
218
  end
131
219
  if Time.now - start > timeout
132
- $loog.info("We are doing this for #{start.ago} already, won't check #{repo}")
220
+ @loog.info("We are doing this for #{start.ago} already, won't check #{repo}")
133
221
  next
134
222
  end
135
223
  next if restarted.include?(repo)
@@ -178,7 +266,7 @@ class Fbe::Iterate
178
266
  break
179
267
  end
180
268
  if Time.now - start > timeout
181
- $loog.info("We are iterating for #{start.ago} already, time to give up")
269
+ @loog.info("We are iterating for #{start.ago} already, time to give up")
182
270
  break
183
271
  end
184
272
  end
data/lib/fbe/just_one.rb CHANGED
@@ -8,22 +8,30 @@ require 'others'
8
8
  require_relative '../fbe'
9
9
  require_relative 'fb'
10
10
 
11
- # Injects a fact if it's absent in the factbase, otherwise (it is already
12
- # there) returns the existing one.
11
+ # Ensures exactly one fact exists with the specified attributes in the factbase.
13
12
  #
14
- # require 'fbe/just_one'
15
- # n =
16
- # Fbe.just_one do |f|
17
- # f.what = 'something'
18
- # f.details = 'important'
13
+ # This method creates a new fact if none exists with the given attributes,
14
+ # or returns an existing fact if one already matches. Useful for preventing
15
+ # duplicate facts while ensuring required facts exist.
16
+ #
17
+ # @example Creating or finding a unique fact
18
+ # require 'fbe/just_one'
19
+ # fact = Fbe.just_one do |f|
20
+ # f.what = 'github_issue'
21
+ # f.issue_id = 123
22
+ # f.repository = 'zerocracy/fbe'
19
23
  # end
24
+ # # Returns existing fact if one exists with these exact attributes,
25
+ # # otherwise creates and returns a new fact
20
26
  #
21
- # This code will guarantee that only one fact with +what+ equals to +something+
22
- # and +details+ equals to +important+ may exist.
27
+ # @example Attributes are matched exactly (case-sensitive)
28
+ # Fbe.just_one { |f| f.name = 'Test' } # Creates fact with name='Test'
29
+ # Fbe.just_one { |f| f.name = 'test' } # Creates another fact (different case)
23
30
  #
24
- # @param [Factbase] fb The global factbase
25
- # @yield [Factbase::Fact] The fact that was either created or found
26
- # @return [Factbase::Fact] The fact found
31
+ # @param [Factbase] fb The factbase to search/insert into (defaults to Fbe.fb)
32
+ # @yield [Factbase::Fact] Block to set attributes on the fact
33
+ # @return [Factbase::Fact] The existing or newly created fact
34
+ # @note System attributes (_id, _time, _version) are ignored when matching
27
35
  def Fbe.just_one(fb: Fbe.fb)
28
36
  attrs = {}
29
37
  f =
@@ -8,22 +8,50 @@ require 'faraday/logging/formatter'
8
8
  require_relative '../../fbe'
9
9
  require_relative '../../fbe/middleware'
10
10
 
11
- # Faraday logging formatter shows verbose logs for error responses only
11
+ # Custom Faraday formatter that logs only error responses (4xx/5xx).
12
+ #
13
+ # This formatter reduces log noise by only outputting details when HTTP
14
+ # requests fail. For 403 errors with JSON responses, it shows a compact
15
+ # warning with the error message. For other errors, it logs the full
16
+ # request/response details including headers and bodies.
17
+ #
18
+ # @example Usage in Faraday middleware
19
+ # connection = Faraday.new do |f|
20
+ # f.response :logger, nil, formatter: Fbe::Middleware::Formatter
21
+ # end
22
+ #
23
+ # @example Log output for 403 error
24
+ # # GET https://api.github.com/repos/private/repo -> 403 / Repository access denied
25
+ #
26
+ # @example Log output for other errors (500, 404, etc)
27
+ # # GET https://api.example.com/endpoint HTTP/1.1
28
+ # # Content-Type: "application/json"
29
+ # # Authorization: "Bearer [FILTERED]"
30
+ # #
31
+ # # {"query": "data"}
32
+ # # HTTP/1.1 500
33
+ # # Content-Type: "text/html"
34
+ # #
35
+ # # Internal Server Error
12
36
  #
13
37
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
14
38
  # Copyright:: Copyright (c) 2024-2025 Zerocracy
15
39
  # License:: MIT
16
40
  class Fbe::Middleware::Formatter < Faraday::Logging::Formatter
17
- # Log HTTP request.
41
+ # Captures HTTP request details for later use in error logging.
18
42
  #
19
- # @param [Hash] http The hash with data about HTTP request
43
+ # @param [Hash] http Request data including method, url, headers, and body
44
+ # @return [void]
20
45
  def request(http)
21
46
  @req = http
22
47
  end
23
48
 
24
- # Log HTTP response.
49
+ # Logs HTTP response details only for error responses (4xx/5xx).
25
50
  #
26
- # @param [Hash] http The hash with data about HTTP response
51
+ # @param [Hash] http Response data including status, headers, and body
52
+ # @return [void]
53
+ # @note Only logs when status >= 400
54
+ # @note Special handling for 403 JSON responses to show compact error message
27
55
  def response(http)
28
56
  return if http.status < 400
29
57
  if http.status == 403 && http.response_headers['content-type'].start_with?('application/json')
@@ -5,11 +5,24 @@
5
5
 
6
6
  require_relative '../fbe'
7
7
 
8
- # The module.
8
+ # Middleware components for Faraday HTTP client configuration.
9
+ #
10
+ # This module serves as a namespace for various middleware components
11
+ # that enhance Faraday HTTP client functionality with custom behaviors
12
+ # such as request/response formatting, logging, and error handling.
13
+ #
14
+ # The middleware components in this module are designed to work with
15
+ # the Faraday HTTP client library and can be plugged into the Faraday
16
+ # middleware stack to provide additional functionality.
17
+ #
18
+ # @example Using middleware in Faraday client
19
+ # Faraday.new do |conn|
20
+ # conn.use Fbe::Middleware::Formatter
21
+ # conn.adapter Faraday.default_adapter
22
+ # end
9
23
  #
10
24
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
11
25
  # Copyright:: Copyright (c) 2024-2025 Zerocracy
12
26
  # License:: MIT
13
27
  module Fbe::Middleware
14
- # empty
15
28
  end
data/lib/fbe/octo.rb CHANGED
@@ -128,7 +128,21 @@ end
128
128
  # Fake GitHub client for testing purposes.
129
129
  #
130
130
  # This class provides mock implementations of Octokit methods for testing.
131
- # It returns predictable data structures that mimic GitHub API responses.
131
+ # It returns predictable, deterministic data structures that mimic GitHub API
132
+ # responses without making actual API calls. The mock data uses consistent
133
+ # patterns:
134
+ # - IDs are generated from string names using character code sums
135
+ # - Timestamps are random but within recent past
136
+ # - Repository and user data follows GitHub's JSON structure
137
+ #
138
+ # @example Using FakeOctokit in tests
139
+ # client = Fbe::FakeOctokit.new
140
+ # repo = client.repository('octocat/hello-world')
141
+ # puts repo[:full_name] # => "octocat/hello-world"
142
+ # puts repo[:id] # => 1224 (deterministic from name)
143
+ #
144
+ # @note All methods return static or pseudo-random data
145
+ # @note No actual API calls are made
132
146
  class Fbe::FakeOctokit
133
147
  # Generates a random time in the past.
134
148
  #
@@ -167,6 +181,13 @@ class Fbe::FakeOctokit
167
181
  o
168
182
  end
169
183
 
184
+ # Lists repositories for a user or organization.
185
+ #
186
+ # @param [String] _user The user/org name (ignored in mock)
187
+ # @return [Array<Hash>] Array of repository hashes
188
+ # @example
189
+ # client.repositories('octocat')
190
+ # # => [{:id=>123, :full_name=>"yegor256/judges", ...}, ...]
170
191
  def repositories(_user = nil)
171
192
  [
172
193
  repository('yegor256/judges'),
@@ -428,6 +449,14 @@ class Fbe::FakeOctokit
428
449
  }
429
450
  end
430
451
 
452
+ # Lists releases for a repository.
453
+ #
454
+ # @param [String] _repo Repository name (ignored in mock)
455
+ # @param [Hash] _opts Options hash (ignored in mock)
456
+ # @return [Array<Hash>] Array of release hashes
457
+ # @example
458
+ # client.releases('octocat/Hello-World')
459
+ # # => [{:tag_name=>"0.19.0", :name=>"just a fake name", ...}, ...]
431
460
  def releases(_repo, _opts = {})
432
461
  [
433
462
  release('https://github...'),
@@ -435,6 +464,13 @@ class Fbe::FakeOctokit
435
464
  ]
436
465
  end
437
466
 
467
+ # Gets a single release.
468
+ #
469
+ # @param [String] _url Release URL (ignored in mock)
470
+ # @return [Hash] Release information
471
+ # @example
472
+ # client.release('https://api.github.com/repos/octocat/Hello-World/releases/1')
473
+ # # => {:tag_name=>"0.19.0", :name=>"just a fake name", ...}
438
474
  def release(_url)
439
475
  {
440
476
  node_id: 'RE_kwDOL6GCO84J7Cen',
@@ -449,6 +485,14 @@ class Fbe::FakeOctokit
449
485
  }
450
486
  end
451
487
 
488
+ # Gets repository information.
489
+ #
490
+ # @param [String, Integer] name Repository name ('owner/repo') or ID
491
+ # @return [Hash] Repository information
492
+ # @raise [Octokit::NotFound] If name is 404123 or 404124 (for testing)
493
+ # @example
494
+ # client.repository('octocat/Hello-World')
495
+ # # => {:id=>1296269, :full_name=>"octocat/Hello-World", ...}
452
496
  def repository(name)
453
497
  raise Octokit::NotFound if [404_123, 404_124].include?(name)
454
498
  {
@@ -488,12 +532,28 @@ class Fbe::FakeOctokit
488
532
  }
489
533
  end
490
534
 
535
+ # Lists pull requests associated with a commit.
536
+ #
537
+ # @param [String] repo Repository name ('owner/repo')
538
+ # @param [String] _sha Commit SHA (ignored in mock)
539
+ # @return [Array<Hash>] Array of pull request hashes
540
+ # @example
541
+ # client.commit_pulls('octocat/Hello-World', 'abc123')
542
+ # # => [{:number=>42, :state=>"open", ...}]
491
543
  def commit_pulls(repo, _sha)
492
544
  [
493
545
  pull_request(repo, 42)
494
546
  ]
495
547
  end
496
548
 
549
+ # Lists issues for a repository.
550
+ #
551
+ # @param [String] repo Repository name ('owner/repo')
552
+ # @param [Hash] _options Query options (ignored in mock)
553
+ # @return [Array<Hash>] Array of issue hashes
554
+ # @example
555
+ # client.list_issues('octocat/Hello-World', state: 'open')
556
+ # # => [{:number=>42, :title=>"Found a bug", ...}, ...]
497
557
  def list_issues(repo, _options = {})
498
558
  [
499
559
  issue(repo, 42),
@@ -501,6 +561,14 @@ class Fbe::FakeOctokit
501
561
  ]
502
562
  end
503
563
 
564
+ # Gets a single issue.
565
+ #
566
+ # @param [String] repo Repository name ('owner/repo')
567
+ # @param [Integer] number Issue number
568
+ # @return [Hash] Issue information
569
+ # @example
570
+ # client.issue('octocat/Hello-World', 42)
571
+ # # => {:id=>42, :number=>42, :created_at=>...}
504
572
  def issue(repo, number)
505
573
  {
506
574
  id: 42,
@@ -515,6 +583,14 @@ class Fbe::FakeOctokit
515
583
  }
516
584
  end
517
585
 
586
+ # Gets a single pull request.
587
+ #
588
+ # @param [String] repo Repository name ('owner/repo')
589
+ # @param [Integer] number Pull request number
590
+ # @return [Hash] Pull request information
591
+ # @example
592
+ # client.pull_request('octocat/Hello-World', 1)
593
+ # # => {:id=>42, :number=>1, :additions=>12, ...}
518
594
  def pull_request(repo, number)
519
595
  {
520
596
  id: 42,
@@ -529,6 +605,14 @@ class Fbe::FakeOctokit
529
605
  }
530
606
  end
531
607
 
608
+ # Lists pull requests for a repository.
609
+ #
610
+ # @param [String] _repo Repository name (ignored in mock)
611
+ # @param [Hash] _options Query options (ignored in mock)
612
+ # @return [Array<Hash>] Array of pull request hashes
613
+ # @example
614
+ # client.pull_requests('octocat/Hello-World', state: 'open')
615
+ # # => [{:number=>100, :state=>"closed", :title=>"#90: some title", ...}]
532
616
  def pull_requests(_repo, _options = {})
533
617
  [
534
618
  {
@@ -1045,6 +1129,45 @@ class Fbe::FakeOctokit
1045
1129
  ]
1046
1130
  end
1047
1131
 
1132
+ def issue_events(_repo, _number)
1133
+ [
1134
+ {
1135
+ id: 126, actor: { login: 'user', id: 411, type: 'User' },
1136
+ event: 'labeled', created_at: Time.parse('2025-05-30 14:41:00 UTC'),
1137
+ label: { name: 'bug', color: 'd73a4a' }
1138
+ },
1139
+ {
1140
+ id: 206, actor: { login: 'user', id: 411, type: 'User' },
1141
+ event: 'mentioned', created_at: Time.parse('2025-05-30 14:41:10 UTC')
1142
+ },
1143
+ {
1144
+ id: 339, actor: { login: 'user2', id: 422, type: 'User' },
1145
+ event: 'subscribed', created_at: Time.parse('2025-05-30 14:41:10 UTC')
1146
+ },
1147
+ {
1148
+ id: 490, actor: { login: 'github-actions[bot]', id: 41_898_282, type: 'Bot' },
1149
+ event: 'renamed', created_at: Time.parse('2025-05-30 14:41:30 UTC'),
1150
+ rename: { from: 'some title', to: 'some title 2' }
1151
+ },
1152
+ {
1153
+ id: 505, actor: { login: 'user', id: 411, type: 'User' },
1154
+ event: 'subscribed', created_at: Time.parse('2025-05-30 16:18:24 UTC')
1155
+ },
1156
+ {
1157
+ id: 608, actor: { login: 'user2', id: 422, type: 'User', test: 123 },
1158
+ event: 'assigned', created_at: Time.parse('2025-05-30 17:59:08 UTC'),
1159
+ assignee: { login: 'user2', id: 422, type: 'User' },
1160
+ assigner: { login: 'user', id: 411, type: 'User' }
1161
+ },
1162
+ {
1163
+ id: 776, actor: { login: 'user2', id: 422, type: 'User' },
1164
+ event: 'referenced', commit_id: '4621af032170f43d',
1165
+ commit_url: 'https://api.github.com/repos/foo/foo/commits/4621af032170f43d',
1166
+ created_at: Time.parse('2025-05-30 19:57:50 UTC')
1167
+ }
1168
+ ]
1169
+ end
1170
+
1048
1171
  def pull_request_comments(_name, _number)
1049
1172
  [
1050
1173
  {
data/lib/fbe/overwrite.rb CHANGED
@@ -6,19 +6,27 @@
6
6
  require_relative '../fbe'
7
7
  require_relative 'fb'
8
8
 
9
- # Overwrites a property in the fact.
9
+ # Overwrites a property in the fact by recreating the entire fact.
10
10
  #
11
11
  # If the property doesn't exist in the fact, it will be added. If it does
12
- # exist, it will be re-set (the entire fact will be destroyed, a new fact
13
- # created, and the property set with the new value).
12
+ # exist, the entire fact will be destroyed, a new fact created with all
13
+ # existing properties, and the specified property set with the new value.
14
14
  #
15
15
  # It is important that the fact has the +_id+ property. If it doesn't,
16
16
  # an exception will be raised.
17
17
  #
18
- # @param [Factbase::Fact] fact The fact to modify
18
+ # @param [Factbase::Fact] fact The fact to modify (must have _id property)
19
19
  # @param [String] property The name of the property to set
20
- # @param [Any] value The value to set
21
- # @return [Factbase::Fact] Returns new fact or previous one
20
+ # @param [Any] value The value to set (can be any type)
21
+ # @param [Factbase] fb The factbase to use (defaults to Fbe.fb)
22
+ # @return [Factbase::Fact] Returns new fact if recreated, or original if unchanged
23
+ # @raise [RuntimeError] If fact is nil, has no _id, or property is not a String
24
+ # @note This operation preserves all other properties during recreation
25
+ # @note If property already has the same single value, no changes are made
26
+ # @example Update a user's status
27
+ # user = fb.query('(eq login "john")').first
28
+ # updated_user = Fbe.overwrite(user, 'status', 'active')
29
+ # # All properties preserved, only 'status' is set to 'active'
22
30
  def Fbe.overwrite(fact, property, value, fb: Fbe.fb)
23
31
  raise 'The fact is nil' if fact.nil?
24
32
  raise 'The fb is nil' if fb.nil?
data/lib/fbe/regularly.rb CHANGED
@@ -6,15 +6,28 @@
6
6
  require_relative '../fbe'
7
7
  require_relative 'fb'
8
8
 
9
- # Run the block provided every X days.
9
+ # Run the block provided every X days based on PMP configuration.
10
+ #
11
+ # Executes a block periodically based on PMP (Project Management Plan) settings.
12
+ # The block will only run if it hasn't been executed within the specified interval.
13
+ # Creates a fact recording when the judge was last run.
10
14
  #
11
15
  # @param [String] area The name of the PMP area
12
- # @param [Integer] p_every_days How frequently to run, every X days
13
- # @param [Integer] p_since_days Since when to collect stats, X days
14
- # @param [Factbase] fb The factbase
15
- # @param [String] judge The name of the judge, from the +judges+ tool
16
- # @param [Loog] loog The logging facility
16
+ # @param [String] p_every_days PMP property name for interval (defaults to 7 days if not in PMP)
17
+ # @param [String] p_since_days PMP property name for since period (defaults to 28 days if not in PMP)
18
+ # @param [Factbase] fb The factbase (defaults to Fbe.fb)
19
+ # @param [String] judge The name of the judge (uses $judge global)
20
+ # @param [Loog] loog The logging facility (uses $loog global)
21
+ # @yield [Factbase::Fact] Fact to populate with judge execution details
17
22
  # @return [nil] Nothing
23
+ # @raise [RuntimeError] If required parameters or globals are nil
24
+ # @note Skips execution if judge was run within the interval period
25
+ # @note The 'since' property is added to the fact when p_since_days is provided
26
+ # @example Run a cleanup task every 3 days
27
+ # Fbe.regularly('cleanup', 'days_between_cleanups', 'cleanup_history_days') do |f|
28
+ # f.total_cleaned = cleanup_old_records
29
+ # # PMP might have: days_between_cleanups=3, cleanup_history_days=30
30
+ # end
18
31
  def Fbe.regularly(area, p_every_days, p_since_days = nil, fb: Fbe.fb, judge: $judge, loog: $loog, &)
19
32
  raise 'The area is nil' if area.nil?
20
33
  raise 'The p_every_days is nil' if p_every_days.nil?
@@ -7,14 +7,29 @@ require_relative '../fbe'
7
7
  require_relative 'fb'
8
8
  require_relative 'overwrite'
9
9
 
10
- # Run the block provided every X hours.
10
+ # Run the block provided every X hours based on PMP configuration.
11
+ #
12
+ # Similar to Fbe.regularly but works with hour intervals instead of days.
13
+ # Executes a block periodically, maintaining a single fact that tracks the
14
+ # last execution time. The fact is overwritten on each run rather than
15
+ # creating new facts.
11
16
  #
12
17
  # @param [String] area The name of the PMP area
13
- # @param [Integer] p_every_hours How frequently to run, every X hours
14
- # @param [Factbase] fb The factbase
15
- # @param [String] judge The name of the judge, from the +judges+ tool
16
- # @param [Loog] loog The logging facility
18
+ # @param [String] p_every_hours PMP property name for interval (defaults to 24 hours if not in PMP)
19
+ # @param [Factbase] fb The factbase (defaults to Fbe.fb)
20
+ # @param [String] judge The name of the judge (uses $judge global)
21
+ # @param [Loog] loog The logging facility (uses $loog global)
22
+ # @yield [Factbase::Fact] The judge fact to populate with execution details
17
23
  # @return [nil] Nothing
24
+ # @raise [RuntimeError] If required parameters or globals are nil
25
+ # @note Skips execution if judge was run within the interval period
26
+ # @note Overwrites the 'when' property of existing judge fact
27
+ # @example Run a monitoring task every 6 hours
28
+ # Fbe.repeatedly('monitoring', 'hours_between_checks') do |f|
29
+ # f.servers_checked = check_all_servers
30
+ # f.issues_found = count_issues
31
+ # # PMP might have: hours_between_checks=6
32
+ # end
18
33
  def Fbe.repeatedly(area, p_every_hours, fb: Fbe.fb, judge: $judge, loog: $loog, &)
19
34
  raise 'The area is nil' if area.nil?
20
35
  raise 'The p_every_hours is nil' if p_every_hours.nil?
data/lib/fbe/sec.rb CHANGED
@@ -6,15 +6,22 @@
6
6
  require 'tago'
7
7
  require_relative '../fbe'
8
8
 
9
- # Converts number of seconds into text.
9
+ # Converts number of seconds into human-readable time format.
10
10
  #
11
11
  # The number of seconds is taken from the +fact+ provided, usually stored
12
- # there in the +seconds+ property. The seconds are formatted to hours,
13
- # days, or weeks.
12
+ # there in the +seconds+ property. The seconds are formatted into a
13
+ # human-readable string like "3 days ago" or "5 hours ago" using the
14
+ # tago gem.
14
15
  #
15
- # @param [Factbase::Fact] fact The fact, where to get the number of seconds
16
- # @param [String] prop The property in the fact, with the seconds
17
- # @return [String] Time interval as a text
16
+ # @param [Factbase::Fact] fact The fact containing the seconds property
17
+ # @param [String, Symbol] prop The property name with seconds (defaults to :seconds)
18
+ # @return [String] Human-readable time interval (e.g., "2 weeks ago", "3 hours ago")
19
+ # @raise [RuntimeError] If the specified property doesn't exist in the fact
20
+ # @note Uses the tago gem's ago method for formatting
21
+ # @example Format elapsed time from a fact
22
+ # build_fact = fb.query('(eq type "build")').first
23
+ # build_fact.duration = 7200 # 2 hours in seconds
24
+ # puts Fbe.sec(build_fact, :duration) # => "2 hours ago"
18
25
  def Fbe.sec(fact, prop = :seconds)
19
26
  s = fact[prop.to_s]
20
27
  raise "There is no #{prop.inspect} property" if s.nil?
@@ -6,31 +6,52 @@
6
6
  require_relative '../fbe'
7
7
  require_relative 'octo'
8
8
 
9
- # Converts mask to repository name.
9
+ # Converts a repository mask pattern to a regular expression.
10
10
  #
11
- # This function takes something like +"zerocracy/*"+ as an input and returns
12
- # a regular expression that may match repositories defined by this mask, which
13
- # is +/zerocracy\/.*+ in this particular case.
11
+ # @example Basic wildcard matching
12
+ # Fbe.mask_to_regex('zerocracy/*')
13
+ # # => /zerocracy\/.*/i
14
14
  #
15
- # @param [String] mask The mask
16
- # @return [Regex] Regular expression
15
+ # @example Specific repository (no wildcard)
16
+ # Fbe.mask_to_regex('zerocracy/fbe')
17
+ # # => /zerocracy\/fbe/i
18
+ #
19
+ # @param [String] mask Repository mask in format 'org/repo' where repo can contain '*'
20
+ # @return [Regexp] Case-insensitive regular expression for matching repositories
21
+ # @raise [RuntimeError] If organization part contains asterisk
17
22
  def Fbe.mask_to_regex(mask)
18
23
  org, repo = mask.split('/')
19
24
  raise "Org '#{org}' can't have an asterisk" if org.include?('*')
20
25
  Regexp.compile("#{org}/#{repo.gsub('*', '.*')}", Regexp::IGNORECASE)
21
26
  end
22
27
 
23
- # Builds a list of repositories required by the +repositories+ option.
28
+ # Resolves repository masks to actual GitHub repository names.
29
+ #
30
+ # Takes a comma-separated list of repository masks from options and expands
31
+ # wildcards by querying GitHub API. Supports inclusion and exclusion patterns.
32
+ # Archived repositories are automatically filtered out.
33
+ #
34
+ # @example Basic usage with wildcards
35
+ # # options.repositories = "zerocracy/fbe,zerocracy/ab*"
36
+ # repos = Fbe.unmask_repos
37
+ # # => ["zerocracy/fbe", "zerocracy/abc", "zerocracy/abcd"]
38
+ #
39
+ # @example Using exclusion patterns
40
+ # # options.repositories = "zerocracy/*,-zerocracy/private*"
41
+ # repos = Fbe.unmask_repos
42
+ # # Returns all zerocracy repos except those starting with 'private'
24
43
  #
25
- # The +repositories+ option defined in the +$options+ must contain something
26
- # like "zerocracy/fbe,zerocracy/ab*" (a comma-separated list of masks). This
27
- # function will go to the GitHub API and fetch all available repositories
28
- # matching these masks.
44
+ # @example Empty result handling
45
+ # # options.repositories = "nonexistent/*"
46
+ # Fbe.unmask_repos # Raises error: "No repos found matching: nonexistent/*"
29
47
  #
30
- # @param [Judges::Options] options The options coming from the +judges+ tool
31
- # @param [Hash] global The hash for global caching
32
- # @param [Loog] loog The logging facility
33
- # @return [Array<String>] List of repository full names
48
+ # @param [Judges::Options] options Options containing 'repositories' field with masks
49
+ # @param [Hash] global Global cache for storing API responses
50
+ # @param [Loog] loog Logger for debug output
51
+ # @return [Array<String>] Shuffled list of repository full names (e.g., 'org/repo')
52
+ # @raise [RuntimeError] If no repositories match the provided masks
53
+ # @note Exclusion patterns must start with '-' (e.g., '-org/pattern*')
54
+ # @note Results are shuffled to distribute load when processing
34
55
  def Fbe.unmask_repos(options: $options, global: $global, loog: $loog)
35
56
  repos = []
36
57
  octo = Fbe.octo(loog:, global:, options:)
data/lib/fbe/who.rb CHANGED
@@ -6,19 +6,26 @@
6
6
  require_relative '../fbe'
7
7
  require_relative 'octo'
8
8
 
9
- # Converts an ID of GitHub user into a nicely formatted string with their name.
9
+ # Converts a GitHub user ID into a formatted username string.
10
10
  #
11
11
  # The ID of the user (integer) is expected to be stored in the +who+ property of the
12
- # provided +fact+. This function makes a live request to GitHub API in order
13
- # to find out what is the name of the user. For example, the ID +526301+
14
- # will be converted to the +"@yegor256"+ string.
12
+ # provided +fact+. This function makes a live request to GitHub API to
13
+ # retrieve the username. The result is cached globally to minimize API calls.
14
+ # For example, the ID +526301+ will be converted to +"@yegor256"+.
15
15
  #
16
- # @param [Factbase::Fact] fact The fact, where to get the ID of GitHub user
17
- # @param [String] prop The property in the fact, with the ID
18
- # @param [Judges::Options] options The options coming from the +judges+ tool
19
- # @param [Hash] global The hash for global caching
20
- # @param [Loog] loog The logging facility
21
- # @return [String] Full name of the user
16
+ # @param [Factbase::Fact] fact The fact containing the GitHub user ID
17
+ # @param [String, Symbol] prop The property name with the ID (defaults to :who)
18
+ # @param [Judges::Options] options The options from judges tool (uses $options global)
19
+ # @param [Hash] global The hash for global caching (uses $global)
20
+ # @param [Loog] loog The logging facility (uses $loog global)
21
+ # @return [String] Formatted username with @ prefix (e.g., "@yegor256")
22
+ # @raise [RuntimeError] If the specified property doesn't exist in the fact
23
+ # @note Results are cached to reduce GitHub API calls
24
+ # @note Subject to GitHub API rate limits
25
+ # @example Convert user ID to username
26
+ # contributor = fb.query('(eq type "contributor")').first
27
+ # contributor.author_id = 526301
28
+ # puts Fbe.who(contributor, :author_id) # => "@yegor256"
22
29
  def Fbe.who(fact, prop = :who, options: $options, global: $global, loog: $loog)
23
30
  id = fact[prop.to_s]
24
31
  raise "There is no #{prop.inspect} property" if id.nil?
data/lib/fbe.rb CHANGED
@@ -10,5 +10,5 @@
10
10
  # License:: MIT
11
11
  module Fbe
12
12
  # Current version of the gem (changed by +.rultor.yml+ on every release)
13
- VERSION = '0.14.1' unless const_defined?(:VERSION)
13
+ VERSION = '0.15.0' unless const_defined?(:VERSION)
14
14
  end
@@ -274,4 +274,23 @@ class TestOcto < Fbe::Test
274
274
  assert_raises(Octokit::NotFound) { o.repository(404_123) }
275
275
  assert_raises(Octokit::NotFound) { o.repository(404_124) }
276
276
  end
277
+
278
+ def test_fetches_fake_issue_events_has_assigned_event
279
+ o = Fbe.octo(loog: Loog::NULL, global: {}, options: Judges::Options.new({ 'testing' => true }))
280
+ result = o.issue_events('foo/foo', 123)
281
+ assert_instance_of(Array, result)
282
+ assert_equal(7, result.size)
283
+ event = result.find { _1[:event] == 'assigned' }
284
+ assert_equal(608, event[:id])
285
+ assert_pattern do
286
+ event => {
287
+ id: Integer,
288
+ actor: { login: 'user2', id: 422, type: 'User' },
289
+ event: 'assigned',
290
+ created_at: Time,
291
+ assignee: { login: 'user2', id: 422, type: 'User' },
292
+ assigner: { login: 'user', id: 411, type: 'User' }
293
+ }
294
+ end
295
+ end
277
296
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fbe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.1
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko