Module: Fbe
- Defined in:
- lib/fbe.rb
Overview
The main and only module of this gem.
- Author
-
Yegor Bugayenko (yegor256@gmail.com)
- Copyright
-
Copyright © 2024-2025 Zerocracy
- License
-
MIT
Defined Under Namespace
Modules: Middleware Classes: Award, Conclude, FakeOctokit, Graph, Iterate
Constant Summary collapse
- VERSION =
Current version of the gem (changed by
.rultor.yml
on every release) '0.2.0'
Class Method Summary collapse
-
.bylaws(anger: 2, love: 2, paranoia: 2) ⇒ Hash<String, String>
Generates policies/bylaws.
-
.conclude(fb: Fbe.fb, judge: $judge, loog: $loog, options: $options, global: $global) {|Factbase::Fact| ... } ⇒ Object
Creates an instance of Conclude and evals it with the block provided.
-
.copy(source, target, except: []) ⇒ Integer
Makes a copy of a fact, moving all properties to a new fact.
-
.enter(badge, why, options: $options, loog: $loog) ⇒ String
Enter a new valve.
-
.fb(fb: $fb, global: $global, options: $options, loog: $loog) ⇒ Factbase
Returns an instance of
Factbase
(cached). -
.github_graph(options: $options, global: $global, loog: $loog) ⇒ Fbe::Graph
Creates an instance of Graph.
-
.if_absent(fb: Fbe.fb) {|Factbase::Fact| ... } ⇒ nil|Factbase::Fact
Injects a fact if it’s absent in the factbase, otherwise (it is already there) returns NIL.
-
.issue(fact, options: $options, global: $global, loog: $loog) ⇒ String
Converts an ID of GitHub issue into a nicely formatting string.
-
.iterate(fb: Fbe.fb, loog: $loog, options: $options, global: $global) {|Factbase::Fact| ... } ⇒ Object
Creates an instance of Iterate and evals it with the block provided.
-
.just_one(fb: Fbe.fb) {|Factbase::Fact| ... } ⇒ Factbase::Fact
Injects a fact if it’s absent in the factbase, otherwise (it is already there) returns the existing one.
-
.mask_to_regex(mask) ⇒ Regex
Converts mask to repository name.
-
.octo(options: $options, global: $global, loog: $loog) ⇒ Hash
Makes a call to the GitHub API.
-
.overwrite(fact, property, value, fb: Fbe.fb) ⇒ Factbase::Fact
Overwrites a property in the fact.
-
.pmp(fb: Fbe.fb, global: $global, options: $options, loog: $loog) ⇒ String|Integer
Takes configuration parameter from the “PMP” fact.
-
.regularly(area, p_every_days, p_since_days = nil, fb: Fbe.fb, judge: $judge, loog: $loog) {|f| ... } ⇒ nil
Run the block provided every X days.
-
.repeatedly(area, p_every_hours, fb: Fbe.fb, judge: $judge, loog: $loog) {|fb.query("(and (eq what '#{judge}'))").each.to_a.first| ... } ⇒ nil
Run the block provided every X hours.
-
.sec(fact, prop = :seconds) ⇒ String
Converts number of seconds into text.
-
.unmask_repos(options: $options, global: $global, loog: $loog) ⇒ Array<String>
Builds a list of repositories required by the
repositories
option. -
.who(fact, prop = :who, options: $options, global: $global, loog: $loog) ⇒ String
Converts an ID of GitHub user into a nicely formatting string with his name.
Class Method Details
.bylaws(anger: 2, love: 2, paranoia: 2) ⇒ Hash<String, String>
Generates policies/bylaws.
Using the templates stored in the assets/bylaws
directory, this function creates a hash, where keys are names and values are formulas of bylaws.
18 19 20 21 22 23 24 25 26 27 28 29 30 |
# File 'lib/fbe/bylaws.rb', line 18 def Fbe.bylaws(anger: 2, love: 2, paranoia: 2) raise "The 'anger' must be in the [0..4] interval: #{anger.inspect}" unless !anger.negative? && anger < 5 raise "The 'love' must be in the [0..4] interval: #{love.inspect}" unless !love.negative? && love < 5 raise "The 'paranoia' must be in the [1..4] interval: #{paranoia.inspect}" unless paranoia.positive? && paranoia < 5 home = File.join(__dir__, '../../assets/bylaws') raise "The directory with templates is absent #{home.inspect}" unless File.exist?(home) Dir[File.join(home, '*.liquid')].to_h do |f| formula = Liquid::Template.parse(File.read(f)).render( 'anger' => anger, 'love' => love, 'paranoia' => paranoia ) [File.basename(f).gsub(/\.liquid$/, ''), formula] end end |
.conclude(fb: Fbe.fb, judge: $judge, loog: $loog, options: $options, global: $global) {|Factbase::Fact| ... } ⇒ Object
Creates an instance of Conclude and evals it with the block provided.
19 20 21 22 |
# File 'lib/fbe/conclude.rb', line 19 def Fbe.conclude(fb: Fbe.fb, judge: $judge, loog: $loog, options: $options, global: $global, &) c = Fbe::Conclude.new(fb:, judge:, loog:, options:, global:) c.instance_eval(&) end |
.copy(source, target, except: []) ⇒ Integer
Makes a copy of a fact, moving all properties to a new fact.
All properties from the source
will be copied to the target
, except those listed in the except
.
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# File 'lib/fbe/copy.rb', line 18 def Fbe.copy(source, target, except: []) raise 'The source is nil' if source.nil? raise 'The target is nil' if target.nil? raise 'The except is nil' if except.nil? copied = 0 source.all_properties.each do |k| next unless target[k].nil? next if except.include?(k) source[k].each do |v| target.send(:"#{k}=", v) copied += 1 end end copied end |
.enter(badge, why, options: $options, loog: $loog) ⇒ String
Enter a new valve.
16 17 18 19 20 |
# File 'lib/fbe/enter.rb', line 16 def Fbe.enter(badge, why, options: $options, loog: $loog, &) return yield unless .testing.nil? baza = BazaRb.new('api.zerocracy.com', 443, .zerocracy_token, loog:) baza.enter(.job_name, badge, why, .job_id.to_i, &) end |
.fb(fb: $fb, global: $global, options: $options, loog: $loog) ⇒ Factbase
Returns an instance of Factbase
(cached).
Instead of using $fb directly, it is recommended to use this utility method. It will not only return the global factbase, but will also make sure it’s properly decorated and cached.
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# File 'lib/fbe/fb.rb', line 25 def Fbe.fb(fb: $fb, global: $global, options: $options, loog: $loog) global[:fb] ||= begin rules = Dir.glob(File.join('rules', '*.fe')).map { |f| File.read(f) } fbe = Factbase::Rules.new( fb, "(and \n#{rules.join("\n")}\n)", uid: '_id' ) fbe = Factbase::Pre.new(fbe) do |f, fbt| max = fbt.query('(eq _id (max _id))').each.to_a.first f._id = (max.nil? ? 0 : max._id) + 1 f._time = Time.now f._version = "#{Factbase::VERSION}/#{Judges::VERSION}/#{.action_version}" f._job = .job_id unless .job_id.nil? end Factbase::Logged.new(fbe, loog) end end |
.github_graph(options: $options, global: $global, loog: $loog) ⇒ Fbe::Graph
Creates an instance of Graph.
16 17 18 19 20 21 22 23 24 |
# File 'lib/fbe/github_graph.rb', line 16 def Fbe.github_graph(options: $options, global: $global, loog: $loog) global[:github_graph] ||= if .testing.nil? Fbe::Graph.new(token: .github_token || ENV.fetch('GITHUB_TOKEN', nil)) else loog.debug('The connection to GitHub GraphQL API is mocked') Fbe::Graph::Fake.new end end |
.if_absent(fb: Fbe.fb) {|Factbase::Fact| ... } ⇒ nil|Factbase::Fact
Injects a fact if it’s absent in the factbase, otherwise (it is already there) returns NIL.
Here is what you do when you want to add a fact to the factbase, but don’t want to make a duplicate of an existing one:
require 'fbe/if_absent'
n =
Fbe.if_absent do |f|
f.what = 'something'
f.details = 'important'
end
return if n.nil?
n.when = Time.now
This code will definitely create one fact with what
equals to something
and details
equals to important
, while the when
will be equal to the time of its first creation.
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/fbe/if_absent.rb', line 33 def Fbe.if_absent(fb: Fbe.fb) attrs = {} f = others(map: attrs) do |*args| k = args[0] if k.end_with?('=') @map[k[0..-2].to_sym] = args[1] else @map[k.to_sym] end end yield f q = attrs.except('_id', '_time', '_version').map do |k, v| vv = v.to_s if v.is_a?(String) vv = "'#{vv.gsub('"', '\\\\"').gsub("'", "\\\\'")}'" elsif v.is_a?(Time) vv = v.utc.iso8601 end "(eq #{k} #{vv})" end.join(' ') q = "(and #{q})" before = fb.query(q).each.to_a.first return nil if before n = fb.insert attrs.each { |k, v| n.send(:"#{k}=", v) } n end |
.issue(fact, options: $options, global: $global, loog: $loog) ⇒ String
Converts an ID of GitHub issue into a nicely formatting string.
The function takes the repository
property of the provided fact
, goes to the GitHub API in order to find the full name of the repository, and then creates a string with the full name of repository + issue, for example “zerocracy/fbe#42”.
21 22 23 24 25 26 27 28 29 |
# File 'lib/fbe/issue.rb', line 21 def Fbe.issue(fact, options: $options, global: $global, loog: $loog) rid = fact['repository'] raise "There is no 'repository' property" if rid.nil? rid = rid.first.to_i issue = fact['issue'] raise "There is no 'issue' property" if issue.nil? issue = issue.first.to_i "#{Fbe.octo(global:, options:, loog:).repo_name_by_id(rid)}##{issue}" end |
.iterate(fb: Fbe.fb, loog: $loog, options: $options, global: $global) {|Factbase::Fact| ... } ⇒ Object
Creates an instance of Iterate and evals it with the block provided.
20 21 22 23 |
# File 'lib/fbe/iterate.rb', line 20 def Fbe.iterate(fb: Fbe.fb, loog: $loog, options: $options, global: $global, &) c = Fbe::Iterate.new(fb:, loog:, options:, global:) c.instance_eval(&) end |
.just_one(fb: Fbe.fb) {|Factbase::Fact| ... } ⇒ Factbase::Fact
Injects a fact if it’s absent in the factbase, otherwise (it is already there) returns the existing one.
require 'fbe/just_one'
n =
Fbe.just_one do |f|
f.what = 'something'
f.details = 'important'
end
This code will guarantee that only one fact with what
equals to something
and details
equals to important
may exist.
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/fbe/just_one.rb', line 27 def Fbe.just_one(fb: Fbe.fb) attrs = {} f = others(map: attrs) do |*args| k = args[0] if k.end_with?('=') @map[k[0..-2].to_sym] = args[1] else @map[k.to_sym] end end yield f q = attrs.except('_id', '_time', '_version').map do |k, v| vv = v.to_s if v.is_a?(String) vv = "'#{vv.gsub('"', '\\\\"').gsub("'", "\\\\'")}'" elsif v.is_a?(Time) vv = v.utc.iso8601 end "(eq #{k} #{vv})" end.join(' ') q = "(and #{q})" before = fb.query(q).each.to_a.first return before unless before.nil? n = fb.insert attrs.each { |k, v| n.send(:"#{k}=", v) } n end |
.mask_to_regex(mask) ⇒ Regex
Converts mask to repository name.
This function takes something like “zerocracy/*” as an input and returns a regular expression that may match repositories defined by this mask, which is /zerocracy/.* in this particular case.
17 18 19 20 21 |
# File 'lib/fbe/unmask_repos.rb', line 17 def Fbe.mask_to_regex(mask) org, repo = mask.split('/') raise "Org '#{org}' can't have an asterisk" if org.include?('*') Regexp.compile("#{org}/#{repo.gsub('*', '.*')}") end |
.octo(options: $options, global: $global, loog: $loog) ⇒ Hash
Makes a call to the GitHub API.
It is supposed to be used instead of Octokit::Client
, because it is pre-configured and enables additional features, such as retrying, logging, and caching.
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/fbe/octo.rb', line 28 def Fbe.octo(options: $options, global: $global, loog: $loog) raise 'The $global is not set' if global.nil? global[:octo] ||= begin if .testing.nil? o = Octokit::Client.new token = .github_token if token.nil? loog.debug("The 'github_token' option is not provided") token = ENV.fetch('GITHUB_TOKEN', nil) if token.nil? loog.debug("The 'GITHUB_TOKEN' environment variable is not set") else loog.debug("The 'GITHUB_TOKEN' environment was provided") end else loog.debug("The 'github_token' option was provided") end if token.nil? loog.warn('Accessing GitHub API without a token!') elsif token.empty? loog.warn('The GitHub API token is an empty string, won\'t use it') else o = Octokit::Client.new(access_token: token) loog.info("Accessing GitHub API with a token (#{token.length} chars, ending by #{token[-4..].inspect})") end o.auto_paginate = true o.per_page = 100 o. = { request: { open_timeout: 15, timeout: 15 } } stack = Faraday::RackBuilder.new do |builder| builder.use( Faraday::Retry::Middleware, exceptions: Faraday::Retry::Middleware::DEFAULT_EXCEPTIONS + [ Octokit::TooManyRequests, Octokit::ServiceUnavailable ], max: 4, interval: ENV['RACK_ENV'] == 'test' ? 0.01 : 4, methods: [:get], backoff_factor: 2 ) builder.use(Fbe::Middleware::Quota, loog:, pause: .github_api_pause || 60) builder.use(Faraday::HttpCache, serializer: Marshal, shared_cache: false, logger: Loog::NULL) builder.use(Octokit::Response::RaiseError) builder.use(Faraday::Response::Logger, loog, formatter: Fbe::Middleware::Formatter) builder.adapter(Faraday.default_adapter) end o.middleware = stack o = Verbose.new(o, log: loog) else loog.debug('The connection to GitHub API is mocked') o = Fbe::FakeOctokit.new end decoor(o, loog:) do def off_quota left = @origin.rate_limit.remaining if left < 5 @loog.info("Too much GitHub API quota consumed already (remaining=#{left}), stopping") true else false end end def user_name_by_id(id) json = @origin.user(id) name = json[:login] @loog.debug("GitHub user ##{id} has a name: @#{name}") name end def repo_id_by_name(name) json = @origin.repository(name) id = json[:id] @loog.debug("GitHub repository #{name.inspect} has an ID: ##{id}") id end def repo_name_by_id(id) json = @origin.repository(id) name = json[:full_name] @loog.debug("GitHub repository ##{id} has a name: #{name}") name end end end end |
.overwrite(fact, property, value, fb: Fbe.fb) ⇒ Factbase::Fact
Overwrites a property in the fact.
If the property doesn’t exist in the fact, it will be added. If it does exist, it will be re-set (the entire fact will be destroyed, new fact created, and property set with the new value).
It is important that the fact has the _id
property. If it doesn’t, an exception will be raised.
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/fbe/overwrite.rb', line 22 def Fbe.overwrite(fact, property, value, fb: Fbe.fb) raise 'The fact is nil' if fact.nil? raise "The property is not a String but #{property.class} (#{property})" unless property.is_a?(String) return fact if !fact[property].nil? && fact[property].size == 1 && fact[property].first == value before = {} fact.all_properties.each do |prop| before[prop.to_s] = fact[prop] end id = fact['_id']&.first raise 'There is no _id in the fact, cannot use Fbe.overwrite' if id.nil? raise "No facts by _id = #{id}" if fb.query("(eq _id #{id})").delete!.zero? n = fb.insert before[property.to_s] = [value] before.each do |k, vv| next unless n[k].nil? vv.each do |v| n.send(:"#{k}=", v) end end n end |
.pmp(fb: Fbe.fb, global: $global, options: $options, loog: $loog) ⇒ String|Integer
Takes configuration parameter from the “PMP” fact.
The factbase may have a few facts with the what
set to pmp
(stands for “project management plan”). These facts contain information that configure the project. It is expected that every fact with the what
set to pmp
also contains the area
property, which is set to one of nine values: scope
, time
, cost
, etc. (by nine process areas in the PMBOK).
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
# File 'lib/fbe/pmp.rb', line 23 def Fbe.pmp(fb: Fbe.fb, global: $global, options: $options, loog: $loog) others do |*args1| area = args1.first unless %w[cost scope hr time procurement risk integration quality communication].include?(area.to_s) raise "Invalid area #{area.inspect} (not part of PMBOK)" end others do |*args2| param = args2.first f = Fbe.fb(global:, fb:, options:, loog:).query("(and (eq what 'pmp') (eq area '#{area}'))").each.to_a.first raise "Unknown area #{area.inspect}" if f.nil? r = f[param] raise "Unknown property #{param.inspect} in the #{area.inspect} area" if r.nil? r.first end end end |
.regularly(area, p_every_days, p_since_days = nil, fb: Fbe.fb, judge: $judge, loog: $loog) {|f| ... } ⇒ nil
Run the block provided every X days.
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
# File 'lib/fbe/regularly.rb', line 18 def Fbe.regularly(area, p_every_days, p_since_days = nil, fb: Fbe.fb, judge: $judge, loog: $loog, &) pmp = fb.query("(and (eq what 'pmp') (eq area '#{area}') (exists #{p_every_days}))").each.to_a.first interval = pmp.nil? ? 7 : pmp[p_every_days].first unless fb.query( "(and (eq what '#{judge}') (gt when (minus (to_time (env 'TODAY' '#{Time.now.utc.iso8601}')) '#{interval} days')))" ).each.to_a.empty? loog.debug("#{$judge} statistics have recently been collected, skipping now") return end f = fb.insert f.what = judge f.when = Time.now unless p_since_days.nil? days = pmp.nil? ? 28 : pmp[p_since_days].first since = Time.now - (days * 24 * 60 * 60) f.since = since end yield f nil end |
.repeatedly(area, p_every_hours, fb: Fbe.fb, judge: $judge, loog: $loog) {|fb.query("(and (eq what '#{judge}'))").each.to_a.first| ... } ⇒ nil
Run the block provided every X hours.
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/fbe/repeatedly.rb', line 18 def Fbe.repeatedly(area, p_every_hours, fb: Fbe.fb, judge: $judge, loog: $loog, &) pmp = fb.query("(and (eq what 'pmp') (eq area '#{area}') (exists #{p_every_hours}))").each.to_a.first hours = pmp.nil? ? 24 : pmp[p_every_hours].first unless fb.query( "(and (eq what '#{judge}') (gt when (minus (to_time (env 'TODAY' '#{Time.now.utc.iso8601}')) '#{hours} hours')))" ).each.to_a.empty? loog.debug("#{$judge} have recently been executed, skipping now") return end f = fb.query("(and (eq what '#{judge}'))").each.to_a.first if f.nil? f = fb.insert f.what = judge end Fbe.overwrite(f, 'when', Time.now) yield fb.query("(and (eq what '#{judge}'))").each.to_a.first nil end |
.sec(fact, prop = :seconds) ⇒ String
Converts number of seconds into text.
THe number of seconds is taken from the fact
provided, usually stored there in the seconds
property. The seconds are formatted to hours, days, or weeks.
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# File 'lib/fbe/sec.rb', line 17 def Fbe.sec(fact, prop = :seconds) s = fact[prop.to_s] raise "There is no #{prop.inspect} property" if s.nil? s = s.first.to_i if s < 60 format('%d seconds', s) elsif s < 60 * 60 format('%d minutes', s / 60) elsif s < 60 * 60 * 24 format('%d hours', s / (60 * 60)) elsif s < 7 * 60 * 60 * 24 format('%d days', s / (60 * 60 * 24)) else format('%d weeks', s / (7 * 60 * 60 * 24)) end end |
.unmask_repos(options: $options, global: $global, loog: $loog) ⇒ Array<String>
Builds a list of repositories required by the repositories
option.
The repositories
option defined in the $options must contain something like “zerocracy/fbe,zerocracy/ab*” (comma-separated list of masks). This function will go to the GitHub API and fetch all available repositories by these masks.
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# File 'lib/fbe/unmask_repos.rb', line 34 def Fbe.unmask_repos(options: $options, global: $global, loog: $loog) repos = [] octo = Fbe.octo(loog:, global:, options:) masks = (.repositories || '').split(',') masks.reject { |m| m.start_with?('-') }.each do |mask| unless mask.include?('*') repos << mask next end re = Fbe.mask_to_regex(mask) octo.repositories(mask.split('/')[0]).each do |r| repos << r[:full_name] if re.match?(r[:full_name]) end end masks.select { |m| m.start_with?('-') }.each do |mask| re = Fbe.mask_to_regex(mask[1..]) repos.reject! { |r| re.match?(r) } end repos.reject! { |repo| octo.repository(repo)[:archived] } raise "No repos found matching: #{.repositories}" if repos.empty? repos.shuffle! loog.debug("Scanning #{repos.size} repositories: #{repos.join(', ')}...") repos end |
.who(fact, prop = :who, options: $options, global: $global, loog: $loog) ⇒ String
Converts an ID of GitHub user into a nicely formatting string with his name.
The ID of the user (integer) is expected to be stored in the who
property of the provided fact
. This function makes a live request to GitHub API in order to find out what is the name of the user. For example, the ID 526301
will be converted to the “@yegor256” string.
22 23 24 25 26 27 |
# File 'lib/fbe/who.rb', line 22 def Fbe.who(fact, prop = :who, options: $options, global: $global, loog: $loog) id = fact[prop.to_s] raise "There is no #{prop.inspect} property" if id.nil? id = id.first.to_i "@#{Fbe.octo(options:, global:, loog:).user_name_by_id(id)}" end |