Module: FlossFunding

Defined in:
lib/floss_funding/wedge.rb,
lib/floss_funding/poke.rb,
lib/floss_funding/config.rb,
lib/floss_funding/silent.rb,
lib/floss_funding/library.rb,
lib/floss_funding/version.rb,
lib/floss_funding/lockfile.rb,
lib/floss_funding/constants.rb,
lib/floss_funding/inclusion.rb,
lib/floss_funding/namespace.rb,
lib/floss_funding/under_bar.rb,
lib/floss_funding/file_finder.rb,
lib/floss_funding/fingerprint.rb,
lib/floss_funding/config_finder.rb,
lib/floss_funding/config_loader.rb,
lib/floss_funding/configuration.rb,
lib/floss_funding/final_summary.rb,
lib/floss_funding/activation_event.rb,
lib/floss_funding/contra_indications.rb,
lib/floss_funding/rakelib/gem_spec_reader.rb,
lib/floss_funding.rb

Overview

Now declare some constants

Defined Under Namespace

Modules: Config, Constants, FileFinder, Fingerprint, Lockfile, Poke, Rakelib, Silent, UnderBar, Version Classes: ActivationEvent, AtExitLockfile, ConfigFinder, ConfigLoader, ConfigNotFoundError, Configuration, ContraIndications, Error, FinalSummary, Inclusion, Library, LockfileBase, Namespace, OnLoadLockfile, Wedge

Constant Summary collapse

DEBUG =

Debug toggle controlled by ENV; set true when ENV[‘FLOSS_FUNDING_DEBUG’] case-insensitively equals “true”.

ENV.fetch("FLOSS_FUNDING_DEBUG", "").casecmp("true") == 0
CONFIG_FILE_NAME =

The file name to look for in the project root.

Returns:

  • (String)
".floss_funding.yml"
FLOSS_FUNDING_HOME =
File.realpath(File.join(File.dirname(__FILE__), ".."))
REQUIRED_YAML_KEYS =

Minimum required keys for a valid .floss_funding.yml file
Used to validate presence when integrating without :wedge mode

%w[library_name funding_uri].freeze
FREE_AS_IN_BEER =

Unpaid activation option intended for open-source and not-for-profit use.

Returns:

  • (String)
"Free-as-in-beer"
BUSINESS_IS_NOT_GOOD_YET =

Unpaid activation option acknowledging commercial use without payment.

Returns:

  • (String)
"Business-is-not-good-yet"
NOT_FINANCIALLY_SUPPORTING =

Activation option to explicitly opt out of funding prompts for a namespace.

Returns:

  • (String)
"Not-financially-supporting"
STATES =
{
  :activated => "activated",
  :unactivated => "unactivated",
  :invalid => "invalid",
}.freeze
STATE_VALUES =
STATES.values.freeze
DEFAULT_STATE =

The default state is unknown / unactivated until proven otherwise.

STATES[:unactivated]
START_MONTH =

First month index against which base words are validated.
Do not change once released as it would invalidate existing activation keys.

Returns:

  • (Integer)
Month.new(2025, 7).to_i
BASE_WORDS_PATH =

Absolute path to the base words list used for paid activation validation.

Returns:

  • (String)
File.expand_path("../../base.txt", __FILE__)
EIGHT_BYTES =

Number of hex characters required for a paid activation key (64 = 256 bits).

Returns:

  • (Integer)
64
HEX_LICENSE_RULE =

Format for a paid activation key (64 hex chars).

Returns:

  • (Regexp)
/\A[0-9a-fA-F]{#{EIGHT_BYTES}}\z/
<<-FOOTER
=====================================================================================
- Please buy FLOSS "peace-of-mind" activation keys to support open source developers.
floss_funding v#{::FlossFunding::Version::VERSION} is made with ❤️ in 🇺🇸 & 🇮🇩 by Galtzo FLOSS (galtzo.com)
FOOTER

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.loaded_atTime (readonly)

Read the deterministic time source

Returns:

  • (Time)

See Also:

  • @loaded_at


204
205
206
# File 'lib/floss_funding.rb', line 204

def loaded_at
  @loaded_at
end

Class Method Details

.add_or_update_namespace_with_event(namespace, event) ⇒ Object



345
346
347
348
349
350
351
352
353
354
355
356
357
358
# File 'lib/floss_funding.rb', line 345

def add_or_update_namespace_with_event(namespace, event)
  @mutex.synchronize do
    ns_obj = namespace
    # Append in place to reduce allocations and avoid extra @mutex churn
    ns_obj.activation_events << event
    @namespaces[namespace.name] = ns_obj
    begin
      lib_name = (event.library ? event.library.library_name : nil)
      ::FlossFunding.debug_log { "[registry] add_or_update ns=#{namespace.name.inspect} events=#{ns_obj.activation_events.size} state=#{event.state} lib=#{lib_name.inspect}" }
    rescue StandardError
      # ignore log errors
    end
  end
end

.all_namespacesArray<::FlossFunding::Namespace>

All namespaces that have any activation events recorded
Returns array of Namespace objects

Returns:



363
364
365
# File 'lib/floss_funding.rb', line 363

def all_namespaces
  @mutex.synchronize { @namespaces.values.flatten.dup }
end

.base_words(num_valid_words = nil) ⇒ Array<String>

Reads the first N lines from the base words file to validate paid activation keys.

Reads base words used to validate paid activation keys.
When called without an argument, uses the current month window to
determine how many words are valid.

Parameters:

  • num_valid_words (Integer, nil) (defaults to: nil)

    number of words to read from the word list

Returns:

  • (Array<String>)

    the first N words; empty when N is nil or zero



402
403
404
405
406
407
# File 'lib/floss_funding.rb', line 402

def base_words(num_valid_words = nil)
  n = num_valid_words.nil? ? @num_valid_words_for_month : num_valid_words
  return [] if n.nil? || n.zero?

  @base_words_all.slice(0, n)
end

.check_activation(plain_text) ⇒ Boolean

Check whether a plaintext activation base word is currently valid

Parameters:

  • plain_text (String)

Returns:

  • (Boolean)


412
413
414
415
416
417
418
419
420
421
422
423
424
425
# File 'lib/floss_funding.rb', line 412

def check_activation(plain_text)
  return false if @num_valid_words_for_month.nil? || @num_valid_words_for_month <= 0
  # Cache a Set for fast membership per current n
  sets = @base_words_set_cache
  set = sets[@num_valid_words_for_month]
  unless set
    words = base_words(@num_valid_words_for_month)
    # Warning inside a cache-protected lookup means it should only happen once per process, at most.
    warn("[FlossFunding] ZOMG! Base words missing. Did you time travel? Is it #{@loaded_month}? Is system clock set in the past?") if words.empty?
    set = Set.new(words)
    sets[@num_valid_words_for_month] = set
  end
  set.include?(plain_text)
end

.configuration(namespace) ⇒ Object



383
384
385
# File 'lib/floss_funding.rb', line 383

def configuration(namespace)
  configurations(namespace)
end

.configurations(namespace = nil) ⇒ Object

Configuration storage and helpers (derived from namespaces and activation events)
When namespace is nil, returns a Hash mapping namespace String => Array<FlossFunding::Configuration>.
When namespace is provided, returns FlossFunding::Configuration for that namespace (or nil if not found).



370
371
372
373
374
375
376
377
378
379
380
381
# File 'lib/floss_funding.rb', line 370

def configurations(namespace = nil)
  @mutex.synchronize do
    if namespace
      nobj = @namespaces[namespace]
      nobj ? nobj.merged_config : nil
    else
      @namespaces.each_with_object({}) do |(ns, nobj), acc|
        acc[ns] = nobj.configs
      end
    end
  end
end

.debug_log(*args) ⇒ void

This method returns an undefined value.

Debug logging helper. Only outputs when FlossFunding::DEBUG is true.
Accepts either a message (or multiple args joined by space) or a block
for lazy construction of the message.

Parameters:

  • args (Array<Object>)

    message parts to join with space

Yield Returns:

  • (String)

    optional block returning the message



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/floss_funding.rb', line 223

def debug_log(*args)
  return unless ::FlossFunding::DEBUG
  msg = if block_given?
    yield
  else
    args.map(&:to_s).join(" ")
  end
  # Prefer Logger to file when configured and available; otherwise STDOUT
  logger = debug_logger
  if logger
    begin
      logger.debug(msg.to_s)
      return
    rescue StandardError
      # fall back to STDOUT below
    end
  end
  puts(msg)
rescue StandardError
  # Never fail the caller due to logging issues
  nil
end

.debug_loggerObject

Lazily build a Logger instance when FLOSS_CFG_FUNDING_LOGFILE is set and ‘logger’ is available.
Returns a Logger or nil when unavailable or initialization failed.



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/floss_funding.rb', line 271

def debug_logger
  path = begin
    ENV["FLOSS_CFG_FUNDING_LOGFILE"]
  rescue StandardError
    nil
  end
  return if path.nil? || path.to_s.strip.empty?

  begin
    require "logger"
  rescue LoadError
    return
  rescue StandardError => e
    # Log but do not set inert for logger init failures
    debug_log { "[WARN][debug_logger] #{e.class}: #{e.message}" }
    return
  end

  @mutex.synchronize do
    return @debug_logger if defined?(@debug_logger) && @debug_logger

    # Ensure directory exists; best-effort
    begin
      dir = File.dirname(path)
      FileUtils.mkdir_p(dir) unless dir.nil? || dir.empty? || Dir.exist?(dir)
    rescue StandardError
      # ignore; Logger.new may still succeed if dir already exists or is current dir
    end

    begin
      # Truncate the debug log file on first initialization to keep runs readable
      begin
        File.open(path, "w") { |f| f.truncate(0) }
      rescue StandardError => e
        debug_log { "[WARN][debug_logger] unable to truncate #{path}: #{e.class}: #{e.message}" }
      end

      logger = Logger.new(path)
      logger.level = Logger::DEBUG
      # Keep output minimal: message only with newline
      logger.formatter = proc { |_severity, _datetime, _progname, message| (message.to_s.end_with?("\n") ? message.to_s : message.to_s + "\n") }
      @debug_logger = logger
    rescue StandardError
      @debug_logger = nil
    end

    @debug_logger
  end
end

.env_var_namesObject

ENV var name mapping helpers (derived from namespaces)
Returns a Hash mapping namespace String => ENV variable name String



389
390
391
392
393
# File 'lib/floss_funding.rb', line 389

def env_var_names
  @mutex.synchronize do
    @namespaces.transform_values { |nobj| nobj.env_var_name }
  end
end

.error!(error, where = nil) ⇒ Object

Mark an internal error, log useful context for diagnostics, and set inert flag.

Parameters:

  • error (Exception)
  • where (String, nil) (defaults to: nil)

    context label



254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/floss_funding.rb', line 254

def error!(error, where = nil)
  begin
    lbl = where ? "[ERROR][#{where}]" : "[ERROR]"
    msg = "#{lbl} #{error.class}: #{error.message}"
    debug_log { msg }
    bt = (error.backtrace || [])[0, 5].join("\n")
    debug_log { "#{lbl} backtrace:\n#{bt}" } unless bt.empty?
  rescue StandardError
    # ignore logging failures
  ensure
    @mutex.synchronize { @errored = true }
  end
  true
end

.errored?Boolean

Global error flag; when set true, library should become inert.

Returns:

  • (Boolean)


247
248
249
# File 'lib/floss_funding.rb', line 247

def errored?
  @mutex.synchronize { !!@errored }
end

.initiate_begging(event) ⇒ Object



466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
# File 'lib/floss_funding.rb', line 466

def initiate_begging(event)
  library = event.library
  ns = library.namespace
  env_var_name = ::FlossFunding::UnderBar.env_variable_name(ns)
  library_name = library.library_name
  activation_key = event.activation_key

  # On-load nag sentinel: allow each library to nag at most once per lockfile lifetime
  lock = ::FlossFunding::Lockfile.on_load

  case event.state
  when ::FlossFunding::STATES[:activated]
    nil
  when ::FlossFunding::STATES[:invalid]
    unless lock && lock.nagged?(library)
      lock.record_nag(library, event, "on_load") if lock
      ::FlossFunding.start_coughing(activation_key, ns, env_var_name)
    end
  else
    unless lock && lock.nagged?(library)
      lock.record_nag(library, event, "on_load") if lock
      ::FlossFunding.start_begging(ns, env_var_name, library_name)
    end
  end
end

.install_tasksObject

Tasks for both development and test environments



213
214
215
# File 'lib/floss_funding.rb', line 213

def install_tasks
  load("floss_funding/tasks.rb")
end

.namespacesHash{String => ::FlossFunding::Namespace}

Accessor for namespaces hash: keys are namespace strings, values are Namespace objects

Returns:



323
324
325
# File 'lib/floss_funding.rb', line 323

def namespaces
  @mutex.synchronize { @namespaces.dup }
end

.namespaces=(value) ⇒ Object

Replace the namespaces hash (expects Hash{String => ::FlossFunding::Namespace})



328
329
330
# File 'lib/floss_funding.rb', line 328

def namespaces=(value)
  @mutex.synchronize { @namespaces = value }
end

.project_rootString?

Expose the discovered project root (may be nil when running inside this gem’s own repo)

Returns:

  • (String, nil)


208
209
210
# File 'lib/floss_funding.rb', line 208

def project_root
  ::FlossFunding::ConfigFinder.project_root
end

.register_wedge(base, custom_namespace = nil) ⇒ Object

Register a minimal activation event for wedge-injected libraries to ensure
they are counted in the final summary without performing config discovery.

Parameters:

  • base (Module)

    the including module

  • custom_namespace (String, nil) (defaults to: nil)

    optional override namespace



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/floss_funding.rb', line 150

def register_wedge(base, custom_namespace = nil)
  # Derive namespace string
  ns_name = (custom_namespace.is_a?(String) && custom_namespace.strip != "") ? custom_namespace : base.name.to_s

  # Build Namespace (derives activation key/state from ENV)
  namespace = ::FlossFunding::Namespace.new(ns_name, base)

  # Derive a library (gem) name from the namespace: underscore segments and downcase
  # Example: "FlossFunding" => "floss_funding"; "My::Lib" => "my_lib"
  derived_lib_name = ns_name.split("::").map { |seg| ::FlossFunding::UnderBar.to_under_bar(seg) }.join("__").downcase

  # Minimal configuration: include required keys so downstream consumers have something sensible
  cfg_hash = {
    "library_name" => ["wedge_#{derived_lib_name}"],
    "funding_uri" => ["https://floss-funding.dev"],
  }
  config = ::FlossFunding::Configuration.new(cfg_hash)

  # Minimal Library record; many fields are nil or placeholders in wedge mode
  library = ::FlossFunding::Library.new(
    derived_lib_name,        # library_name
    namespace,               # ns
    custom_namespace,        # custom_ns
    base.name.to_s,          # base_name
    nil,                     # including_path
    nil,                     # root_path
    nil,                     # config_path
    namespace.env_var_name,  # env_var_name
    config,                  # configuration
    nil,                     # silent
  )

  # Event with the derived state and key
  event = ::FlossFunding::ActivationEvent.new(
    library,
    namespace.activation_key,
    namespace.state,
    nil,
  )

  add_or_update_namespace_with_event(namespace, event)
  initiate_begging(event)

  event
rescue StandardError => e
  # Never raise; wedge registration is best-effort only
  ::FlossFunding.error!(e, "register_wedge")
  nil
end

.silencedBoolean

Global silenced flag accessor (boolean)

Returns:

  • (Boolean)


334
335
336
# File 'lib/floss_funding.rb', line 334

def silenced
  @mutex.synchronize { @silenced }
end

.silenced=(value) ⇒ void

This method returns an undefined value.

Set the global silenced flag

Parameters:

  • value (Boolean)


341
342
343
# File 'lib/floss_funding.rb', line 341

def silenced=(value)
  @mutex.synchronize { @silenced = !!value }
end

.start_begging(namespace, env_var_name, library_name) ⇒ void

This method returns an undefined value.

Emit the standard friendly funding message for unactivated usage

Parameters:

  • namespace (String)
  • env_var_name (String)
  • library_name (String)


461
462
463
464
# File 'lib/floss_funding.rb', line 461

def start_begging(namespace, env_var_name, library_name)
  return if ::FlossFunding::ContraIndications.at_exit_contraindicated?
  puts %(FLOSS Funding: Activation key missing for #{library_name} (#{namespace}). Set ENV["#{env_var_name}"] to your activation key; details will be shown at exit.)
end

.start_coughing(activation_key, namespace, env_var_name) ⇒ void

This method returns an undefined value.

Emit a diagnostic message when an activation key is invalid

Parameters:

  • activation_key (String)
  • namespace (String)
  • env_var_name (String)


432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
# File 'lib/floss_funding.rb', line 432

def start_coughing(activation_key, namespace, env_var_name)
  return if ::FlossFunding::ContraIndications.at_exit_contraindicated?
  puts <<-COUGHING
==============================================================
COUGH, COUGH.
Ahem, it appears as though you tried to set an activation key
for #{namespace}, but it was invalid.

  Current (Invalid) Activation Key: #{activation_key}
  Namespace: #{namespace}
  ENV Variable: #{env_var_name}

Paid activation keys are 8 bytes, 64 hex characters, long.
Unpaid activation keys have varying lengths, depending on type and namespace.
Yours is #{activation_key.length} characters long, and doesn't match any paid or unpaid keys.

Please unset the current ENV variable #{env_var_name}, since it is invalid.

Then find the correct one, or get a new one @ https://floss-funding.dev and set it.

#{FlossFunding::FOOTER}
  COUGHING
end