From c18de992fe6a1dbbcf07cd466cf0f35d028bcb58 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 28 Aug 2026 17:03:03 +1000 Subject: [PATCH 01/23] squash-commit --- .github/workflows/ruby.yml | 3 +-- xqsr3.gemspec | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 8aaa99c..a641a80 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -7,7 +7,7 @@ # # Created: 29th August 2025 -# Updated: 19th August 2026 +# Updated: 28th August 2026 # name: Ruby @@ -18,7 +18,6 @@ on: - master - dev - boilerplate - - bp-3 - idiomatic - rc1 - rc2 diff --git a/xqsr3.gemspec b/xqsr3.gemspec index 2936431..93b29e6 100644 --- a/xqsr3.gemspec +++ b/xqsr3.gemspec @@ -13,6 +13,7 @@ $:.unshift File.join(File.dirname(__FILE__), 'lib') require 'xqsr3/version' + PROJECT_URL = 'https://github.com/synesissoftware/xqsr3' From b08f3e5a4990f1ac7957ace1ce001525ba7064f2 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 28 Aug 2026 17:33:03 +1000 Subject: [PATCH 02/23] chore(doc): update documentation metadata for 0.39.11 --- CHANGES.md | 11 ++ NEWS.md | 1 + lib/xqsr3/containers/frequency_map.rb | 102 +++++++++--------- lib/xqsr3/diagnostics/exception_utilities.rb | 11 +- .../diagnostics/exceptions/with_cause.rb | 26 ++--- lib/xqsr3/quality/parameter_checking.rb | 25 ++--- lib/xqsr3/version.rb | 4 +- 7 files changed, 98 insertions(+), 82 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 96d14dc..b7c6021 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,17 @@ # xqsr3 - Changes +## 0.39.11 - 29th August 2026 + +* Documented construct summaries in **FrequencyMap**, **ExceptionUtilities**, + **WithCause**, and **ParameterChecking** now terminate with periods and + comply with the 76-column rule; +* Refreshed `Updated:` fields and copyright date ranges in modified library + sources; +* Bumped the library version to 0.39.11 and recorded the release in + **NEWS.md**; + + ## 0.39.10 - 28th August 2026 * **Gemfile.lock** is no longer tracked in the repository, completing the **0.39.9** lockfile work (`lockfile false` in **Gemfile**, **.gitignore** entry, and `spec.files` exclusion in **xqsr3.gemspec**); diff --git a/NEWS.md b/NEWS.md index b70173a..01e4379 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,7 @@ | Date | News Item | | ------------------- | ------------------------------------------------------------------------------------ | +| 29th August 2026 | [**xqsr3** 0.39.11](https://github.com/synesissoftware/xqsr3/releases/tag/0.39.11) | | 28th August 2026 | [**xqsr3** 0.39.10](https://github.com/synesissoftware/xqsr3/releases/tag/0.39.10) | | 21st August 2026 | [**xqsr3** 0.39.9](https://github.com/synesissoftware/xqsr3/releases/tag/0.39.9) | | 20th August 2026 | [**xqsr3** 0.39.8](https://github.com/synesissoftware/xqsr3/releases/tag/0.39.8) | diff --git a/lib/xqsr3/containers/frequency_map.rb b/lib/xqsr3/containers/frequency_map.rb index daee228..28075d8 100644 --- a/lib/xqsr3/containers/frequency_map.rb +++ b/lib/xqsr3/containers/frequency_map.rb @@ -5,7 +5,7 @@ # Purpose: FrequencyMap container # # Created: 28th January 2005 -# Updated: 19th August 2026 +# Updated: 28th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # @@ -55,13 +55,13 @@ module Xqsr3 module Containers # Hash-like class that counts, as the map's values, the frequencies of - # elements, as the map's keys + # elements, as the map's keys. class FrequencyMap include Enumerable include ::Xqsr3::Diagnostics::InspectBuilder - # Class that provides a Hash[]-like syntax as follows: + # Class that provides a Hash[]-like syntax as follows. # # fm = FrequencyMap::ByElement[ 'abc', 'def', 'abc', :x, 'x', :y ] # @@ -79,7 +79,7 @@ class FrequencyMap # fm[:z] # => 0 ByElement = Class.new do - # Create an instance of Xqsr3::FrequencyMap from an array + # Create an instance of Xqsr3::FrequencyMap from an array. def self.[] *args fm = FrequencyMap.new @@ -92,7 +92,7 @@ def self.[] *args private_class_method :new end - # Creates an instance from the given arguments + # Creates an instance from the given arguments. def self.[] *args return self.new if 0 == args.length @@ -156,14 +156,14 @@ def self.[] *args end end - # Initialises an instance + # Initialises an instance. def initialize @elements = {} @count = 0 end - # Pushes an element into the map, assigning it an initial count of 1 + # Pushes an element into the map, assigning it an initial count of 1. # # === Signature # @@ -174,7 +174,7 @@ def << key push key, 1 end - # Compares the instance for equality against +rhs+ + # Compares the instance for equality against +rhs+. # # === Signature # @@ -203,7 +203,7 @@ def == rhs false end - # Obtains the count for a given key, or +0+ if the key does not exist + # Obtains the count for a given key, or +0+ if the key does not exist. # # === Signature # @@ -214,7 +214,7 @@ def [] key @elements[key] || 0 end - # Assigns a key and a count + # Assigns a key and a count. # # === Signature # @@ -232,32 +232,32 @@ def []= key, count end # Searches the instance comparing each element with +key+, returning the - # count if found, or +nil+ if not + # count if found, or +nil+ if not. def assoc key @elements.assoc key end - # Removes all elements from the instance + # Removes all elements from the instance. def clear @elements.clear @count = 0 end - # The total number of instances recorded + # The total number of instances recorded. def count @count end - # Obtains the default value of the instance, which will always be +nil+ + # Obtains the default value of the instance, which will always be +nil+. def default @elements.default end - # Deletes the element with the given +key+ and its counts + # Deletes the element with the given +key+ and its counts. # # === Signature # @@ -270,7 +270,7 @@ def delete key @count -= key_count if key_count end - # Duplicates the instance + # Duplicates the instance. def dup fm = self.class.new @@ -278,9 +278,9 @@ def dup fm.merge! self end - # Calls _block_ once for each element in the instance, passing the element - # and its frequency as parameters. If no block is provided, an enumerator - # is returned + # Calls _block_ once for each element in the instance, passing the + # element and its frequency as parameters. If no block is provided, an + # enumerator is returned. def each return @elements.each unless block_given? @@ -291,7 +291,7 @@ def each end end - # Enumerates each entry pair - element + frequency - in key order + # Enumerates each entry pair - element + frequency - in key order. # # Note: this method is more expensive than +each+ because an array of # keys must be created and sorted from which enumeration is directed. @@ -308,8 +308,8 @@ def each_by_key end end - # Enumerates each entry pair - element + frequency - in descending - # order of frequency + # Enumerates each entry pair - element + frequency - in descending order + # of frequency. # # Note: this method is expensive, as it must create a new dictionary # and map all entries into it in order to achieve the ordering @@ -326,7 +326,7 @@ def each_by_frequency end # Calls _block_ once for each element in the instance, passing the - # element. If no block is provided, an enumerator is returned + # element. If no block is provided, an enumerator is returned. def each_key return @elements.each_key unless block_given? @@ -340,7 +340,7 @@ def each_key alias each_pair each # Calls _block_ once for each element in the instance, passing the - # count. If no block is provided, an enumerator is returned + # count. If no block is provided, an enumerator is returned. def each_value return @elements.each_value unless block_given? @@ -351,14 +351,14 @@ def each_value end end - # Returns +true+ if instance contains no elements; +false+ otherwise + # Returns +true+ if instance contains no elements; +false+ otherwise. def empty? 0 == size end # Returns +true+ if +rhs+ is an instance of +FrequencyMap+ and contains - # the same elements and their counts; +false+ otherwise + # the same elements and their counts; +false+ otherwise. def eql? rhs case rhs @@ -375,7 +375,7 @@ def eql? rhs # +key+ cannot be found, there are several options: with no other # arguments, it will raise a +KeyError+ exception; if +default+ is # given, then that will be returned; if the optional code block is - # specified, then that will be run and its result returned + # specified, then that will be run and its result returned. def fetch key, default = nil, &block case default @@ -412,21 +412,21 @@ def fetch key, default = nil, &block @elements[key] end - # Returns the equivalent flattened form of the instance + # Returns the equivalent flattened form of the instance. def flatten @elements.flatten end - # Returns +true+ if an element with the given +key+ is in the map; +false+ - # otherwise + # Returns +true+ if an element with the given +key+ is in the map; + # +false+ otherwise. def has_key? key @elements.has_key? key end - # Returns +true+ if an element with a count of the given +value+ is in the - # map; +false+ otherwise + # Returns +true+ if an element with a count of the given +value+ is in + # the map; +false+ otherwise. # # === Signature # @@ -449,7 +449,7 @@ def has_value? value @elements.has_value? value end - # A hash-code for this instance + # A hash-code for this instance. def hash @elements.hash @@ -457,7 +457,7 @@ def hash alias include? has_key? - # A diagnostics string form of the instance + # A diagnostics string form of the instance. def inspect make_inspect show_fields: true @@ -470,7 +470,7 @@ def inspect =begin =end - # Returns the element that has the given count, or +nil+ if none found + # Returns the element that has the given count, or +nil+ if none found. # # === Signature # @@ -488,13 +488,13 @@ def key count alias key? has_key? - # An array of the elements only + # An array of the elements only. def keys @elements.keys end - # The number of elements in the map + # The number of elements in the map. def length @elements.length @@ -502,8 +502,8 @@ def length alias member? has_key? - # Returns a new instance containing a merging of the current instance and - # the +fm+ instance + # Returns a new instance containing a merging of the current instance + # and the +fm+ instance. # # NOTE: where any element is found in both merging instances the count # will be a combination of the two counts @@ -519,7 +519,7 @@ def merge fm fm_new end - # Merges the contents of +fm+ into the current instance + # Merges the contents of +fm+ into the current instance. # # NOTE: where any element is found in both merging instances the count # will be a combination of the two counts @@ -542,7 +542,7 @@ def merge! fm # Pushes the +element+ and +count+. If the +element+ already exists, # +count+ will be added to the existing count; otherwise it will be - # +count+ + # +count+. # # === Signature # @@ -574,8 +574,8 @@ def push key, count = 1 self end - # Removes a key-value pair from the instance and return as a two-item - # array + # Removes a key-value pair from the instance and returns it as a two-item + # array. def shift r = @elements.shift @@ -588,9 +588,9 @@ def shift alias size length # Causes an element with the given +key+ and +count+ to be stored. If an - # element with the given +key+ already exists, its count will be adjusted, - # as will the total count. A +count+ of +0+ removes the key (consistent - # with +#push+ reducing a count to zero). + # element with the given +key+ already exists, its count will be + # adjusted, as will the total count. A +count+ of +0+ removes the key + # (consistent with +#push+ reducing a count to zero). # # === Return # +true+ if the element was inserted; +false+ if the element was @@ -614,31 +614,31 @@ def store key, count old_count == 0 end - # Converts instance to an array of +[key,value]+ pairs + # Converts instance to an array of +[key,value]+ pairs. def to_a @elements.to_a end - # Obtains a +Hash+ copy of the instance contents + # Obtains a +Hash+ copy of the instance contents. def to_h @elements.dup end - # Obtains equivalent hash to instance + # Obtains an equivalent hash to the instance. def to_hash @elements.dup end - # A string-form of the instance + # A string-form of the instance. def to_s @elements.to_s end - # An array of all frequencies (without element keys) in the instance + # An array of all frequencies (without element keys) in the instance. def values @elements.values diff --git a/lib/xqsr3/diagnostics/exception_utilities.rb b/lib/xqsr3/diagnostics/exception_utilities.rb index 36ac205..ce0cf69 100644 --- a/lib/xqsr3/diagnostics/exception_utilities.rb +++ b/lib/xqsr3/diagnostics/exception_utilities.rb @@ -5,13 +5,13 @@ # Purpose: Definition of the ExceptionUtilities module # # Created: 12th February 2015 -# Updated: 19th August 2026 +# Updated: 28th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # # Author: Matthew Wilson # -# Copyright (c) 2019-2024, Matthew Wilson and Synesis Information Systems +# Copyright (c) 2019-2026, Matthew Wilson and Synesis Information Systems # Copyright (c) 2015-2019, Matthew Wilson and Synesis Software # All rights reserved. # @@ -54,7 +54,9 @@ module Diagnostics module ExceptionUtilities # Raises an instance of a named exception that takes options in its - # constructor, as in: + # constructor. + # + # For example: # # class ArgumentErrorWithOptions < ArgumentError # @@ -74,7 +76,8 @@ module ExceptionUtilities # $stderr.puts x.options # => {:opt1=>:val1, :opt2=>"val2"} # end # - # It can also be used with full compatibility with Kernel#raise, as in: + # It can also be used with full compatibility with + # Kernel#raise, as in: # # begin # diff --git a/lib/xqsr3/diagnostics/exceptions/with_cause.rb b/lib/xqsr3/diagnostics/exceptions/with_cause.rb index 1302dc6..7d0d330 100644 --- a/lib/xqsr3/diagnostics/exceptions/with_cause.rb +++ b/lib/xqsr3/diagnostics/exceptions/with_cause.rb @@ -5,13 +5,13 @@ # Purpose: Definition of the WithCause inclusion module # # Created: 16th December 2017 -# Updated: 19th August 2026 +# Updated: 28th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # # Author: Matthew Wilson # -# Copyright (c) 2019-2024, Matthew Wilson and Synesis Information Systems +# Copyright (c) 2019-2026, Matthew Wilson and Synesis Information Systems # Copyright (c) 2017-2019, Matthew Wilson and Synesis Software # All rights reserved. # @@ -54,7 +54,7 @@ module Exceptions # This inclusion module adds to an exception class the means to chain a # cause (aka inner-exception), which is then exposed with the +cause+ - # attribute + # attribute. # # *Examples:* # @@ -64,12 +64,12 @@ module Exceptions # module WithCause - # Array of hidden fields + # Array of hidden fields. INSPECT_HIDDEN_FIELDS = [ 'has_implicit_message', 'uses_cause_message' ] # Defines an initializer for an exception class that allows a cause (aka # an inner exception) to be specified, either as the first or last - # argument or as a +:cause+ option + # argument or as a +:cause+ option. # # === Signature # @@ -133,16 +133,16 @@ def initialize(*args, **options) end # The cause / inner-exception, if any, specified to the instance - # initialiser. This attribute shadows +Exception#cause+ (the interpreter's - # raise-chain cause); for includers, +#cause+ refers to this module's - # +@cause+. + # initialiser. This attribute shadows +Exception#cause+ (the + # interpreter's raise-chain cause); for includers, +#cause+ refers to + # this module's +@cause+. attr_reader :cause # The options passed to the initialiser, with +:cause+ removed, if - # present + # present. attr_reader :options - # Message obtained by concatenation of all chained exceptions' messages + # Message obtained by concatenation of all chained exceptions' messages. # # === Signature # @@ -168,7 +168,7 @@ def chained_message **options "#{m}#{sep}#{cm}" end - # An array of exceptions in the chain, excluding +self+ + # An array of exceptions in the chain, excluding +self+. def chainees return [] unless cause @@ -180,13 +180,13 @@ def chainees r end - # An array of exceptions in the chain, including +self+ + # An array of exceptions in the chain, including +self+. def exceptions [ self ] + chainees end - # A combination of the backtrace(s) of all chained exception(s) + # A combination of the backtrace(s) of all chained exception(s). def chained_backtrace b = backtrace diff --git a/lib/xqsr3/quality/parameter_checking.rb b/lib/xqsr3/quality/parameter_checking.rb index c9943cb..4f50be5 100644 --- a/lib/xqsr3/quality/parameter_checking.rb +++ b/lib/xqsr3/quality/parameter_checking.rb @@ -5,13 +5,13 @@ # Purpose: Definition of the ParameterChecking module # # Created: 12th February 2015 -# Updated: 19th August 2026 +# Updated: 28th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # # Author: Matthew Wilson # -# Copyright (c) 2019-2025, Matthew Wilson and Synesis Information Systems +# Copyright (c) 2019-2026, Matthew Wilson and Synesis Information Systems # Copyright (c) 2016-2019, Matthew Wilson and Synesis Software # All rights reserved. # @@ -51,10 +51,9 @@ module Xqsr3 module Quality - # Inclusion module that creates class and instance methods +check_option()+ - # and +check_parameter()+ that may be used to check option/parameter values - # and types. - # + # Inclusion module that creates class and instance methods + # +check_option()+ and +check_parameter()+ that may be used to check + # option/parameter values and types. module ParameterChecking private @@ -106,7 +105,8 @@ def self.included base # :nodoc: end private - # Check a given parameter (value=+value+, name=+name+) for type and value + # Check a given parameter (value=+value+, name=+name+) for type and + # value. # # === Signature # @@ -140,14 +140,14 @@ def check_parameter value, name, options = {}, &block # @see check_parameter # # @note This is obsolete, and will be removed in a future version. - # Please use +check_parameter+ instead + # Please use +check_parameter+ instead. def check_param value, name, options = {}, &block Util_.check_parameter value, name, options, &block end # Specific form of the +check_parameter()+ that is used to check - # options, taking instead the hash and the key + # options, taking instead the hash and the key. # # === Signature # @@ -163,7 +163,8 @@ def check_option h, name, options = {}, &block end public - # Check a given parameter (value=+value+, name=+name+) for type and value + # Check a given parameter (value=+value+, name=+name+) for type and + # value. # # === Signature # @@ -195,14 +196,14 @@ def self.check_parameter value, name, options = {}, &block # @see check_parameter # # @note This is obsolete, and will be removed in a future version. - # Please use +check_parameter+ instead + # Please use +check_parameter+ instead. def self.check_param value, name, options = {}, &block Util_.check_parameter value, name, options, &block end # Specific form of the +check_parameter()+ that is used to check - # options, taking instead the hash and the key + # options, taking instead the hash and the key. # # === Signature # diff --git a/lib/xqsr3/version.rb b/lib/xqsr3/version.rb index e47d0c0..ecf747b 100644 --- a/lib/xqsr3/version.rb +++ b/lib/xqsr3/version.rb @@ -5,7 +5,7 @@ # Purpose: Version for Xqsr3 library # # Created: 3rd April 2016 -# Updated: 28th August 2026 +# Updated: 29th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # @@ -51,7 +51,7 @@ module Xqsr3 # Current version of the Xqsr3 library - VERSION = '0.39.10' + VERSION = '0.39.11' private VERSION_PARTS_ = VERSION.split(/[.]/).collect { |n| n.to_i } # :nodoc: From 431b69cb0af9f2cb9ae9de5a9352dea1060ba7b1 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 28 Aug 2026 20:31:06 +1000 Subject: [PATCH 03/23] chore(doc): establish component documentation structure * add the initial component catalogue under docs/components/; * reserve docs/guides/ for task-oriented documentation; * link component documentation from README.md; * record the documentation roadmap in TODO.md; --- README.md | 38 ++++++----- TODO.md | 9 +++ docs/components/README.md | 37 +++++++++++ docs/components/array-utilities.md | 35 ++++++++++ docs/components/command-line-utilities.md | 34 ++++++++++ docs/components/containers.md | 41 ++++++++++++ docs/components/conversion.md | 39 +++++++++++ docs/components/diagnostics.md | 47 +++++++++++++ docs/components/extensions.md | 80 +++++++++++++++++++++++ docs/components/hash-utilities.md | 42 ++++++++++++ docs/components/io.md | 32 +++++++++ docs/components/quality.md | 35 ++++++++++ docs/components/string-utilities.md | 38 +++++++++++ docs/guides/README.md | 10 +++ 14 files changed, 500 insertions(+), 17 deletions(-) create mode 100644 docs/components/README.md create mode 100644 docs/components/array-utilities.md create mode 100644 docs/components/command-line-utilities.md create mode 100644 docs/components/containers.md create mode 100644 docs/components/conversion.md create mode 100644 docs/components/diagnostics.md create mode 100644 docs/components/extensions.md create mode 100644 docs/components/hash-utilities.md create mode 100644 docs/components/io.md create mode 100644 docs/components/quality.md create mode 100644 docs/components/string-utilities.md create mode 100644 docs/guides/README.md diff --git a/README.md b/README.md index 1ef977e..57b1016 100644 --- a/README.md +++ b/README.md @@ -69,29 +69,33 @@ which brings in nine extensions. **xqsr3** provides components in the following categories: -* Array Utilities -* Command-line Utilities -* Containers -* Conversion -* Diagnostics -* Hash Utilities -* IO -* Quality -* String Utilities +* [Array Utilities](./docs/components/array-utilities.md); +* [Command-line Utilities](./docs/components/command-line-utilities.md); +* [Containers](./docs/components/containers.md); +* [Conversion](./docs/components/conversion.md); +* [Diagnostics](./docs/components/diagnostics.md); +* [Hash Utilities](./docs/components/hash-utilities.md); +* [IO](./docs/components/io.md); +* [Quality](./docs/components/quality.md); +* [String Utilities](./docs/components/string-utilities.md); * ~~XML Utilities~~ **NOTE**: The **XML** components formerly in **xqsr3** in versions **0.29**-**0.30** are now contained in the separate project [**xqsr3-xml**](https://github.com/synesissoftware/xqsr3-xml/). and extensions to the following standard library components: -* Array extensions -* Enumerable extensions -* Hash extensions -* Integer extensions -* IO extensions -* Kernel extensions -* String extensions -* test/unit extensions +* [Array extensions](./docs/components/extensions.md#array-extensions); +* [Enumerable extensions](./docs/components/extensions.md#enumerable-extensions); +* [Hash extensions](./docs/components/extensions.md#hash-extensions); +* [Integer extensions](./docs/components/extensions.md#integer-extensions); +* [IO extensions](./docs/components/extensions.md#io-extensions); +* [Kernel extensions](./docs/components/extensions.md#kernel-extensions); +* [String extensions](./docs/components/extensions.md#string-extensions); +* [test/unit extensions](./docs/components/extensions.md#testunit-extensions); + +The complete [component catalogue](./docs/components/README.md) provides +loading instructions and initial API summaries. Task-oriented guides will be +developed separately under [docs/guides/](./docs/guides/README.md). ## Examples diff --git a/TODO.md b/TODO.md index 5935d9f..99e0f89 100644 --- a/TODO.md +++ b/TODO.md @@ -7,6 +7,15 @@ * [x] ~~~quiet Ruby 3.4 `test-unit` warnings for blocks passed to `assert_nil` / `assert_not_nil` in **test/unit/quality/tc_parameter_checking.rb**~~~; +## Documentation + +* [ ] Curated component catalogue under **docs/components/**; +* [ ] Task-oriented user guide under **docs/guides/**; +* [ ] Expanded generated API reference; +* [ ] Executable cookbook and recipe documentation; +* [ ] Static documentation website with search and versioned references; + + ## Performance improvements * \ diff --git a/docs/components/README.md b/docs/components/README.md new file mode 100644 index 0000000..a5a2039 --- /dev/null +++ b/docs/components/README.md @@ -0,0 +1,37 @@ +# xqsr3 Component Catalogue + +This catalogue is the user-oriented guide to the components provided by +**xqsr3**. It is organised by component category rather than by Ruby source +file. + + +## Table of Contents + +- [Using the catalogue](#using-the-catalogue) +- [Categories](#categories) + + +## Using the catalogue + +Components are loaded explicitly. Each category page documents the relevant +`require` path, public entry points, and representative usage. + +The catalogue documents the supported public surface. Internal files and +implementation details are intentionally excluded. + + +## Categories + +* [Array Utilities](./array-utilities.md); +* [Command-line Utilities](./command-line-utilities.md); +* [Containers](./containers.md); +* [Conversion](./conversion.md); +* [Diagnostics](./diagnostics.md); +* [Extensions](./extensions.md); +* [Hash Utilities](./hash-utilities.md); +* [IO](./io.md); +* [Quality](./quality.md); +* [String Utilities](./string-utilities.md); + + + diff --git a/docs/components/array-utilities.md b/docs/components/array-utilities.md new file mode 100644 index 0000000..57d2242 --- /dev/null +++ b/docs/components/array-utilities.md @@ -0,0 +1,35 @@ +# xqsr3 Array Utilities + +Array utilities provide standalone operations for working with Ruby arrays. + + +## Table of Contents + +- [Loading](#loading) +- [Components](#components) + + +## Loading + +```Ruby +require 'xqsr3/array_utilities' +``` + +Individual components can also be loaded directly. + + +## Components + +### `ArrayUtilities::JoinWithOr` + +Formats an array as a human-readable list using commas and “or” before the +final item. + +```Ruby +require 'xqsr3/array_utilities/join_with_or' +``` + +The corresponding extension is available as `Array#join_with_or`. + + + diff --git a/docs/components/command-line-utilities.md b/docs/components/command-line-utilities.md new file mode 100644 index 0000000..af1723d --- /dev/null +++ b/docs/components/command-line-utilities.md @@ -0,0 +1,34 @@ +# xqsr3 Command-line Utilities + +Command-line utilities support small, reusable operations for processing +option strings. + + +## Table of Contents + +- [Loading](#loading) +- [Components](#components) + + +## Loading + +```Ruby +require 'xqsr3/command_line_utilities' +``` + + +## Components + +### `CommandLineUtilities::MapOptionString` + +Maps a command-line option string into a structured representation. + +```Ruby +require 'xqsr3/command_line_utilities/map_option_string' +``` + +The same functionality is available through the `String#map_option_string` +extension. + + + diff --git a/docs/components/containers.md b/docs/components/containers.md new file mode 100644 index 0000000..31e1559 --- /dev/null +++ b/docs/components/containers.md @@ -0,0 +1,41 @@ +# xqsr3 Containers + +The container components provide focused collection types for common +counting and key-to-multiple-value use cases. + + +## Table of Contents + +- [Loading](#loading) +- [Components](#components) + + +## Loading + +```Ruby +require 'xqsr3/containers' +``` + +Individual containers can also be loaded directly. + + +## Components + +### `Containers::FrequencyMap` + +Collects and exposes frequencies for observed values. + +```Ruby +require 'xqsr3/containers/frequency_map' +``` + +### `Containers::MultiMap` + +Associates keys with multiple values. + +```Ruby +require 'xqsr3/containers/multi_map' +``` + + + diff --git a/docs/components/conversion.md b/docs/components/conversion.md new file mode 100644 index 0000000..88e60dd --- /dev/null +++ b/docs/components/conversion.md @@ -0,0 +1,39 @@ +# xqsr3 Conversion + +Conversion components parse common scalar values while keeping conversion +rules explicit at the call site. + + +## Table of Contents + +- [Loading](#loading) +- [Components](#components) + + +## Loading + +```Ruby +require 'xqsr3/conversion' +``` + + +## Components + +### `Conversion::BoolParser` + +Parses supported textual representations of boolean values. + +```Ruby +require 'xqsr3/conversion/bool_parser' +``` + +### `Conversion::IntegerParser` + +Parses integer values according to the component's documented rules. + +```Ruby +require 'xqsr3/conversion/integer_parser' +``` + + + diff --git a/docs/components/diagnostics.md b/docs/components/diagnostics.md new file mode 100644 index 0000000..2b27240 --- /dev/null +++ b/docs/components/diagnostics.md @@ -0,0 +1,47 @@ +# xqsr3 Diagnostics + +Diagnostics components provide small utilities for constructing, raising, and +inspecting exceptions. + + +## Table of Contents + +- [Loading](#loading) +- [Components](#components) + + +## Loading + +```Ruby +require 'xqsr3/diagnostics' +``` + + +## Components + +### `Diagnostics::ExceptionUtilities` + +Provides helpers for raising exceptions with additional options. + +```Ruby +require 'xqsr3/diagnostics/exception_utilities' +``` + +### `Diagnostics::InspectBuilder` + +Supports the construction of diagnostic inspection strings. + +```Ruby +require 'xqsr3/diagnostics/inspect_builder' +``` + +### `Diagnostics::Exceptions::WithCause` + +Represents an exception that retains an underlying cause. + +```Ruby +require 'xqsr3/diagnostics/exceptions/with_cause' +``` + + + diff --git a/docs/components/extensions.md b/docs/components/extensions.md new file mode 100644 index 0000000..4558b0b --- /dev/null +++ b/docs/components/extensions.md @@ -0,0 +1,80 @@ +# xqsr3 Ruby Extensions + +Ruby extensions add focused methods to standard-library classes and modules. +They are grouped by the class or module they extend. + + +## Table of Contents + +- [Loading](#loading) +- [Extension groups](#extension-groups) + + +## Loading + +```Ruby +require 'xqsr3/extensions' +``` + +Individual extension groups and methods can be loaded directly. + + +## Extension groups + +* [Array extensions](#array-extensions); +* [Enumerable extensions](#enumerable-extensions); +* [Hash extensions](#hash-extensions); +* [Integer extensions](#integer-extensions); +* [IO extensions](#io-extensions); +* [Kernel extensions](#kernel-extensions); +* [String extensions](#string-extensions); +* [test/unit extensions](#testunit-extensions); + +The group-specific APIs are documented in the generated API reference and +will be expanded here with examples and behavioural details. + + +### Array extensions + +Provides `Array#join_with_or`. + + +### Enumerable extensions + +Provides `Enumerable#collect_with_index`, `Enumerable#detect_map`, and +`Enumerable#unique`. + + +### Hash extensions + +Provides deep transformation, matching, slicing, and exception-style +operations for hashes. + + +### Integer extensions + +Provides `Integer#to_s_grp`. + + +### IO extensions + +Provides `IO#writelines`. + + +### Kernel extensions + +Provides `Kernel::Integer` and `Kernel#raise_with_options`. + + +### String extensions + +Provides string predicates, conversions, formatting, and truncation helpers. + + +### test/unit extensions + +Provides additional assertions for `test-unit`, including type and exception +assertions. + + + diff --git a/docs/components/hash-utilities.md b/docs/components/hash-utilities.md new file mode 100644 index 0000000..e1b81c6 --- /dev/null +++ b/docs/components/hash-utilities.md @@ -0,0 +1,42 @@ +# xqsr3 Hash Utilities + +Hash utilities provide standalone operations for transforming and matching +hashes. + + +## Table of Contents + +- [Loading](#loading) +- [Components](#components) + + +## Loading + +```Ruby +require 'xqsr3/hash_utilities' +``` + + +## Components + +### `HashUtilities::DeepTransform` + +Transforms values recursively through nested hashes and collections. + +```Ruby +require 'xqsr3/hash_utilities/deep_transform' +``` + +### `HashUtilities::KeyMatching` + +Provides matching operations for hash keys. + +```Ruby +require 'xqsr3/hash_utilities/key_matching' +``` + +Related extensions are available as `Hash#deep_transform`, `Hash#has_match?`, +`Hash#match`, `Hash#except`, and `Hash#slice`. + + + diff --git a/docs/components/io.md b/docs/components/io.md new file mode 100644 index 0000000..9b130ad --- /dev/null +++ b/docs/components/io.md @@ -0,0 +1,32 @@ +# xqsr3 IO + +The IO components provide small output helpers for Ruby streams. + + +## Table of Contents + +- [Loading](#loading) +- [Components](#components) + + +## Loading + +```Ruby +require 'xqsr3/io' +``` + + +## Components + +### `IO#writelines` + +Writes a sequence of lines to an IO object. + +```Ruby +require 'xqsr3/io/writelines' +``` + +The same functionality is exposed through the `IO#writelines` extension. + + + diff --git a/docs/components/quality.md b/docs/components/quality.md new file mode 100644 index 0000000..545a567 --- /dev/null +++ b/docs/components/quality.md @@ -0,0 +1,35 @@ +# xqsr3 Quality + +Quality components support explicit validation of method parameters and +preconditions. + + +## Table of Contents + +- [Loading](#loading) +- [Components](#components) + + +## Loading + +```Ruby +require 'xqsr3/quality' +``` + + +## Components + +### `Quality::ParameterChecking` + +Provides reusable checks for validating parameter values, types, and +relationships. + +```Ruby +require 'xqsr3/quality/parameter_checking' +``` + +Use the checks at public method boundaries so invalid arguments fail close to +their source. + + + diff --git a/docs/components/string-utilities.md b/docs/components/string-utilities.md new file mode 100644 index 0000000..4f6f793 --- /dev/null +++ b/docs/components/string-utilities.md @@ -0,0 +1,38 @@ +# xqsr3 String Utilities + +String utilities provide standalone predicates, normalisation, conversion, +quoting, and truncation operations. + + +## Table of Contents + +- [Loading](#loading) +- [Components](#components) + + +## Loading + +```Ruby +require 'xqsr3/string_utilities' +``` + +Individual components can also be loaded directly. + + +## Components + +* `StringUtilities::EndsWith`; +* `StringUtilities::NilIfEmpty`; +* `StringUtilities::NilIfWhitespace`; +* `StringUtilities::QuoteIf`; +* `StringUtilities::StartsWith`; +* `StringUtilities::ToSymbol`; +* `StringUtilities::Truncate`; + +The corresponding standard-library extensions include +`String#ends_with?`, `String#nil_if_empty`, `String#nil_if_whitespace`, +`String#quote_if`, `String#starts_with?`, `String#to_bool`, `String#to_symbol`, +and `String#truncate`. + + + diff --git a/docs/guides/README.md b/docs/guides/README.md new file mode 100644 index 0000000..759db23 --- /dev/null +++ b/docs/guides/README.md @@ -0,0 +1,10 @@ +# xqsr3 User Guides + +This directory is reserved for task-oriented guides to **xqsr3**. Guides will +explain how to combine components to solve common Ruby programming tasks. + +The component catalogue is available in +[`docs/components/`](../components/README.md). + + + From 7815caea73e3dca58a21c137fecf299d42928f52 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 28 Aug 2026 20:36:35 +1000 Subject: [PATCH 04/23] docs(components): document container selection and behaviour * replace the generic Containers summary with substantive usage guidance; * explain when to choose FrequencyMap, MultiMap, or Hash; * document category-level and component-level loading; * describe FrequencyMap counting, construction, mutation, querying, and errors; * describe MultiMap cardinality, construction, mutation, enumeration, and merge semantics; * document absent-key, empty-value, ordering, and conversion behaviour; * link the catalogue to the container unit tests; --- docs/components/containers.md | 116 ++++++++++++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 7 deletions(-) diff --git a/docs/components/containers.md b/docs/components/containers.md index 31e1559..6d55ea6 100644 --- a/docs/components/containers.md +++ b/docs/components/containers.md @@ -3,39 +3,141 @@ The container components provide focused collection types for common counting and key-to-multiple-value use cases. +Use `FrequencyMap` when each key has one numeric count. Use `MultiMap` when +each key has an ordered collection of values. Both preserve the distinction +between the number of keys and the number of associated items. + ## Table of Contents - [Loading](#loading) -- [Components](#components) +- [Choosing a container](#choosing-a-container) +- [FrequencyMap](#frequencymap) +- [MultiMap](#multimap) +- [Shared conventions](#shared-conventions) ## Loading +Load both containers through the category entry point: + ```Ruby require 'xqsr3/containers' ``` -Individual containers can also be loaded directly. +Or load only the component required by the application: + +```Ruby +require 'xqsr3/containers/frequency_map' +require 'xqsr3/containers/multi_map' +``` + +The classes are defined in `Xqsr3::Containers`. -## Components +## Choosing a container -### `Containers::FrequencyMap` +* Choose `FrequencyMap` for histograms, frequency tables, and tallying; +* Choose `MultiMap` for one-to-many relationships and grouped values; +* Choose a regular `Hash` when neither counting nor one-to-many behaviour is + required. -Collects and exposes frequencies for observed values. + +## `FrequencyMap` + +`Xqsr3::Containers::FrequencyMap` maps each element to a numeric count. It +includes `Enumerable`, and its `count` method returns the total number of +observations, while `size` returns the number of distinct elements. ```Ruby require 'xqsr3/containers/frequency_map' + +frequencies = Xqsr3::Containers::FrequencyMap::ByElement[ + 'ruby', 'ruby', 'crystal', 'ruby', +] + +frequencies['ruby'] # => 3 +frequencies['crystal'] # => 1 +frequencies['python'] # => 0 +frequencies.size # => 2 +frequencies.count # => 4 ``` -### `Containers::MultiMap` +`ByElement[...]` is the convenient constructor when starting with a sequence +of observations. `FrequencyMap[...]` also accepts a `Hash`, an array of +`[key, count]` pairs, or an even-length key/count array. + +The primary mutation methods are: + +* `push(key, count = 1)` adds to the existing count and removes the key when + the resulting count is zero; +* `store(key, count)` replaces the existing count and removes the key when + `count` is zero; +* `<< key` records one observation; +* `delete(key)` removes the key and its contribution to the total; +* `merge` and `merge!` combine counts for duplicate keys. + +Counts must be integers. A push that would make an individual count negative +raises `RangeError`; invalid count values raise `TypeError`. -Associates keys with multiple values. +Useful queries include `each`, `each_by_key`, `each_by_frequency`, `fetch`, +`has_key?`, `has_value?`, `key`, `keys`, `values`, `to_a`, and `to_h`. +Indexing an absent key returns zero, whereas `fetch` can return a supplied +default, invoke a block, or raise `KeyError`. + + +## `MultiMap` + +`Xqsr3::Containers::MultiMap` maps each key to an array of values. It includes +`Enumerable`, and its `size` is the number of keys while `count` is the total +number of stored values. ```Ruby require 'xqsr3/containers/multi_map' + +groups = Xqsr3::Containers::MultiMap.new +groups.push :ruby, 'MRI', 'JRuby' +groups.push :ruby, 'TruffleRuby' +groups.push :python, 'CPython' + +groups[:ruby] # => ['MRI', 'JRuby', 'TruffleRuby'] +groups[:python] # => ['CPython'] +groups.size # => 2 +groups.count # => 4 ``` +Pushing a key with no values still creates the key and associates it with an +empty array. Indexing an absent key returns `nil`, which differs from an +existing key whose value array is empty. + +The primary mutation methods are: + +* `push(key, *values)` appends values to the key's existing array; +* `store(key, *values)` replaces the key's value array; +* `delete(key)` removes all values mapped to the key; +* `multi_merge` and `multi_merge!` concatenate values for duplicate keys; +* `strict_merge` and `strict_merge!` replace values for duplicate keys. + +`MultiMap[...]` accepts a `Hash` whose values are arrays, an array of +`[key, value, ...]` entries, or multiple such entries. `each` yields one +`[key, value]` pair for every stored value. Use `each_unflattened` when the +value array must be yielded as a whole. + + +## Shared conventions + +Both containers provide `assoc`, `clear`, `delete`, `each_key`, `fetch`, +`flatten`, `has_key?`, `keys`, `length`, `shift`, `size`, `to_a`, `to_hash`, +and `values`-style operations. Both support `dup`, equality comparison with +their corresponding `Hash` representation, and enumerator-returning methods +when no block is supplied. + +The containers retain insertion order through their underlying Ruby hashes. +Methods explicitly described as sorted—`FrequencyMap#each_by_key` and +`FrequencyMap#each_by_frequency`—perform ordering work at enumeration time. + +For executable behavioural examples, see the unit tests in +`test/unit/containers/`. + From de4d4cd6c202db501d7f17631d7cec40c4be0893 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 28 Aug 2026 20:40:55 +1000 Subject: [PATCH 05/23] docs(components): document scalar conversion policies * explain when to use BoolParser and IntegerParser at input boundaries; * document strict boolean matching, custom matchers, aliases, and fallbacks; * document integer bases, native exceptions, defaults, nil handling, and recovery blocks; * clarify the distinction between conversion and validation; * verify the documented conversion examples against the implementation; --- docs/components/conversion.md | 143 ++++++++++++++++++++++++++++++++-- 1 file changed, 135 insertions(+), 8 deletions(-) diff --git a/docs/components/conversion.md b/docs/components/conversion.md index 88e60dd..4a3863b 100644 --- a/docs/components/conversion.md +++ b/docs/components/conversion.md @@ -1,39 +1,166 @@ # xqsr3 Conversion -Conversion components parse common scalar values while keeping conversion -rules explicit at the call site. +Conversion components turn external scalar values into application values +while making invalid-input policy explicit. Use them at boundaries such as +configuration files, command-line options, and environment variables. + +The two parsers have deliberately different failure models: boolean parsing +returns a default when there is no match, while integer parsing preserves +Ruby's normal conversion exceptions unless a fallback is requested. ## Table of Contents - [Loading](#loading) -- [Components](#components) +- [Choosing a parser](#choosing-a-parser) +- [BoolParser](#boolparser) +- [IntegerParser](#integerparser) +- [Conversion versus validation](#conversion-versus-validation) ## Loading +Load both parsers through the category entry point: + ```Ruby require 'xqsr3/conversion' ``` +Or load only the parser required by the application: + +```Ruby +require 'xqsr3/conversion/bool_parser' +require 'xqsr3/conversion/integer_parser' +``` + +The parsers are defined in `Xqsr3::Conversion`. + -## Components +## Choosing a parser -### `Conversion::BoolParser` +* Choose `BoolParser` for configurable textual true/false tokens; +* Choose `IntegerParser` when Ruby's `Integer` conversion rules, including + numeric bases, are appropriate; +* Use a fallback option when invalid external input is expected and should not + interrupt processing; +* Leave fallbacks out when invalid input should be surfaced to the caller. -Parses supported textual representations of boolean values. + +## `BoolParser` + +`Xqsr3::Conversion::BoolParser.to_bool` performs a whole-string match. By +default, `true`, `TRUE`, and `1` produce `true`; `false`, `FALSE`, and `0` +produce `false`; anything else produces `nil`. ```Ruby require 'xqsr3/conversion/bool_parser' + +parser = Xqsr3::Conversion::BoolParser + +parser.to_bool('true') # => true +parser.to_bool('0') # => false +parser.to_bool('truest') # => nil +``` + +The parser accepts custom literal strings and regular expressions: + +```Ruby +parser.to_bool( + 'enabled', + true_values: ['enabled', 'yes'], + false_values: ['disabled', 'no'], +) # => true + +parser.to_bool( + 'enabled for production', + true_values: /\Aenabled/, +) # => true ``` -### `Conversion::IntegerParser` +Regular expressions are applied as supplied. The defaults are anchored and +case-insensitive, which is why substrings such as `truest` and `falsehood` +are not accepted by default. + +Fallback and result values are independently configurable. The long option +names and their shorter aliases are equivalent: -Parses integer values according to the component's documented rules. +* `default_value` / `default` is returned when neither set matches; +* `true_value` / `true` is returned when a true token matches; +* `false_value` / `false` is returned when a false token matches. + +The configured values may be any objects, including `nil` and `false`. +True-token matching is attempted before false-token matching, so overlapping +custom matchers resolve to the true result. + + +## `IntegerParser` + +`Xqsr3::Conversion::IntegerParser.to_integer` wraps Ruby integer conversion +with optional fallback behaviour. It also has an instance form when the module +is included. ```Ruby require 'xqsr3/conversion/integer_parser' + +parser = Xqsr3::Conversion::IntegerParser + +parser.to_integer('42') # => 42 +parser.to_integer('-100', 2) # => -4 +parser.to_integer(42, 16) # => 42 +``` + +The `base` argument is passed to Ruby's conversion only for string inputs. +Non-string inputs use the ordinary one-argument conversion, so a base does +not reinterpret an existing numeric value. + +By default, `nil` and malformed values raise the underlying conversion +exception: + +```Ruby +parser.to_integer('not a number') # raises ArgumentError +parser.to_integer(nil) # raises TypeError ``` +Use `default` to return a chosen value for `nil` or conversion failure: + +```Ruby +parser.to_integer('not a number', default: 0) # => 0 +parser.to_integer(nil, default: :missing) # => :missing +``` + +Use `nil: true` when all failed conversions should return `nil`. If both +options are supplied, `default` takes precedence: + +```Ruby +parser.to_integer('not a number', nil: true) # => nil +parser.to_integer('not a number', default: :missing, nil: true) # => :missing +``` + +For custom recovery, pass a block. It receives the conversion exception, the +original argument, the base, and the options hash. Its return value becomes +the result: + +```Ruby +parser.to_integer('not a number') do |exception, argument, base, options| + warn "#{argument.inspect}: #{exception.message}" + :unparseable +end # => :unparseable +``` + +The recovery block is invoked for `ArgumentError` and `TypeError`. Exceptions +raised by the recovery block itself are not swallowed. + + +## Conversion versus validation + +These parsers are conversion tools, not complete input-validation policies. +For example, `BoolParser` can return an application-specific sentinel, and +`IntegerParser` can delegate failure handling to a block. If an application +must reject unknown values, check for the chosen sentinel or allow the +conversion exception to propagate rather than silently accepting a fallback. + +For executable behavioural examples, see the unit tests in +`test/unit/conversion/`. + From 7008f9fc7779a771d94ca08e75c037f5d76964cd Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 28 Aug 2026 20:49:25 +1000 Subject: [PATCH 06/23] docs(components): document string transformation and matching semantics * explain prefix and suffix matching return values and edge cases; * distinguish empty-string and whitespace-only normalization; * document conditional quoting triggers and quote configuration; * document symbol conversion, character transformation, and rejection policies; * document truncation width and omission behaviour; * clarify standalone utility and String extension loading; * link behavioural documentation to the relevant unit tests; --- docs/components/string-utilities.md | 212 +++++++++++++++++++++++++--- 1 file changed, 196 insertions(+), 16 deletions(-) diff --git a/docs/components/string-utilities.md b/docs/components/string-utilities.md index 4f6f793..aa95887 100644 --- a/docs/components/string-utilities.md +++ b/docs/components/string-utilities.md @@ -1,38 +1,218 @@ # xqsr3 String Utilities -String utilities provide standalone predicates, normalisation, conversion, -quoting, and truncation operations. +String utilities provide small operations for matching, normalising, +quoting, converting, and shortening strings. They are especially useful when +turning loosely formatted external text into a stable value for later +processing. + +Several methods have deliberately useful return values rather than merely +returning `true` or `false`: the prefix and suffix methods return the matching +text, while the `nil_if_*` methods return either the original string or +`nil`. ## Table of Contents - [Loading](#loading) -- [Components](#components) +- [Choosing a utility](#choosing-a-utility) +- [Prefix and suffix matching](#prefix-and-suffix-matching) +- [Blank-value normalization](#blank-value-normalization) +- [Conditional quoting](#conditional-quoting) +- [Symbol conversion](#symbol-conversion) +- [Truncation](#truncation) +- [Extensions and standalone forms](#extensions-and-standalone-forms) ## Loading +Load all standalone string utilities through the category entry point: + ```Ruby require 'xqsr3/string_utilities' ``` -Individual components can also be loaded directly. +Or load a specific utility: + +```Ruby +require 'xqsr3/string_utilities/to_symbol' +``` + +The utility modules are defined in `Xqsr3::StringUtilities`. + + +## Choosing a utility + +* Use `starts_with?` or `ends_with?` when the matching text itself is useful; +* Use `nil_if_empty` when whitespace is meaningful and only `''` is blank; +* Use `nil_if_whitespace` when input containing only whitespace is also blank; +* Use `quote_if` for display or command-line-like formatting; +* Use `to_symbol` when accepting a restricted identifier-like external value; +* Use `truncate` when output must fit a known character width. + + +## Prefix and suffix matching + +`starts_with?` and `ends_with?` accept one or more strings. They return the +matching prefix or suffix string, or `nil` when no argument matches. This +allows a match to be used directly: + +```Ruby +require 'xqsr3/string_utilities/starts_with' +require 'xqsr3/string_utilities/ends_with' + +name = 'xqsr3-utilities.rb' + +Xqsr3::StringUtilities::StartsWith.string_starts_with?(name, 'xqsr3') +# => 'xqsr3' +Xqsr3::StringUtilities::EndsWith.string_ends_with?(name, '.rb') +# => '.rb' +Xqsr3::StringUtilities::StartsWith.string_starts_with?(name, 'ruby', 'xqsr3') +# => 'xqsr3' +``` + +The standalone forms are `StringUtilities::StartsWith.string_starts_with?` +and `StringUtilities::EndsWith.string_ends_with?`. A prefix or suffix may be +`nil`, or an object responding to `to_str`; other types raise `TypeError`. +Passing no arguments, or a `nil` argument, returns the empty string rather +than `nil`. + +Matching is literal and case-sensitive. These methods do not interpret +regular expressions. + + +## Blank-value normalization + +`nil_if_empty` returns `nil` for `''` and returns the original string for +every non-empty string, including whitespace: + +```Ruby +Xqsr3::StringUtilities::NilIfEmpty.string_nil_if_empty(' ') # => ' ' +Xqsr3::StringUtilities::NilIfEmpty.string_nil_if_empty('') # => nil +``` + +`nil_if_whitespace` uses `String#strip`: it returns `nil` for an empty or +whitespace-only string, and otherwise returns the original, unmodified string: + +```Ruby +Xqsr3::StringUtilities::NilIfWhitespace.string_nil_if_whitespace(' ') +# => nil +Xqsr3::StringUtilities::NilIfWhitespace.string_nil_if_whitespace(' ruby ') +# => ' ruby ' +``` + +Neither method strips a non-blank value. Use `strip` explicitly when +normalisation of the surviving value is also required. + + +## Conditional quoting + +`quote_if` converts its input to a string and surrounds it with quotes only +when it contains a quotable character. By default, whitespace is quotable and +the surrounding quote is `"`. + +```Ruby +require 'xqsr3/string_utilities/quote_if' + +Xqsr3::StringUtilities::QuoteIf.quote_if('ruby') +# => 'ruby' +Xqsr3::StringUtilities::QuoteIf.quote_if('ruby language') +# => '"ruby language"' +``` + +Use `quotables` to select the trigger. It accepts a string, an array of +strings, or a regular expression. Use `quotes` to provide either one string +for both sides or an array containing opening and closing strings: + +```Ruby +Xqsr3::StringUtilities::QuoteIf.quote_if( + 'a=b', + quotables: '=', + quotes: ['<', '>'], +) +# => '' + +Xqsr3::StringUtilities::QuoteIf.quote_if('a/b', quotables: ['/']) +# => '"a/b"' +``` + +An invalid `quotables` type raises `ArgumentError`. Existing quoted strings +are not specially detected; quoting is based only on whether the configured +quotables are present. + + +## Symbol conversion + +`to_symbol` accepts a string, or an object responding to `to_str`, and returns +an identifier-like symbol. ASCII letters and underscores are retained, digits +are retained after the first character, and other accepted separators are +converted to underscores: + +```Ruby +require 'xqsr3/string_utilities/to_symbol' + +Xqsr3::StringUtilities::ToSymbol.string_to_symbol('user-name') +# => :user_name +Xqsr3::StringUtilities::ToSymbol.string_to_symbol('user name') +# => :user_name +Xqsr3::StringUtilities::ToSymbol.string_to_symbol( + 'user-name', + reject_hyphens: true, +) # => nil +``` + +By default, hyphens, spaces, and tabs are converted to underscores. Other +characters cause `nil` unless they appear in `transform_characters`, in which +case they are also converted to underscores. `reject_spaces`, `reject_tabs`, +and `reject_whitespace` reject the corresponding separators instead of +transforming them. An empty input returns `nil`. + +The standalone form is +`StringUtilities::ToSymbol.string_to_symbol(string, options)`. + + +## Truncation + +`truncate` limits a string to a requested width. If the string already fits, +it is returned unchanged. Otherwise, the default omission string `...` is +included within the requested width: + +```Ruby +require 'xqsr3/string_utilities/truncate' + +Xqsr3::StringUtilities::Truncate.string_truncate('abcdef', 6) +# => 'abcdef' +Xqsr3::StringUtilities::Truncate.string_truncate('abcdefgh', 6) +# => 'abc...' +Xqsr3::StringUtilities::Truncate.string_truncate('abcdefgh', 2) +# => '..' +``` + +Provide `omission` to change the marker. If the requested width is shorter +than the omission, the omission itself is truncated: + +```Ruby +Xqsr3::StringUtilities::Truncate.string_truncate( + 'abcdefgh', + 6, + omission: ' [...] ', +) +# => ' [...]' +``` + +The standalone form is +`StringUtilities::Truncate.string_truncate(string, width, options)`. -## Components +## Extensions and standalone forms -* `StringUtilities::EndsWith`; -* `StringUtilities::NilIfEmpty`; -* `StringUtilities::NilIfWhitespace`; -* `StringUtilities::QuoteIf`; -* `StringUtilities::StartsWith`; -* `StringUtilities::ToSymbol`; -* `StringUtilities::Truncate`; +Each utility has a standalone module method and a corresponding instance +method when its extension is loaded. The extension methods are grouped under +`xqsr3/extensions/string`; loading all extension groups is broader than +loading the standalone utility category. -The corresponding standard-library extensions include -`String#ends_with?`, `String#nil_if_empty`, `String#nil_if_whitespace`, -`String#quote_if`, `String#starts_with?`, `String#to_bool`, `String#to_symbol`, -and `String#truncate`. +For executable behavioural examples, see the string extension tests in +`test/unit/extensions/string/` and the standalone truncation tests in +`test/unit/string_utilities/`. From 553ee441900bf86429a76c55532713258eed9977 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 28 Aug 2026 20:57:42 +1000 Subject: [PATCH 07/23] docs(components): document diagnostic context and exception chaining * explain when to use option-aware raising, inspection building, and causes; * document ExceptionUtilities constructor options and raise compatibility; * document InspectBuilder field selection, truncation, and hidden-field policies; * document WithCause construction, chaining, traversal, and message separators; * clarify application-level versus interpreter-managed exception causes; * add guidance for avoiding sensitive diagnostic output; * verify examples against the diagnostics unit tests; --- docs/components/diagnostics.md | 183 +++++++++++++++++++++++++++++++-- 1 file changed, 173 insertions(+), 10 deletions(-) diff --git a/docs/components/diagnostics.md b/docs/components/diagnostics.md index 2b27240..9490ca7 100644 --- a/docs/components/diagnostics.md +++ b/docs/components/diagnostics.md @@ -1,47 +1,210 @@ # xqsr3 Diagnostics -Diagnostics components provide small utilities for constructing, raising, and -inspecting exceptions. +Diagnostics components help preserve useful context when a program fails. +They cover three related needs: raising option-aware exceptions, producing +consistent object inspection strings, and retaining an application-level +exception chain. + +Use these components at boundaries where a generic Ruby exception would lose +information that a caller needs to diagnose or report a failure. ## Table of Contents - [Loading](#loading) -- [Components](#components) +- [Choosing a diagnostic tool](#choosing-a-diagnostic-tool) +- [ExceptionUtilities](#exceptionutilities) +- [InspectBuilder](#inspectbuilder) +- [Exceptions::WithCause](#exceptionswithcause) +- [Design considerations](#design-considerations) ## Loading +Load all diagnostics components through the category entry point: + ```Ruby require 'xqsr3/diagnostics' ``` +Or load only the required component: + +```Ruby +require 'xqsr3/diagnostics/exception_utilities' +require 'xqsr3/diagnostics/inspect_builder' +require 'xqsr3/diagnostics/exceptions/with_cause' +``` + +The components are defined under `Xqsr3::Diagnostics`. + -## Components +## Choosing a diagnostic tool -### `Diagnostics::ExceptionUtilities` +* Use `ExceptionUtilities` when an exception class accepts keyword options; +* Use `InspectBuilder` when diagnostic output should expose selected object + state consistently; +* Use `Exceptions::WithCause` when an exception should retain a domain-level + inner exception and report the chain; +* Use Ruby's native exception cause when the interpreter's raise-chain + semantics, rather than an application-owned chain, are wanted. -Provides helpers for raising exceptions with additional options. + +## `ExceptionUtilities` + +`Xqsr3::Diagnostics::ExceptionUtilities.raise_with_options` preserves the +usual `Kernel#raise` forms and adds keyword options when the first argument is +an exception class. The options are passed to that class's constructor. ```Ruby require 'xqsr3/diagnostics/exception_utilities' + +class ConfigurationError < ArgumentError + attr_reader :options + + def initialize(message = nil, **options) + super(message) + @options = options + end +end + +begin + Xqsr3::Diagnostics::ExceptionUtilities.raise_with_options( + ConfigurationError, + 'invalid port', + section: :server, + value: 'abc', + ) +rescue ConfigurationError => error + error.message # => 'invalid port' + error.options # => { section: :server, value: 'abc' } +end +``` + +When no options are needed, the method behaves like `raise`: + +```Ruby +Xqsr3::Diagnostics::ExceptionUtilities.raise_with_options 'failure' +# raises RuntimeError +Xqsr3::Diagnostics::ExceptionUtilities.raise_with_options ArgumentError +# raises ArgumentError +Xqsr3::Diagnostics::ExceptionUtilities.raise_with_options( + ArgumentError, + 'failure', +) ``` -### `Diagnostics::InspectBuilder` +The class must be the first argument for options to be meaningful. Supplying +options with a message, exception instance, or other non-class first argument +causes a warning and falls back to ordinary `Kernel#raise` behaviour. + +The method preserves an explicitly supplied backtrace and trims the helper's +own internal frames. This keeps reported failures focused on the caller's +operation rather than on the implementation of `raise_with_options`. + + +## `InspectBuilder` -Supports the construction of diagnostic inspection strings. +`Xqsr3::Diagnostics::InspectBuilder` is an includable module for classes that +need consistent `inspect`-style diagnostics. Its `make_inspect` method can +include class information, object identity, and instance fields: ```Ruby require 'xqsr3/diagnostics/inspect_builder' + +class Job + include Xqsr3::Diagnostics::InspectBuilder + + def initialize(name, token) + @name = name + @token = token + end +end + +job = Job.new('compile', 'secret') +job.make_inspect(show_fields: true, no_object_id: true) +# => "#" ``` -### `Diagnostics::Exceptions::WithCause` +The options are: + +* `no_class` omits the class qualification; +* `no_object_id` omits the object identifier; +* `show_fields` includes instance variables; +* `shown_fields` restricts output to named fields; +* `hidden_fields` excludes named fields; +* `truncate_width` limits field values using the String Utilities truncation + rules; +* `deep_inspect` obtains field values through their own `inspect` methods. + +Field names may be supplied with or without the leading `@`. `shown_fields` +takes precedence over `hidden_fields`. A class can define +`INSPECT_HIDDEN_FIELDS` to establish default exclusions for its instances and +subclasses. -Represents an exception that retains an underlying cause. +Use `shown_fields` or a class-level hidden-field list for secrets, credentials, +and other values that should not appear in logs. `inspect` output is a +diagnostic representation, not a serialization format. + + +## `Exceptions::WithCause` + +`Xqsr3::Diagnostics::Exceptions::WithCause` is an inclusion module for custom +exception classes. It adds a `cause` attribute, preserves constructor options, +and exposes the chain through `chainees`, `exceptions`, `chained_message`, and +`chained_backtrace`. ```Ruby require 'xqsr3/diagnostics/exceptions/with_cause' + +class ImportError < StandardError + include Xqsr3::Diagnostics::Exceptions::WithCause +end + +begin + begin + raise ArgumentError, 'port is not numeric' + rescue ArgumentError => cause + raise ImportError.new('configuration is invalid', cause: cause) + end +rescue ImportError => error + error.message # => 'configuration is invalid' + error.cause.message # => 'port is not numeric' + error.chained_message # => 'configuration is invalid: port is not numeric' + error.exceptions # => [error, error.cause] +end +``` + +The `cause:` keyword is the clearest way to specify the inner exception. The +module can also infer an exception supplied among the positional constructor +arguments. When an exception is supplied without a separate outer message, +its message can become the outer exception's message. + +`chained_message` uses `': '` between levels by default. Pass `separator` to +choose another separator: + +```Ruby +error.chained_message(separator: ' <- ') +# => 'configuration is invalid <- port is not numeric' ``` +`chainees` excludes the receiver; `exceptions` includes the receiver. Both +follow the cause chain in outer-to-inner order. `chained_backtrace` combines +the backtraces of the chain for diagnostic display. + + +## Design considerations + +`WithCause#cause` is an application-level attribute and intentionally shadows +Ruby's interpreter-managed `Exception#cause` for classes that include the +module. Do not mix the two models casually in a single exception hierarchy. + +Avoid putting secrets into exception messages or inspectable instance +variables. Prefer `InspectBuilder` field selection and explicit exception +options when diagnostic context must be retained without exposing everything +in logs. + +For executable behavioural examples, see the unit tests in +`test/unit/diagnostics/` and `test/unit/diagnostics/exceptions/`. + From a02fedbea6f2a8ec2b0ebd0fe06b5ebcf567120b Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 28 Aug 2026 21:06:57 +1000 Subject: [PATCH 08/23] docs(components): document Ruby extension loading and semantics * explain narrow, grouped, standard, and test-inclusive loading scopes; * document global core-class modification and reusable-library trade-offs; * describe Enumerable extension semantics and block contracts; * document Hash extension mutation and Ruby-version compatibility behaviour; * document Integer, IO, Kernel, String, and test/unit extensions; * correct examples for IO.writelines and Integer#to_s_grp; * verify extension examples against 119 unit tests and 390 assertions; --- docs/components/extensions.md | 218 +++++++++++++++++++++++++++++----- 1 file changed, 185 insertions(+), 33 deletions(-) diff --git a/docs/components/extensions.md b/docs/components/extensions.md index 4558b0b..96788bc 100644 --- a/docs/components/extensions.md +++ b/docs/components/extensions.md @@ -1,80 +1,232 @@ # xqsr3 Ruby Extensions Ruby extensions add focused methods to standard-library classes and modules. -They are grouped by the class or module they extend. +They are useful when an operation reads naturally as a method on the value +being processed, but they also modify the method sets of those classes for the +rest of the process. + +The extensions are opt-in. Choose the narrowest loading scope that suits the +application, particularly in gems and reusable libraries where global method +changes can surprise consumers. ## Table of Contents - [Loading](#loading) -- [Extension groups](#extension-groups) +- [Choosing a loading scope](#choosing-a-loading-scope) +- [Array extensions](#array-extensions) +- [Enumerable extensions](#enumerable-extensions) +- [Hash extensions](#hash-extensions) +- [Integer extensions](#integer-extensions) +- [IO extensions](#io-extensions) +- [Kernel extensions](#kernel-extensions) +- [String extensions](#string-extensions) +- [test/unit extensions](#testunit-extensions) +- [Extension design considerations](#extension-design-considerations) ## Loading +Load the standard-library extension groups: + ```Ruby require 'xqsr3/extensions' ``` -Individual extension groups and methods can be loaded directly. +This loads the Array, Enumerable, Hash, Integer, IO, Kernel, and String +groups. It does not load the `test/unit` extensions. + +Load every extension, including test helpers, with: + +```Ruby +require 'xqsr3/all_extensions' +``` + +Individual groups and methods can be loaded directly: + +```Ruby +require 'xqsr3/extensions/enumerable' +require 'xqsr3/extensions/enumerable/detect_map' +``` + + +## Choosing a loading scope + +* Use a method-specific `require` when only one extension is needed; +* Use a class-group `require` when an application deliberately adopts several + extensions for one standard-library type; +* Use `xqsr3/extensions` for an application that wants the standard-library + extensions as a set; +* Use `xqsr3/all_extensions` in test runners or controlled application + environments that also need the `test/unit` assertions; +* Avoid loading extensions implicitly from a reusable library's top-level + entry point unless the global methods are part of that library's contract. + + +## Array extensions + +`Array#join_with_or` formats an array as a human-readable list. The +standalone implementation and its extension are described in the +[Array Utilities](./array-utilities.md) page. + +```Ruby +require 'xqsr3/extensions/array/join_with_or' + +['red', 'green', 'blue'].join_with_or +``` + + +## Enumerable extensions + +The Enumerable extensions are: + +* `collect_with_index(base = 0)`, which maps values while passing a + base-adjusted index to the block; +* `detect_map`, which returns the first non-`nil` block result and therefore + combines detection with transformation; +* `unique`, which retains the first occurrence of each distinct value and + also supports a two-argument equality block. + +```Ruby +require 'xqsr3/extensions/enumerable' + +['a', 'b'].collect_with_index(1) { |value, index| "#{index}:#{value}" } +# => ['1:a', '2:b'] + +[1, 2, 3].detect_map { |value| value * 10 if value > 1 } +# => 20 + +[1, 2, 1, 3].unique +# => [1, 2, 3] +``` + +`detect_map` treats `false` and `0` as successful results; only `nil` +continues the search. Its block must have arity one for sequences or two for +key/value associations. `unique` without a block uses hash membership and +retains encounter order. Comparator mode is more expensive because retained +values are checked one at a time. + + +## Hash extensions + +The Hash extensions are: +* `deep_transform` and `deep_transform!` for recursive value transformation; +* `except` and `except!` for removing named keys; +* `has_match?` and `match` for regular-expression-based key matching; +* `slice` for selecting named keys. -## Extension groups +`except` returns a copy, while `except!` mutates the receiver. `slice` is +provided only where the Ruby version does not already define it; loading the +extension does not replace the native implementation. -* [Array extensions](#array-extensions); -* [Enumerable extensions](#enumerable-extensions); -* [Hash extensions](#hash-extensions); -* [Integer extensions](#integer-extensions); -* [IO extensions](#io-extensions); -* [Kernel extensions](#kernel-extensions); -* [String extensions](#string-extensions); -* [test/unit extensions](#testunit-extensions); +```Ruby +require 'xqsr3/extensions/hash/except' -The group-specific APIs are documented in the generated API reference and -will be expanded here with examples and behavioural details. +settings = { host: 'localhost', port: 80, secret: 'hidden' } +settings.except(:secret) +# => { host: 'localhost', port: 80 } +settings +# remains unchanged +``` -### Array extensions +## Integer extensions -Provides `Array#join_with_or`. +`Integer#to_s_grp` formats an integer's decimal representation with grouping. +Pass the group width explicitly; a call without arguments returns the ordinary +decimal representation. +```Ruby +require 'xqsr3/extensions/integer/to_s_grp' -### Enumerable extensions +1234567.to_s_grp(3) # => '1,234,567' +``` -Provides `Enumerable#collect_with_index`, `Enumerable#detect_map`, and -`Enumerable#unique`. +## IO extensions -### Hash extensions +`IO.writelines` writes supplied contents to a path. It is useful when the +caller wants the library to handle opening and writing the output file. -Provides deep transformation, matching, slicing, and exception-style -operations for hashes. +```Ruby +require 'xqsr3/extensions/io/writelines' + +IO.writelines('output.txt', ['first', 'second']) +``` -### Integer extensions +## Kernel extensions -Provides `Integer#to_s_grp`. +The Kernel extensions are: +* `Kernel::Integer`, which provides the xqsr3 integer conversion entry point; +* `Kernel#raise_with_options`, which raises option-aware exception classes. -### IO extensions +The conversion and diagnostics pages describe their behaviour in detail. -Provides `IO#writelines`. +## String extensions -### Kernel extensions +The String extension group provides: -Provides `Kernel::Integer` and `Kernel#raise_with_options`. +* `ends_with?` and `starts_with?`; +* `nil_if_empty` and `nil_if_whitespace`; +* `quote_if`; +* `to_bool` and `to_symbol`; +* `truncate`; +* `map_option_string`. + +```Ruby +require 'xqsr3/extensions/string' + +'user-name'.to_symbol # => :user_name +'ruby language'.quote_if # => '"ruby language"' +``` + +See [String Utilities](./string-utilities.md) for the matching, normalization, +quoting, symbol, and truncation semantics. `to_bool` and `map_option_string` +are documented with the other extension-specific APIs. + + +## test/unit extensions + +The `test/unit` group adds assertions such as: + +* `assert_eql`; +* `assert_false`; +* `assert_not`; +* `assert_not_eql`; +* `assert_raise_with_message`; +* `assert_subclass_of`; +* `assert_superclass_of`; +* `assert_true`; +* `assert_type_has_instance_methods`. + +Load them explicitly in test code: + +```Ruby +require 'xqsr3/extensions/test/unit' +``` +These helpers are development/test conveniences and are not included by +`require 'xqsr3/extensions'`. -### String extensions -Provides string predicates, conversions, formatting, and truncation helpers. +## Extension design considerations +Loading an extension changes a core Ruby class or module globally. This is +convenient for application code but can create name collisions and hidden +coupling between gems. Prefer narrow `require` paths in libraries and make +broader loading an explicit application decision. -### test/unit extensions +The extension methods delegate to the standalone xqsr3 utilities where both +forms exist. This permits code to choose between explicit module calls and +method syntax without introducing a runtime dependency beyond the standard +Ruby library. -Provides additional assertions for `test-unit`, including type and exception -assertions. +For executable behavioural examples, see the extension tests under +`test/unit/extensions/`. From cc0a4517794f33d0b428fd4b83ae6b72a45c1b63 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 28 Aug 2026 21:13:59 +1000 Subject: [PATCH 09/23] docs(components): document hash transformation and matching semantics * explain standalone and extension loading choices; * document deep transformation block arities and recursion boundaries; * distinguish copy and in-place transformation safety; * document exact-key precedence and regular-expression matching; * explain match versus has_match? results and nil ambiguity; * document regex-key handling and iteration-order effects; * verify examples against the hash utility unit tests; --- docs/components/hash-utilities.md | 172 ++++++++++++++++++++++++++++-- 1 file changed, 161 insertions(+), 11 deletions(-) diff --git a/docs/components/hash-utilities.md b/docs/components/hash-utilities.md index e1b81c6..c319c17 100644 --- a/docs/components/hash-utilities.md +++ b/docs/components/hash-utilities.md @@ -1,42 +1,192 @@ # xqsr3 Hash Utilities -Hash utilities provide standalone operations for transforming and matching -hashes. +Hash utilities provide explicit operations for recursively transforming hash +keys and values, and for finding entries through exact or regular-expression +matching. + +Use the utility modules when a library should avoid loading global `Hash` +extensions. The corresponding extensions are available when method syntax is +preferable. ## Table of Contents - [Loading](#loading) -- [Components](#components) +- [Choosing a utility](#choosing-a-utility) +- [DeepTransform](#deeptransform) +- [KeyMatching](#keymatching) +- [Hash extensions](#hash-extensions) +- [Safety and ordering](#safety-and-ordering) ## Loading +Load both utility modules through the category entry point: + ```Ruby require 'xqsr3/hash_utilities' ``` +Or load only the required utility: + +```Ruby +require 'xqsr3/hash_utilities/deep_transform' +require 'xqsr3/hash_utilities/key_matching' +``` + +The utility modules are defined in `Xqsr3::HashUtilities` and are includable +in hash-like classes. + + +## Choosing a utility + +* Use `DeepTransform` to apply a key or key/value transformation through + nested hashes; +* Use `KeyMatching#match` to retrieve the first matching value; +* Use `KeyMatching#has_match?` when only the existence of a match matters; +* Use `deep_transform` when the original hash must remain unchanged; +* Use `deep_transform!` only when in-place transformation is intentional and + partial mutation on failure is acceptable. -## Components -### `HashUtilities::DeepTransform` +## `DeepTransform` -Transforms values recursively through nested hashes and collections. +`Xqsr3::HashUtilities::DeepTransform` provides `deep_transform` and +`deep_transform!`. Both require a block with arity one or two: + +* A one-argument block transforms keys; +* A two-argument block transforms a key and value and must return the + replacement key/value pair. + +The non-bang method returns a new hash: ```Ruby -require 'xqsr3/hash_utilities/deep_transform' +require 'xqsr3/extensions/hash/deep_transform' + +input = { + user: { + name: 'Ada', + }, +} + +result = input.deep_transform { |key| key.to_s.upcase } + +result # => {'USER' => {'NAME' => 'Ada'}} +input # => {:user => {:name => 'Ada'}} ``` -### `HashUtilities::KeyMatching` +The one-argument form transforms keys, not values. Use the two-argument form +to transform values: + +```Ruby +result = input.deep_transform do |key, value| + [key.to_s, value.is_a?(String) ? value.strip : value] +end + +result # => {'user' => {'name' => 'Ada'}} +``` -Provides matching operations for hash keys. +Nested hashes are transformed recursively. Other nested collection types are +treated as values and are not traversed by this utility. + +The bang form changes the receiver and returns it: + +```Ruby +input = { user: { name: ' Ada ' } } + +input.deep_transform! do |key, value| + [key, value.is_a?(String) ? value.strip : value] +end + +input # => { user: { name: 'Ada' } } +``` + +The block is mandatory. A block with any arity other than one or two raises +`ArgumentError`. The non-bang form requires a hash-like object responding to +`map`; the bang form requires an object supporting `[]=`, `delete`, and +`keys`. + + +## `KeyMatching` + +`Xqsr3::HashUtilities::KeyMatching` provides `match` and `has_match?` in both +standalone and includable forms. A direct key match is checked first: ```Ruby require 'xqsr3/hash_utilities/key_matching' + +settings = { + 'database.host' => 'db.example.test', + 'database.port' => 5432, +} + +Xqsr3::HashUtilities::KeyMatching.match(settings, 'database.port') +# => 5432 +Xqsr3::HashUtilities::KeyMatching.has_match?(settings, 'database') +# => false +Xqsr3::HashUtilities::KeyMatching.has_match?(settings, /database\./) +# => true +``` + +When the search argument is a `Regexp`, ordinary keys are converted to strings +and tested against it. `match` returns the value for the first matching key, +or `nil`; `has_match?` returns `true` or `false`. + +Hashes may themselves contain regular-expression keys. When the search +argument is not a `Regexp`, those expression keys are tested against the +string form of the search argument: + +```Ruby +rules = { + /staging|production/ => :remote, + /development/ => :local, +} + +Xqsr3::HashUtilities::KeyMatching.match(rules, :production) +# => :remote +``` + +Because `match` returns the matching value, a matching entry whose value is +`nil` is indistinguishable from no match through the return value alone. Use +`has_match?` when that distinction matters. + + +## Hash extensions + +The standalone utilities can be applied to `Hash` through the corresponding +extension files: + +```Ruby +require 'xqsr3/extensions/hash/deep_transform' +require 'xqsr3/extensions/hash/has_match' + +{ 'a' => 1 }.deep_transform { |key| key.to_sym } +# => {:a => 1} ``` -Related extensions are available as `Hash#deep_transform`, `Hash#has_match?`, -`Hash#match`, `Hash#except`, and `Hash#slice`. +The complete Hash extension group additionally provides: + +* `except`, which returns a copy without selected keys; +* `except!`, which removes selected keys from the receiver; +* `match` and `has_match?`; +* `slice`, which selects existing keys into a new hash. + +`slice` is loaded only on Ruby versions that do not already provide the +native method. + + +## Safety and ordering + +`deep_transform` is the safer default when a transformation may fail because +the original receiver remains unchanged. `deep_transform!` is not strongly +exception-safe: an exception can leave the receiver partially transformed. + +Matching follows hash iteration order after the exact-key check. For +regular-expression searches, the first matching key determines the result; +overlapping expressions should therefore be ordered deliberately. + +For executable behavioural examples, see the unit tests in +`test/unit/hash_utilities/` and the related Hash extension tests. From 387b927ced9c724aef7963d2bce65fc1c11ec589 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 28 Aug 2026 21:26:29 +1000 Subject: [PATCH 10/23] docs(components): document parameter and option validation * explain boundary-checking patterns for public APIs; * document type, shape, capability, value, and emptiness checks; * describe case-insensitive and order-insensitive comparisons; * document custom validation blocks and failure classification; * explain option aliases and missing-option handling; * document nothrow, custom messages, and obsolete compatibility aliases; * verify examples against the quality unit tests; --- docs/components/quality.md | 234 +++++++++++++++++++++++++++++++++++-- 1 file changed, 225 insertions(+), 9 deletions(-) diff --git a/docs/components/quality.md b/docs/components/quality.md index 545a567..7a59f7a 100644 --- a/docs/components/quality.md +++ b/docs/components/quality.md @@ -1,35 +1,251 @@ # xqsr3 Quality -Quality components support explicit validation of method parameters and -preconditions. +The Quality components provide reusable parameter and option checks for +public method boundaries. They centralise common failure messages while +allowing a caller to choose whether invalid input raises or returns a +failure value. ## Table of Contents - [Loading](#loading) -- [Components](#components) +- [A boundary-checking pattern](#a-boundary-checking-pattern) +- [ParameterChecking](#parameterchecking) +- [Type and shape checks](#type-and-shape-checks) +- [Value and emptiness checks](#value-and-emptiness-checks) +- [Custom validation](#custom-validation) +- [Option checking](#option-checking) +- [Failure policy](#failure-policy) +- [Compatibility](#compatibility) ## Loading +Load the quality category: + ```Ruby require 'xqsr3/quality' ``` +Or load the component directly: + +```Ruby +require 'xqsr3/quality/parameter_checking' +``` + +The module is defined as `Xqsr3::Quality::ParameterChecking`. -## Components -### `Quality::ParameterChecking` +## A boundary-checking pattern -Provides reusable checks for validating parameter values, types, and -relationships. +Include the module in a class when checks are part of the class's +implementation: ```Ruby require 'xqsr3/quality/parameter_checking' + +class Endpoint + include Xqsr3::Quality::ParameterChecking + + def initialize(host, port) + @host = check_parameter( + host, + 'host', + type: String, + reject_empty: true, + ) + @port = check_parameter(port, 'port', values: [1..65_535]) + end +end +``` + +The module also exposes class-level forms for utility code: + +```Ruby +checker = Xqsr3::Quality::ParameterChecking +checker.check_parameter('localhost', 'host', type: String) +# => 'localhost' +``` + +Checks return the accepted or transformed value. This makes them convenient +in assignments and constructor boundaries. + + +## `ParameterChecking` + +`check_parameter(value, name, options = {})` checks one value. The `name` is +used in generated error messages and may be a string or symbol. The main +options are: + +* `type` accepts one class; +* `types` accepts several classes, including `:boolean` for either Boolean + class; +* an array within `types` requires an array whose elements match one of the + nested classes; +* `responds_to` requires one or more methods; +* `allow_nil` or its alias `nil` permits `nil`; +* `strip_str_whitespace` strips a `to_str` value before subsequent checks. + +For example: + +```Ruby +checker.check_parameter(true, 'enabled', types: [:boolean]) +# => true +checker.check_parameter(%w[red blue], 'colours', types: [[String]]) +# => ['red', 'blue'] +checker.check_parameter({}, 'options', responds_to: [:[], :keys]) +# => {} +``` + +Type failures raise `TypeError` by default. A nil value that is not allowed +raises `ArgumentError`. + + +## Type and shape checks + +Use `type` for one expected class and `types` for alternatives: + +```Ruby +checker.check_parameter(12, 'count', type: Integer) +# => 12 + +checker.check_parameter(:ready, 'state', types: [String, Symbol]) +# => :ready +``` + +Nested type arrays describe the contents of an array rather than accepting +the array class itself: + +```Ruby +checker.check_parameter([1, 2, 3], 'ids', types: [[Integer]]) +# => [1, 2, 3] +``` + +`responds_to` checks capabilities rather than concrete classes. It can accept +a single method name or an array: + +```Ruby +checker.check_parameter({}, 'options', responds_to: :keys) +# => {} +``` + + +## Value and emptiness checks + +`values` accepts an array of permitted values. Range entries are treated as +inclusive membership checks: + +```Ruby +checker.check_parameter(8080, 'port', values: [1..65_535]) +# => 8080 +``` + +For strings, `ignore_case: true` enables case-insensitive comparison. For +arrays, `ignore_order: true` permits the same values in a different order; +the two options can be combined for arrays of strings: + +```Ruby +checker.check_parameter( + 'JSON', + 'format', + values: ['json', 'yaml'], + ignore_case: true, +) +# => 'JSON' + +checker.check_parameter( + %w[blue red], + 'colours', + values: [%w[red blue]], + ignore_order: true, +) +# => ['blue', 'red'] +``` + +Use `reject_empty: true` to require a non-empty value, or +`require_empty: true` to require an empty value. These checks apply to values +responding to `empty?`. `strip_str_whitespace: true` runs first, so a string +containing only whitespace can become empty before `reject_empty` is checked. + + +## Custom validation + +Pass a one-argument block for a value-only predicate or a two-argument block +to receive the value and options. A truthy Boolean result accepts the value; +any other non-`true` result replaces the value; a falsey result rejects it: + +```Ruby +checker.check_parameter(7, 'count') { |value| value.positive? } +# => 7 ``` -Use the checks at public method boundaries so invalid arguments fail close to -their source. +For numeric values, a failed predicate raises `RangeError`; for other values +it raises `ArgumentError`. A block may return an exception instance to raise +that exception, or raise its own exception directly. + +The block is not called for `nil` values, and must have arity one or two. + + +## Option checking + +`check_option(options_hash, name, options = {})` applies the same validation +rules to a named option. It is useful when a method accepts a keyword/options +hash: + +```Ruby +options = { port: 8080 } + +checker.check_option( + options, + :port, + type: Integer, + values: [1..65_535], +) +# => 8080 +``` + +The name can be an array of aliases. The first present alias is selected, and +the selected value is returned: + +```Ruby +checker.check_option( + { stringize: :name }, + [:stringise, :stringize], +) +# => :name +``` + +With `allow_nil: true` or `nil: true`, a missing aliased option returns +`nil` instead of raising. Otherwise a missing option is reported as an option +failure. + + +## Failure policy + +By default, failed checks raise: + +* `ArgumentError` for missing, nil, empty, or invalid non-numeric values; +* `TypeError` for type, capability, or option-shape failures; +* `RangeError` for numeric values outside permitted ranges or failing a + custom numeric predicate. + +Use `nothrow: true` when a caller needs a pass/fail probe. A failed check then +returns `nil` instead of raising, while a successful check still returns its +value. Use `message` to replace the generated exception message. + +`check_param` is an obsolete alias for `check_parameter`; new code should use +the longer name. + + +## Compatibility + +`ParameterChecking` is includable, adds class-level forwarding methods when +included, and keeps the internal instance checking methods private. This +makes it suitable for implementation reuse without expanding the public +instance API of a class. + +For executable behavioural examples, see +`test/unit/quality/tc_parameter_checking.rb`. From 12dd71bc300fa73bd34f15e23284aaef8522d303 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 28 Aug 2026 21:32:20 +1000 Subject: [PATCH 11/23] docs(components): document human-readable array formatting * explain when join_with_or is appropriate; * document cardinality-specific formatting behaviour; * document conjunction, separator, Oxford-comma, and quote options; * explain standalone and Array extension loading; * document type and quoting caveats; * verify examples against the array utility unit tests; --- docs/components/array-utilities.md | 99 +++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 7 deletions(-) diff --git a/docs/components/array-utilities.md b/docs/components/array-utilities.md index 57d2242..1135be7 100644 --- a/docs/components/array-utilities.md +++ b/docs/components/array-utilities.md @@ -1,12 +1,17 @@ # xqsr3 Array Utilities Array utilities provide standalone operations for working with Ruby arrays. +The current component is a formatter for presenting alternatives in +human-readable text. ## Table of Contents - [Loading](#loading) -- [Components](#components) +- [When to use it](#when-to-use-it) +- [JoinWithOr](#joinwithor) +- [Formatting options](#formatting-options) +- [Standalone and extension forms](#standalone-and-extension-forms) ## Loading @@ -15,21 +20,101 @@ Array utilities provide standalone operations for working with Ruby arrays. require 'xqsr3/array_utilities' ``` -Individual components can also be loaded directly. +Or load the component directly: +```Ruby +require 'xqsr3/array_utilities/join_with_or' +``` + +The standalone module is +`Xqsr3::ArrayUtilities::JoinWithOr`. + + +## When to use it + +Use `join_with_or` for messages that describe alternatives, such as accepted +formats, permitted values, or choices presented to a user. It is not a +general-purpose serialization method: it adds grammatical spacing and an +optional Oxford comma. -## Components -### `ArrayUtilities::JoinWithOr` +## `JoinWithOr` -Formats an array as a human-readable list using commas and “or” before the -final item. +`Xqsr3::ArrayUtilities::JoinWithOr.join_with_or` formats values using these +cardinality rules: + +* `nil` and an empty array produce `''`; +* one value produces that value; +* two values are joined with `or` and no comma; +* three or more values use commas and, by default, an Oxford comma before + `or`. ```Ruby require 'xqsr3/array_utilities/join_with_or' + +formatter = Xqsr3::ArrayUtilities::JoinWithOr + +formatter.join_with_or([]) +# => '' +formatter.join_with_or(['red']) +# => 'red' +formatter.join_with_or(['red', 'green']) +# => 'red or green' +formatter.join_with_or(['red', 'green', 'blue']) +# => 'red, green, or blue' +``` + +Values are interpolated into the result, so they do not need to be strings. +The array argument itself must be an `Array` or `nil`; another type raises +`TypeError`. + + +## Formatting options + +The `or` word, separator, Oxford-comma policy, and quote character are +independent options: + +```Ruby +formatter.join_with_or( + ['red', 'green', 'blue'], + or: 'OR', + separator: ';', + oxford_comma: false, + quote_char: '"', +) +# => '"red"; "green" OR "blue"' +``` + +The options are: + +* `or` replaces the default word `or`; +* `separator` replaces the default comma between items; +* `oxford_comma: false` removes the separator before `or` for lists of three + or more values; +* `quote_char` surrounds every value with the supplied character. + +The Oxford-comma option has no effect for zero, one, or two values. A +`quote_char` is applied literally to both sides and does not escape quote +characters already present in a value. + + +## Standalone and extension forms + +The extension form adds `Array#join_with_or`: + +```Ruby +require 'xqsr3/extensions/array/join_with_or' + +['red', 'green', 'blue'].join_with_or +# => 'red, green, or blue' ``` -The corresponding extension is available as `Array#join_with_or`. +The extension delegates to the standalone implementation. Use the standalone +form in reusable libraries that want to avoid modifying `Array`; use the +extension form when method syntax improves the surrounding application code. + +For executable behavioural examples, see +`test/unit/array_utilities/tc_join_with_or.rb`. From 61e7c87c6d78397e5853916ca4914ba5b19a1afc Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 28 Aug 2026 21:36:51 +1000 Subject: [PATCH 12/23] docs(components): document command-line option mapping * distinguish option mapping from complete command-line parsing; * document long-option and shortcut matching semantics; * explain bracketed shortcut declaration grammar; * document canonical symbol conversion behaviour; * explain standalone and String extension forms; * document nil, unmatched, ordering, and failure behaviour; * verify examples against the command-line unit tests; --- docs/components/command-line-utilities.md | 133 ++++++++++++++++++++-- 1 file changed, 125 insertions(+), 8 deletions(-) diff --git a/docs/components/command-line-utilities.md b/docs/components/command-line-utilities.md index af1723d..1ff7a07 100644 --- a/docs/components/command-line-utilities.md +++ b/docs/components/command-line-utilities.md @@ -1,34 +1,151 @@ # xqsr3 Command-line Utilities -Command-line utilities support small, reusable operations for processing -option strings. +Command-line utilities support compact mapping from user-facing option text +to stable Ruby symbols. The component is particularly useful when a command +line accepts both long option names and declared single-letter shortcuts. ## Table of Contents - [Loading](#loading) -- [Components](#components) +- [When to use it](#when-to-use-it) +- [MapOptionString](#mapoptionstring) +- [Option-string grammar](#option-string-grammar) +- [Standalone and extension forms](#standalone-and-extension-forms) +- [Failure and matching behaviour](#failure-and-matching-behaviour) ## Loading +Load the command-line utility category: + ```Ruby require 'xqsr3/command_line_utilities' ``` +Or load the component directly: + +```Ruby +require 'xqsr3/command_line_utilities/map_option_string' +``` + +The module is defined as +`Xqsr3::CommandLineUtilities::MapOptionString`. -## Components -### `CommandLineUtilities::MapOptionString` +## When to use it -Maps a command-line option string into a structured representation. +Use `MapOptionString` when the application has a fixed set of option +spellings and wants the result to be a symbol suitable for dispatch: + +```Ruby +options = ['help', 'version', 'verbose'] +option = 'verbose' + +Xqsr3::CommandLineUtilities::MapOptionString + .map_option_string_from_string(option, options) +# => :verbose +``` + +It is not a complete command-line parser. It does not consume `ARGV`, parse +option arguments, or validate an option's value. + + +## `MapOptionString` + +`map_option_string_from_string(string, option_strings)` returns the symbol +corresponding to the first matching declared option. A declared option +without shortcut syntax maps to itself: ```Ruby require 'xqsr3/command_line_utilities/map_option_string' + +mapper = Xqsr3::CommandLineUtilities::MapOptionString +declared = ['help', 'version', 'dry-run'] + +mapper.map_option_string_from_string('help', declared) +# => :help +mapper.map_option_string_from_string('dry-run', declared) +# => :dry_run +mapper.map_option_string_from_string('unknown', declared) +# => nil +``` + +The canonical option text is converted to a symbol using the String Utilities +symbol rules. Consequently, hyphens become underscores in the returned +symbol. + + +## Option-string grammar + +Place each shortcut character in square brackets within its long spelling: + +```Ruby +declared = ['[h]elp', '[v]ersion', '[d]ry-[r]un'] + +mapper.map_option_string_from_string('h', declared) +# => :help +mapper.map_option_string_from_string('help', declared) +# => :help +mapper.map_option_string_from_string('dr', declared) +# => :dry_run +mapper.map_option_string_from_string('dry-run', declared) +# => :dry_run +``` + +Every bracketed character contributes to the shortcut. Thus `[d]ry-[r]un` +declares `dr`; the unbracketed long form remains `dry-run`. Text between +brackets is retained in the long spelling and removed from the shortcut. + +Shortcut matching is exact. Partial long names and individual unbracketed +characters do not match: + +```Ruby +mapper.map_option_string_from_string('d', declared) +# => nil +mapper.map_option_string_from_string('dry', declared) +# => nil +``` + + +## Standalone and extension forms + +The standalone form is useful when the caller wants explicit dependencies: + +```Ruby +Xqsr3::CommandLineUtilities::MapOptionString + .map_option_string_from_string('v', ['[v]ersion']) +# => :version ``` -The same functionality is available through the `String#map_option_string` -extension. +The extension form adds `String#map_option_string`: + +```Ruby +require 'xqsr3/extensions/string/map_option_string' + +'v'.map_option_string(['[v]ersion']) +# => :version +``` + +The extension also defines `NilClass#map_option_string`, which returns `nil`. +Other receiver types must respond to `to_str`. + + +## Failure and matching behaviour + +An unmatched input returns `nil`; no exception is raised for an ordinary +non-match. Matching is performed in declaration order, so duplicate or +overlapping declarations should be avoided. + +The canonical option string is converted to a symbol only after a match. +Names containing characters that the symbol converter rejects can therefore +produce `nil` even though the option text matched. + +The optional `options` argument is accepted for interface compatibility but +does not currently alter the mapping rules. + +For executable behavioural examples, see +`test/unit/command_line_utilities/tc_map_option_string.rb`. From b4c44686547875647870ef769afcb45ee4b42b93 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 28 Aug 2026 21:44:08 +1000 Subject: [PATCH 13/23] docs(components): document structured IO writing * explain path and writable-stream target behaviour; * document string, array, and hash input forms; * describe line and column separator options; * document line-ending deduction and lookahead limits; * explain final-EOL suppression and return values; * clarify standalone and IO extension loading; * verify examples against the IO unit tests; --- docs/components/io.md | 170 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 163 insertions(+), 7 deletions(-) diff --git a/docs/components/io.md b/docs/components/io.md index 9b130ad..0b25568 100644 --- a/docs/components/io.md +++ b/docs/components/io.md @@ -1,32 +1,188 @@ # xqsr3 IO -The IO components provide small output helpers for Ruby streams. +The IO component writes strings, arrays, and hashes to either a named file or +an existing writable stream. It handles line separators, hash column +separators, and the common case where input already contains line endings. + +The API is a class-level writer: use `Xqsr3::IO.writelines` for the +standalone form or `IO.writelines` after loading the extension. ## Table of Contents - [Loading](#loading) -- [Components](#components) +- [When to use it](#when-to-use-it) +- [Basic writing](#basic-writing) +- [Input forms](#input-forms) +- [Formatting options](#formatting-options) +- [Line-ending deduction](#line-ending-deduction) +- [Targets and return value](#targets-and-return-value) +- [Standalone and extension forms](#standalone-and-extension-forms) ## Loading +Load the standalone component: + +```Ruby +require 'xqsr3/io/writelines' +``` + +Or load the IO extension: + ```Ruby -require 'xqsr3/io' +require 'xqsr3/extensions/io/writelines' ``` -## Components +## When to use it + +Use `writelines` when the data is already structured as records and the +output needs consistent separators. It is more expressive than manually +looping when the records are arrays, hashes, or strings with known line +handling. + +Use a lower-level stream API when output requires per-record state, complex +escaping, buffering control, or a format-specific serializer. + + +## Basic writing + +Write to a path: + +```Ruby +require 'xqsr3/io/writelines' -### `IO#writelines` +Xqsr3::IO.writelines('output.txt', ['first', 'second']) +# creates: +# first +# second +``` -Writes a sequence of lines to an IO object. +Write to an existing stream when the caller owns its lifetime: ```Ruby require 'xqsr3/io/writelines' +require 'stringio' + +output = StringIO.new +Xqsr3::IO.writelines(output, ['first', 'second']) +output.string # => "first\nsecond\n" +``` + +When the target is a path, the file is opened in write mode and therefore +replaced. When the target is a stream, the stream remains open and receives +the output through its `<<` method. + + +## Input forms + +An array writes one converted element per line: + +```Ruby +require 'stringio' + +output = StringIO.new +Xqsr3::IO.writelines(output, [1, :two, false]) +output.string # => "1\ntwo\nfalse\n" +``` + +Each element is converted with `to_s`; no additional leading space is added. + +A hash writes each key/value pair on one line. The default column separator +is the empty string: + +```Ruby +output = StringIO.new +Xqsr3::IO.writelines(output, { 'host' => 'localhost', 'port' => 8080 }) +output.string # => "hostlocalhost\nport8080\n" ``` -The same functionality is exposed through the `IO#writelines` extension. +A string without a newline is treated as one entry. A string containing +newlines is split into entries while preserving a final empty entry, so +existing line endings can be handled without automatically adding another +separator. + + +## Formatting options + +The options hash supports: + +* `line_separator`, which is appended after each output entry; +* `column_separator`, which is inserted between each hash key and value; +* `no_last_eol`, which suppresses the line separator after the final entry; +* `eol_lookahead_limit`, which controls how many entries are inspected when + deducing whether input already contains line endings. + +```Ruby +output = StringIO.new +Xqsr3::IO.writelines( + output, + { 'host' => 'localhost', 'port' => 8080 }, + line_separator: "\r\n", + column_separator: '=', + no_last_eol: true, +) +output.string # => "host=localhost\r\nport=8080" +``` + +The return value is the number of input entries written, not the number of +bytes or characters. For a hash, this is the number of key/value pairs. + + +## Line-ending deduction + +When `line_separator` is omitted, the implementation examines input entries +and defaults to `"\n"`. If an examined key or value already contains `"\n"`, +it uses an empty separator so that an additional line ending is not inserted. + +The default lookahead limit is 20 entries. Set it to `nil` to inspect every +entry, or to `0` to skip inspection and use `"\n"`: + +```Ruby +output = StringIO.new +Xqsr3::IO.writelines( + output, + ["first\n", "second\n"], + line_separator: '|', +) +output.string # => "first\n|second\n|" +``` + +An explicit `line_separator` always wins over deduction. This is useful when +input records deliberately contain their own line endings but a boundary +separator is still required. + + +## Targets and return value + +The target must be a path string or respond to `<<`. Contents must be a +string, hash, or array. Invalid targets and contents fail through parameter +validation before writing begins. + +Use `StringIO` or another writable stream when output should be captured in +memory. Passing a string target always means a filesystem path; a string +cannot act as an in-memory output buffer. + + +## Standalone and extension forms + +The extension adds `IO.writelines` while retaining the same underlying +implementation: + +```Ruby +require 'xqsr3/extensions/io/writelines' + +IO.writelines('output.txt', ['first', 'second']) +``` + +The standalone form avoids modifying the `IO` class and is preferable for a +reusable library that wants explicit dependencies. The extension form is +convenient for application code that already uses xqsr3's standard-library +extensions. + +For executable behavioural examples, see +`test/unit/io/tc_writelines.rb`. From ba01eae274bb6c1e6defdc00e13236d96f732ae5 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 28 Aug 2026 21:47:37 +1000 Subject: [PATCH 14/23] docs(guides): add the xqsr3 getting-started workflow * document gem and Bundler installation; * explain explicit category and component loading; * demonstrate frequency collection with FrequencyMap; * demonstrate boundary conversion and parameter validation; * demonstrate structured output with IO.writelines; * link the guide to the component catalogue; * verify the documented workflow against the library; --- docs/guides/README.md | 3 + docs/guides/getting-started.md | 181 +++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 docs/guides/getting-started.md diff --git a/docs/guides/README.md b/docs/guides/README.md index 759db23..7903e74 100644 --- a/docs/guides/README.md +++ b/docs/guides/README.md @@ -3,6 +3,9 @@ This directory is reserved for task-oriented guides to **xqsr3**. Guides will explain how to combine components to solve common Ruby programming tasks. +* [Getting Started](./getting-started.md) — install the gem and compose + components in a small application workflow; + The component catalogue is available in [`docs/components/`](../components/README.md). diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md new file mode 100644 index 0000000..9f1d135 --- /dev/null +++ b/docs/guides/getting-started.md @@ -0,0 +1,181 @@ +# xqsr3 Getting Started + +This guide takes a small application from installation to its first useful +**xqsr3** components. It uses explicit loading throughout, so each example +makes its dependency on a component visible. + + +## Table of Contents + +- [Install the gem](#install-the-gem) +- [Load only what you need](#load-only-what-you-need) +- [Use a container](#use-a-container) +- [Convert external values](#convert-external-values) +- [Validate a boundary](#validate-a-boundary) +- [Write structured output](#write-structured-output) +- [Choose the next page](#choose-the-next-page) + + +## Install the gem + +Install the released gem: + +```Shell +gem install xqsr3 +``` + +For an application managed with Bundler, add the gem to its `Gemfile`: + +```Ruby +gem 'xqsr3' +``` + +Then run: + +```Shell +bundle install +``` + +**xqsr3** has no runtime dependencies outside the Ruby standard library. + + +## Load only what you need + +Components are not all loaded by a single default `require`. Start with a +category entry point when several related components are needed: + +```Ruby +require 'xqsr3/containers' +require 'xqsr3/conversion' +``` + +Use a component-specific path when the application needs only one feature: + +```Ruby +require 'xqsr3/containers/frequency_map' +``` + +The category entry points expose namespaced components without changing Ruby's +core classes. Extension entry points, such as +`xqsr3/extensions/string`, deliberately add methods to standard Ruby classes. +Use those broader extensions only when that global method syntax is intended. + + +## Use a container + +Suppose an application has received a list of words and wants both the total +number of observations and the most frequent words: + +```Ruby +require 'xqsr3/containers/frequency_map' + +words = %w[ruby ruby crystal ruby crystal] +frequencies = Xqsr3::Containers::FrequencyMap::ByElement[*words] + +frequencies.count # => 5, total observations +frequencies.size # => 2, distinct words + +frequencies.each_by_frequency do |word, frequency| + puts "#{word}: #{frequency}" +end +``` + +`FrequencyMap` is a good fit because each word has one numeric count. Use +`MultiMap` instead when each key needs an ordered array of associated values. +See the [Containers catalogue page](../components/containers.md) for the +choice and mutation rules. + + +## Convert external values + +External values should be converted at their input boundary, where the +application can choose what invalid input means: + +```Ruby +require 'xqsr3/conversion' + +enabled = Xqsr3::Conversion::BoolParser.to_bool( + ENV.fetch('FEATURE_ENABLED', 'false'), + default_value: false, +) + +port = Xqsr3::Conversion::IntegerParser.to_integer( + ENV.fetch('PORT', '8080'), + default: 8080, +) +``` + +The Boolean parser returns its default for an unrecognised token. The integer +parser uses Ruby's conversion rules and returns the specified fallback for +invalid or nil input. Omit the fallback when invalid input should raise. + + +## Validate a boundary + +Use `ParameterChecking` when a value must satisfy both a type and a domain +rule: + +```Ruby +require 'xqsr3/quality/parameter_checking' + +checker = Xqsr3::Quality::ParameterChecking + +port = checker.check_parameter( + port, + 'port', + type: Integer, + values: [1..65_535], +) +``` + +Successful checks return the accepted value, so they can be assigned directly. +By default, invalid types raise `TypeError`; invalid values and ranges raise +`ArgumentError` or `RangeError` according to the value and check. Use +`nothrow: true` when a probe returning `nil` is more useful than an exception. + + +## Write structured output + +Write an array or hash to a path, or pass an existing stream when the caller +needs to retain control of its lifetime: + +```Ruby +require 'xqsr3/io/writelines' + +Xqsr3::IO.writelines( + 'frequencies.txt', + frequencies.to_h, + column_separator: ': ', + no_last_eol: true, +) +``` + +`IO.writelines` returns the number of entries written. A path is opened in +write mode and replaced; a stream is left open. See the [IO catalogue +page](../components/io.md) for line-ending deduction and stream examples. + + +## Choose the next page + +The component catalogue explains individual APIs and their edge cases: + +* [Array Utilities](../components/array-utilities.md) for human-readable + alternatives; +* [Command-line Utilities](../components/command-line-utilities.md) for + option names and shortcuts; +* [Containers](../components/containers.md) for counts and one-to-many data; +* [Conversion](../components/conversion.md) for scalar parsing; +* [Diagnostics](../components/diagnostics.md) for failure context; +* [Extensions](../components/extensions.md) for standard-library methods; +* [Hash Utilities](../components/hash-utilities.md) for transformation and + matching; +* [IO](../components/io.md) for structured output; +* [Quality](../components/quality.md) for validation; +* [String Utilities](../components/string-utilities.md) for string handling. + +The [component catalogue index](../components/README.md) provides the full +list. Task-specific guides will be added here as common workflows are +documented. + + + From a356bab0caea2787da079ad4a0be600244285062 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 28 Aug 2026 21:51:57 +1000 Subject: [PATCH 15/23] docs(guides): add component selection guidance * explain selection by data shape and desired operation; * distinguish standalone and globally modifying extension APIs; * document narrow and broad require scopes; * demonstrate conversion, validation, storage, and output composition; * identify common component mismatches and failure modes; * link the guide to the detailed component catalogue; * verify the composed workflow against the library; --- docs/guides/README.md | 2 + docs/guides/choosing-a-component.md | 183 ++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 docs/guides/choosing-a-component.md diff --git a/docs/guides/README.md b/docs/guides/README.md index 7903e74..587c257 100644 --- a/docs/guides/README.md +++ b/docs/guides/README.md @@ -5,6 +5,8 @@ explain how to combine components to solve common Ruby programming tasks. * [Getting Started](./getting-started.md) — install the gem and compose components in a small application workflow; +* [Choosing a Component](./choosing-a-component.md) — select components and + loading scopes according to the problem being solved; The component catalogue is available in [`docs/components/`](../components/README.md). diff --git a/docs/guides/choosing-a-component.md b/docs/guides/choosing-a-component.md new file mode 100644 index 0000000..1664dfb --- /dev/null +++ b/docs/guides/choosing-a-component.md @@ -0,0 +1,183 @@ +# xqsr3 Choosing a Component + +**xqsr3** is intentionally a collection of small, low-coupling components. +The quickest way to use it is to start with the problem being solved, then +load the narrowest component that provides the needed behaviour. + + +## Table of Contents + +- [Start with the data shape](#start-with-the-data-shape) +- [Start with the operation](#start-with-the-operation) +- [Decide between standalone and extension APIs](#decide-between-standalone-and-extension-apis) +- [Choose a loading scope](#choose-a-loading-scope) +- [Compose components at boundaries](#compose-components-at-boundaries) +- [Avoid common mismatches](#avoid-common-mismatches) +- [Further reading](#further-reading) + + +## Start with the data shape + +Choose the component that matches the information the application already +has: + +* A sequence whose members need counting: use + [`FrequencyMap`](../components/containers.md#frequencymap); +* A key with several ordered values: use + [`MultiMap`](../components/containers.md#multimap); +* A hash whose keys or values need recursive transformation: use + [`DeepTransform`](../components/hash-utilities.md#deeptransform); +* A hash that must be searched by exact or regular-expression key: use + [`KeyMatching`](../components/hash-utilities.md#keymatching); +* A string representing a scalar configuration value: use the + [Conversion](../components/conversion.md) components; +* A list of alternatives intended for a person to read: use + [`join_with_or`](../components/array-utilities.md#joinwithor). + +Prefer the standard Ruby `Array`, `Hash`, or `Enumerable` APIs when the data +does not require one of these additional semantics. + + +## Start with the operation + +If the desired operation is more important than the data structure, use these +shortcuts: + +* Need to count occurrences? Use `FrequencyMap`; +* Need to group one key under many values? Use `MultiMap`; +* Need to parse `true`, `false`, `1`, or custom Boolean tokens? Use + `BoolParser`; +* Need an integer with a base, fallback, or custom recovery? Use + `IntegerParser`; +* Need to reject a value that is the wrong type, empty, or outside a range? + Use `ParameterChecking`; +* Need to add a separator only when text contains whitespace? Use `quote_if`; +* Need to turn an identifier-like string into a symbol? Use `to_symbol`; +* Need to limit output width? Use `truncate`; +* Need to format alternatives with a final “or”? Use `join_with_or`; +* Need to write arrays or hashes as lines? Use `IO.writelines`; +* Need to add context to a failure? Use the + [Diagnostics](../components/diagnostics.md) components; +* Need a method that combines detection and transformation? Use + `Enumerable#detect_map`. + + +## Decide between standalone and extension APIs + +Many xqsr3 facilities have two ways to be used: + +* A standalone module or namespaced class keeps the call site explicit and + avoids changing Ruby core classes; +* An extension method reads naturally on the receiver but changes the method + set of a core class or module for the process. + +Prefer standalone APIs inside reusable gems: + +```Ruby +require 'xqsr3/conversion' +require 'xqsr3/hash_utilities/key_matching' + +enabled = Xqsr3::Conversion::BoolParser.to_bool(value) +host = Xqsr3::HashUtilities::KeyMatching.match(settings, /host/) +``` + +Prefer extension APIs in an application that has explicitly adopted them: + +```Ruby +require 'xqsr3/extensions/string' +require 'xqsr3/extensions/hash/match' + +enabled = value.to_bool +host = settings.match(/host/) +``` + +Do not assume that requiring a standalone utility adds an instance method. +For example, `xqsr3/string_utilities/to_symbol` provides the +`StringUtilities::ToSymbol` module; `String#to_symbol` requires the matching +extension. + + +## Choose a loading scope + +Use the narrowest require path that communicates the dependency: + +* `xqsr3/` loads the standalone components in that category; +* `xqsr3//` loads one standalone component; +* `xqsr3/extensions/` loads extensions for one core class or module; +* `xqsr3/extensions//` loads one extension method; +* `xqsr3/extensions` loads the standard-library extension groups; +* `xqsr3/all_extensions` also loads the `test/unit` extensions. + +The broad extension entry points are convenient but create global method +changes. A reusable library should generally avoid requiring them as a side +effect of its own top-level load. + + +## Compose components at boundaries + +A useful composition order is: + +1. Convert external text into an application value; +2. Validate the converted value and its domain; +3. Store or transform the value using the matching data structure; +4. Format or write the result; +5. Add diagnostic context if a step fails. + +For example, configuration processing can convert and validate a port before +writing a normalized summary: + +```Ruby +require 'xqsr3/conversion' +require 'xqsr3/quality/parameter_checking' +require 'xqsr3/io/writelines' + +port = Xqsr3::Conversion::IntegerParser.to_integer( + ENV.fetch('PORT', '8080'), +) +port = Xqsr3::Quality::ParameterChecking.check_parameter( + port, + 'port', + type: Integer, + values: [1..65_535], +) + +Xqsr3::IO.writelines('effective-config.txt', { port: port }) +``` + +This keeps parsing policy, validation policy, storage, and output concerns +separate. It also makes each policy independently testable. + + +## Avoid common mismatches + +* Do not use `FrequencyMap` when values, rather than counts, must be retained; + use `MultiMap`; +* Do not use `MultiMap` as a set: duplicate values are retained; +* Do not use `BoolParser` as a strict validator without checking its fallback + result; +* Do not use `IntegerParser` with a base expecting non-string numbers to be + reinterpreted; +* Do not use `deep_transform!` when a failed transformation must leave the + original hash untouched; +* Do not treat `Hash#match` returning `nil` as proof that no key matched if a + matching value may itself be `nil`; +* Do not load all extensions merely to use one String or Hash method; +* Do not use `IO.writelines` as a replacement for a serializer that must + escape or encode structured data. + + +## Further reading + +* [Getting Started](./getting-started.md) for a complete first workflow; +* [Component Catalogue](../components/README.md) for category-level detail; +* [Containers](../components/containers.md) for collection choices; +* [Conversion](../components/conversion.md) for scalar parsing; +* [Diagnostics](../components/diagnostics.md) for failure context; +* [Extensions](../components/extensions.md) for loading and global methods; +* [Hash Utilities](../components/hash-utilities.md) for hash operations; +* [IO](../components/io.md) for structured output; +* [Quality](../components/quality.md) for validation; +* [String Utilities](../components/string-utilities.md) for string handling. + + + From f280a9d5a7404b34e50eb46f78f9b8fa3d48b24a Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 28 Aug 2026 21:54:58 +1000 Subject: [PATCH 16/23] docs(guides): document external input processing * separate normalization, conversion, validation, and storage; * document blank-value handling and explicit text normalization; * demonstrate Boolean and integer conversion policies; * document domain validation and failure strategies; * explain raising, fallback, non-throwing, and recovery modes; * provide a complete normalized-configuration workflow; * verify the composed workflow against the library; --- docs/guides/README.md | 2 + docs/guides/parsing-and-validating-input.md | 262 ++++++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 docs/guides/parsing-and-validating-input.md diff --git a/docs/guides/README.md b/docs/guides/README.md index 587c257..da2b6e3 100644 --- a/docs/guides/README.md +++ b/docs/guides/README.md @@ -7,6 +7,8 @@ explain how to combine components to solve common Ruby programming tasks. components in a small application workflow; * [Choosing a Component](./choosing-a-component.md) — select components and loading scopes according to the problem being solved; +* [Parsing and Validating External Input](./parsing-and-validating-input.md) + — separate normalization, conversion, and validation at boundaries; The component catalogue is available in [`docs/components/`](../components/README.md). diff --git a/docs/guides/parsing-and-validating-input.md b/docs/guides/parsing-and-validating-input.md new file mode 100644 index 0000000..a5f0ff6 --- /dev/null +++ b/docs/guides/parsing-and-validating-input.md @@ -0,0 +1,262 @@ +# xqsr3 Parsing and Validating External Input + +External input often arrives as strings, even when the application needs +Booleans, integers, symbols, or constrained values. This guide shows how to +separate normalization, conversion, and validation so each policy is visible +and testable. + + +## Table of Contents + +- [The boundary pipeline](#the-boundary-pipeline) +- [Normalize text](#normalize-text) +- [Convert scalar values](#convert-scalar-values) +- [Validate the result](#validate-the-result) +- [Handle invalid input](#handle-invalid-input) +- [Build a normalized configuration](#build-a-normalized-configuration) +- [Further reading](#further-reading) + + +## The boundary pipeline + +Use this sequence for configuration, environment, and command-line values: + +1. Normalize representation-only differences; +2. Convert text into the application's conceptual type; +3. Validate type and domain constraints; +4. Store the accepted value in application state; +5. Report or recover from invalid input according to the caller's policy. + +Do not use conversion defaults as a substitute for validation. A default can +hide a misspelled or unsupported input unless that is explicitly the desired +policy. + + +## Normalize text + +Use String Utilities when blank input has a defined meaning: + +```Ruby +require 'xqsr3/string_utilities/nil_if_whitespace' + +raw_host = ENV['HOST'] +host = raw_host && Xqsr3::StringUtilities::NilIfWhitespace + .string_nil_if_whitespace(raw_host) +``` + +`nil_if_whitespace` returns `nil` for empty or whitespace-only input and +returns the original non-blank string unchanged. If leading and trailing +whitespace should be removed from an accepted value, do that explicitly: + +```Ruby +host = host.strip if host +``` + +Use `nil_if_empty` instead when whitespace is meaningful: + +```Ruby +require 'xqsr3/string_utilities/nil_if_empty' + +label = Xqsr3::StringUtilities::NilIfEmpty + .string_nil_if_empty(' ') +# => ' ' +``` + +This distinction prevents an input-normalization policy from being hidden +inside a later conversion step. + + +## Convert scalar values + +Convert Booleans using explicit accepted tokens: + +```Ruby +require 'xqsr3/conversion/bool_parser' + +enabled = Xqsr3::Conversion::BoolParser.to_bool( + ENV.fetch('FEATURE_ENABLED', 'false'), +) +``` + +The default Boolean vocabulary is `true`, `TRUE`, `1`, `false`, `FALSE`, and +`0`. Unknown text returns `nil`. Configure a fallback when unknown input +should have a deliberate application value: + +```Ruby +enabled = Xqsr3::Conversion::BoolParser.to_bool( + ENV['FEATURE_ENABLED'], + default_value: false, +) +``` + +Convert integers with an optional numeric base and failure policy: + +```Ruby +require 'xqsr3/conversion/integer_parser' + +port = Xqsr3::Conversion::IntegerParser.to_integer( + ENV.fetch('PORT', '8080'), +) +``` + +The base applies to string input. Omit `default` and `nil: true` when +malformed input must raise. Use `default` for a specific fallback, or +`nil: true` when all conversion failures should become `nil`. + + +## Validate the result + +Conversion answers “what value does this text represent?” Validation answers +“is that value permitted here?” Keep those questions separate: + +```Ruby +require 'xqsr3/quality/parameter_checking' + +port = Xqsr3::Quality::ParameterChecking.check_parameter( + port, + 'port', + type: Integer, + values: [1..65_535], +) +``` + +The check returns the accepted value. A non-integer raises `TypeError`; an +integer outside the permitted range raises `RangeError`. + +Validate a finite vocabulary directly: + +```Ruby +format = Xqsr3::Quality::ParameterChecking.check_parameter( + 'JSON', + 'format', + type: String, + values: ['json', 'yaml'], + ignore_case: true, +) +# => 'JSON' +``` + +The accepted value is returned unchanged. If the application wants a +canonical lowercase value, normalize that result explicitly: + +```Ruby +format = format.downcase +# => 'json' +``` + + +## Handle invalid input + +Choose a failure policy at the boundary rather than deep inside the +application: + +* Raise when invalid input indicates a configuration or programming error; +* Return `nil` when the caller is probing optional input; +* Supply a named default when the fallback is part of the documented + application behaviour; +* Use a recovery block when diagnostics or a custom sentinel are required. + +For a non-raising validation probe: + +```Ruby +valid_port = Xqsr3::Quality::ParameterChecking.check_parameter( + candidate, + 'port', + type: Integer, + values: [1..65_535], + nothrow: true, +) + +if valid_port + puts "using port #{valid_port}" +else + puts 'port was not accepted' +end +``` + +Be aware that `nothrow: true` uses `nil` as the failure signal. Do not use it +when `nil` is also a valid accepted value without adding a separate +presence check. + +For conversion diagnostics: + +```Ruby +port = Xqsr3::Conversion::IntegerParser.to_integer( + candidate, +) do |exception, argument, base, options| + warn "invalid port #{argument.inspect}: #{exception.message}" + nil +end +``` + +The recovery block receives the original argument and conversion context. +Exceptions raised inside the block are not silently discarded. + + +## Build a normalized configuration + +The following function combines the stages while retaining a simple +application-level result: + +```Ruby +require 'xqsr3/conversion' +require 'xqsr3/quality/parameter_checking' +require 'xqsr3/string_utilities/nil_if_whitespace' + +def load_configuration(environment) + checker = Xqsr3::Quality::ParameterChecking + string_utilities = Xqsr3::StringUtilities + + raw_host = environment.fetch('HOST', 'localhost') + host = string_utilities::NilIfWhitespace + .string_nil_if_whitespace(raw_host) + host = checker.check_parameter( + host, + 'host', + type: String, + reject_empty: true, + ) + + port = Xqsr3::Conversion::IntegerParser.to_integer( + environment.fetch('PORT', '8080'), + ) + port = checker.check_parameter( + port, + 'port', + type: Integer, + values: [1..65_535], + ) + + enabled = Xqsr3::Conversion::BoolParser.to_bool( + environment.fetch('ENABLED', 'false'), + ) + enabled = checker.check_parameter( + enabled, + 'enabled', + types: [:boolean], + ) + + { + enabled: enabled, + host: host, + port: port, + } +end +``` + +This function rejects a blank host, an invalid port, and an unrecognised +Boolean token. It returns values in application-ready types rather than +leaving every downstream caller to repeat conversion logic. + + +## Further reading + +* [Getting Started](./getting-started.md) for a first complete workflow; +* [Choosing a Component](./choosing-a-component.md) for selection guidance; +* [Conversion](../components/conversion.md) for parser policies; +* [Quality](../components/quality.md) for validation options; +* [String Utilities](../components/string-utilities.md) for normalization; +* [Diagnostics](../components/diagnostics.md) for failure context. + + + From cddffd7783011c8abd09bab48b8fc261ebd4d227 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 28 Aug 2026 22:01:03 +1000 Subject: [PATCH 17/23] docs(reference): establish the generated API reference * complete the RDoc namespace index for core components; * add missing Containers and Conversion cross-references; * correct Hash Utilities namespace documentation; * document generated doc/ output and the RDoc workflow; * distinguish authored guides from generated API reference material; * verify RDoc generation and document its coverage result; --- docs/reference/README.md | 37 +++++++++++++++++++++++++++++++++++++ lib/xqsr3/doc_.rb | 23 +++++++++++++++++++---- 2 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 docs/reference/README.md diff --git a/docs/reference/README.md b/docs/reference/README.md new file mode 100644 index 0000000..0abe7ac --- /dev/null +++ b/docs/reference/README.md @@ -0,0 +1,37 @@ +# xqsr3 Generated API Reference + +The generated API reference is produced by RDoc from the Ruby source +documentation comments. It complements the authored guides and component +catalogue: + +* [`docs/components/`](../components/README.md) explains component selection + and behaviour; +* [`docs/guides/`](../guides/README.md) explains task-oriented workflows; +* generated `doc/` explains the complete public Ruby API. + + +## Generate the reference + +From the project root, run: + +```Shell +./generate_rdoc.sh +``` + +The script removes any previous generated output and writes the new reference +to `doc/`. The generated files are build output and should not be edited by +hand. + + +## Reading the reference + +Use the generated namespace and method pages for exact signatures and +source-level API details. Start with the authored documentation when deciding +which component to use, then use RDoc to inspect the complete method surface. + +The RDoc index is anchored by **lib/xqsr3/doc_.rb**, which provides the +cross-component namespace overview. Public implementation comments remain the +authoritative source for signatures, options, and exceptions. + + + diff --git a/lib/xqsr3/doc_.rb b/lib/xqsr3/doc_.rb index b3f8685..6a1b677 100644 --- a/lib/xqsr3/doc_.rb +++ b/lib/xqsr3/doc_.rb @@ -5,7 +5,7 @@ # Purpose: Documentation of the ::Xqsr3 modules # # Created: 10th June 2016 -# Updated: 19th August 2026 +# Updated: 28th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # @@ -75,11 +75,19 @@ module CommandLineUtilities # Containers # + # === Subordinate modules of interest + # * ::Xqsr3::Containers::FrequencyMap + # * ::Xqsr3::Containers::MultiMap + # module Containers end # module Containers # Conversion # + # === Subordinate modules of interest + # * ::Xqsr3::Conversion::BoolParser + # * ::Xqsr3::Conversion::IntegerParser + # module Conversion end # module Conversion @@ -121,14 +129,21 @@ module Exceptions # * ::Xqsr3::HashUtilities::KeyMatching module HashUtilities - # Exception-related utilities + # Deep hash transformation # # === Components of interest - # * ::Xqsr3::Diagnostics::HashUtilities::deep_transform - # * ::Xqsr3::Diagnostics::HashUtilities::deep_transform! + # * ::Xqsr3::HashUtilities::DeepTransform # module DeepTransform end # module DeepTransform + + # Hash key matching + # + # === Components of interest + # * ::Xqsr3::HashUtilities::KeyMatching + # + module KeyMatching + end # module KeyMatching end # module HashUtilities # IO From 77bf0147d6f848fba4e79932c5fe0eea1c5a4a1d Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 28 Aug 2026 22:05:46 +1000 Subject: [PATCH 18/23] docs(reference): complete generated API documentation coverage * document the Integer extension class; * document the compatibility Hash#slice method; * document NilClass#map_option_string; * exclude authored guides and examples from API extraction; * update RDoc generation metadata; * achieve complete coverage across 197 API entities; --- generate_rdoc.sh | 4 +++- lib/xqsr3/extensions/hash/slice.rb | 2 ++ lib/xqsr3/extensions/integer/to_s_grp.rb | 3 ++- lib/xqsr3/extensions/string/map_option_string.rb | 2 ++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/generate_rdoc.sh b/generate_rdoc.sh index e7dc865..2293738 100755 --- a/generate_rdoc.sh +++ b/generate_rdoc.sh @@ -6,7 +6,7 @@ # Purpose: Generates documentation # # Created: 11th June 2016 -# Updated: 14th August 2026 +# Updated: 28th August 2026 # ############################################################################# @@ -20,6 +20,8 @@ rdoc \ -x *.gemspec \ \ -x doc/ \ + -x docs/ \ + -x examples/ \ -x gems/ \ -x old-gems/ \ -x test/performance/ \ diff --git a/lib/xqsr3/extensions/hash/slice.rb b/lib/xqsr3/extensions/hash/slice.rb index df34b35..e08c429 100644 --- a/lib/xqsr3/extensions/hash/slice.rb +++ b/lib/xqsr3/extensions/hash/slice.rb @@ -2,8 +2,10 @@ unless Hash.instance_methods.include? :slice + # Standard Ruby Hash extended with #slice when unavailable. class Hash + # Returns a new hash containing only the requested existing keys. def slice(*args) r = {} diff --git a/lib/xqsr3/extensions/integer/to_s_grp.rb b/lib/xqsr3/extensions/integer/to_s_grp.rb index f99a243..fcbcb97 100644 --- a/lib/xqsr3/extensions/integer/to_s_grp.rb +++ b/lib/xqsr3/extensions/integer/to_s_grp.rb @@ -5,7 +5,7 @@ # Purpose: Adds a to_s_grp() method to the Integer class # # Created: 29th March 2024 -# Updated: 19th August 2026 +# Updated: 28th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # @@ -47,6 +47,7 @@ =begin =end +# Standard Ruby Integer extended with #to_s_grp. class Integer # Extends +Integer+ type with the +#to_s_grp()+ method diff --git a/lib/xqsr3/extensions/string/map_option_string.rb b/lib/xqsr3/extensions/string/map_option_string.rb index e0dbd95..d04fd45 100644 --- a/lib/xqsr3/extensions/string/map_option_string.rb +++ b/lib/xqsr3/extensions/string/map_option_string.rb @@ -8,8 +8,10 @@ class String include ::Xqsr3::CommandLineUtilities::MapOptionString end # class String +# Standard NilClass extension for safely mapping an absent option string. class NilClass + # Returns nil because a nil option string cannot match a declared option. def map_option_string *args nil From 9411181056c647cc98184c5731c87ad7fe4c608e Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 28 Aug 2026 22:07:13 +1000 Subject: [PATCH 19/23] ci(docs): verify generated API reference coverage * add a dedicated documentation workflow job; * run RDoc coverage under Ruby 3.4 on Ubuntu; * fail CI when undocumented API entities are introduced; * preserve the existing test and warning jobs; * validate the workflow YAML locally; --- .github/workflows/ruby.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index a641a80..0e7ae88 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -18,6 +18,8 @@ on: - master - dev - boilerplate + - doc + - doc.3 - idiomatic - rc1 - rc2 @@ -32,6 +34,26 @@ defaults: shell: bash jobs: + + documentation: + + name: Documentation + + runs-on: ubuntu-latest + + steps: + - name: Checking out code + uses: actions/checkout@v7 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.4' + bundler-cache: false + + - name: Generate RDoc coverage report + run: ./generate_rdoc.sh -C + test: strategy: From 4c5ce26a3a80d71ab1fd0f28c04a6973a504794e Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 28 Aug 2026 22:10:39 +1000 Subject: [PATCH 20/23] docs(reference): exclude the private BoolParser helper * mark the private matches_to_ helper as no-doc; * remove implementation-detail prose from the API documentation; * preserve the helper as an internal conversion detail; * restore complete RDoc coverage at 100 percent; * verify all 197 documentable API entities; --- lib/xqsr3/conversion/bool_parser.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/xqsr3/conversion/bool_parser.rb b/lib/xqsr3/conversion/bool_parser.rb index e71d0a2..9505b0c 100644 --- a/lib/xqsr3/conversion/bool_parser.rb +++ b/lib/xqsr3/conversion/bool_parser.rb @@ -5,7 +5,7 @@ # Purpose: Definition of the ::Xqsr3::Conversion::BoolParser module # # Created: 3rd June 2017 -# Updated: 19th August 2026 +# Updated: 28th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # @@ -55,7 +55,7 @@ module Conversion module BoolParser private - def self.matches_to_ s, expr + def self.matches_to_ s, expr # :nodoc: case expr when ::Regexp From 233a2f2ca7d091e86e9a144d1223e75ba1043750 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 28 Aug 2026 22:21:15 +1000 Subject: [PATCH 21/23] docs(xqsr3): complete task-oriented workflow documentation - add collection processing workflow guidance; - add failure handling and diagnostic workflow guidance; - add formatting and output writing workflow guidance; - update the guide index and mark Option 2 complete; - update the documentation roadmap status; --- TODO.md | 2 +- docs/guides/README.md | 14 +- docs/guides/formatting-and-writing-output.md | 182 +++++++++++++++++ docs/guides/handling-failures.md | 157 +++++++++++++++ docs/guides/processing-collections.md | 195 +++++++++++++++++++ 5 files changed, 545 insertions(+), 5 deletions(-) create mode 100644 docs/guides/formatting-and-writing-output.md create mode 100644 docs/guides/handling-failures.md create mode 100644 docs/guides/processing-collections.md diff --git a/TODO.md b/TODO.md index 99e0f89..8551a09 100644 --- a/TODO.md +++ b/TODO.md @@ -10,7 +10,7 @@ ## Documentation * [ ] Curated component catalogue under **docs/components/**; -* [ ] Task-oriented user guide under **docs/guides/**; +* [x] ~~~task-oriented user guide under **docs/guides/**~~~; * [ ] Expanded generated API reference; * [ ] Executable cookbook and recipe documentation; * [ ] Static documentation website with search and versioned references; diff --git a/docs/guides/README.md b/docs/guides/README.md index da2b6e3..27b5c48 100644 --- a/docs/guides/README.md +++ b/docs/guides/README.md @@ -1,14 +1,20 @@ # xqsr3 User Guides -This directory is reserved for task-oriented guides to **xqsr3**. Guides will -explain how to combine components to solve common Ruby programming tasks. +This directory contains task-oriented guides to **xqsr3**. The guides explain +how to combine components to solve common Ruby programming tasks. -* [Getting Started](./getting-started.md) — install the gem and compose - components in a small application workflow; * [Choosing a Component](./choosing-a-component.md) — select components and loading scopes according to the problem being solved; +* [Formatting and Writing Output](./formatting-and-writing-output.md) — + format human-readable values and write them to paths or streams; +* [Getting Started](./getting-started.md) — install the gem and compose + components in a small application workflow; +* [Handling Failures](./handling-failures.md) — validate boundaries, preserve + causes, and produce safe diagnostics; * [Parsing and Validating External Input](./parsing-and-validating-input.md) — separate normalization, conversion, and validation at boundaries; +* [Processing Collections](./processing-collections.md) — count, group, + transform, and search collection data; The component catalogue is available in [`docs/components/`](../components/README.md). diff --git a/docs/guides/formatting-and-writing-output.md b/docs/guides/formatting-and-writing-output.md new file mode 100644 index 0000000..8bd9e18 --- /dev/null +++ b/docs/guides/formatting-and-writing-output.md @@ -0,0 +1,182 @@ +# xqsr3 Formatting and Writing Output + +This guide shows how to turn application values into readable text and write +that text to a file or stream. It separates presentation choices from output +ownership so the same data can be displayed, tested, or persisted. + + +## Table of Contents + +- [Format alternatives](#format-alternatives) +- [Prepare strings](#prepare-strings) +- [Limit display width](#limit-display-width) +- [Write sequences](#write-sequences) +- [Write key-value data](#write-key-value-data) +- [Choose a stream or path](#choose-a-stream-or-path) +- [Compose a report](#compose-a-report) + + +## Format alternatives + +Use `join_with_or` when text describes permitted alternatives: + +```Ruby +require 'xqsr3/array_utilities/join_with_or' + +formats = ['JSON', 'YAML', 'TOML'] +message = Xqsr3::ArrayUtilities::JoinWithOr.join_with_or(formats) +# => 'JSON, YAML, or TOML' +``` + +Configure the conjunction, separator, Oxford comma, or quote character when +the surrounding presentation requires a different style: + +```Ruby +Xqsr3::ArrayUtilities::JoinWithOr.join_with_or( + formats, + or: 'OR', + quote_char: '"', + oxford_comma: false, +) +# => '"JSON", "YAML" OR "TOML"' +``` + +This formatter is for human-readable messages, not machine-readable +serialization. + + +## Prepare strings + +Use `quote_if` when values should be quoted only if they contain whitespace: + +```Ruby +require 'xqsr3/string_utilities/quote_if' + +formatter = Xqsr3::StringUtilities::QuoteIf +formatter.quote_if('simple') # => 'simple' +formatter.quote_if('two words') # => '"two words"' +``` + +The `quotables` option can replace whitespace with another trigger. Use +`quotes` when the opening and closing delimiters differ. + +Use `nil_if_whitespace` to identify absent human-entered values, and +`nil_if_empty` when whitespace should remain meaningful. Neither method strips +a non-blank value; call `strip` explicitly when that is wanted. + + +## Limit display width + +Use `truncate` for labels, logs, and fixed-width displays: + +```Ruby +require 'xqsr3/string_utilities/truncate' + +truncator = Xqsr3::StringUtilities::Truncate +truncator.string_truncate('a very long filename.txt', 16) +# => 'a very long f...' +``` + +The omission string is included within the requested width: + +```Ruby +truncator.string_truncate( + 'a very long filename.txt', + 16, + omission: ' [...]', +) +# => 'a very lon [...]' +``` + +Choose an omission marker whose meaning is clear in the target interface. +Truncation is based on the string width used by the implementation and does +not provide escaping or format-specific encoding. + + +## Write sequences + +Use `IO.writelines` for an array whose members should become separate output +lines: + +```Ruby +require 'xqsr3/io/writelines' +require 'stringio' + +output = StringIO.new +Xqsr3::IO.writelines(output, ['first', 'second']) +output.string # => "first\nsecond\n" +``` + +The method converts array elements with `to_s`, appends a line separator, and +returns the number of entries written. Pass `line_separator: ''` when the +records already contain all required separators. + + +## Write key-value data + +Pass a hash when each output line consists of a key and value: + +```Ruby +output = StringIO.new +Xqsr3::IO.writelines( + output, + { 'host' => 'localhost', 'port' => 8080 }, + column_separator: '=', + line_separator: "\n", + no_last_eol: true, +) +output.string # => "host=localhost\nport=8080" +``` + +Hash keys and values are converted with `to_s`. Use a serializer instead when +values require escaping, nesting, or a machine-readable format. + + +## Choose a stream or path + +Use a path when xqsr3 should open and replace the file: + +```Ruby +Xqsr3::IO.writelines('report.txt', ['first', 'second']) +``` + +Use a writable stream when the caller owns the destination: + +```Ruby +File.open('report.txt', 'w') do |file| + Xqsr3::IO.writelines(file, ['first', 'second']) +end +``` + +The path form opens in write mode. The stream form leaves the stream open. +`StringIO` is useful for unit tests because it makes output directly +assertable without filesystem access. + + +## Compose a report + +Keep formatting and writing separate when output may have several consumers: + +```Ruby +require 'xqsr3/array_utilities/join_with_or' +require 'xqsr3/io/writelines' + +supported = ['JSON', 'YAML', 'TOML'] +summary = Xqsr3::ArrayUtilities::JoinWithOr.join_with_or(supported) + +Xqsr3::IO.writelines( + 'supported-formats.txt', + { 'formats' => summary }, + column_separator: ': ', +) +``` + +This keeps the choice of human-readable wording independent from the choice +of destination. For parsing and validation before formatting, see +[Parsing and Validating External Input](./parsing-and-validating-input.md). +For component-level details, see [Array Utilities](../components/array-utilities.md), +[IO](../components/io.md), and +[String Utilities](../components/string-utilities.md). + + + diff --git a/docs/guides/handling-failures.md b/docs/guides/handling-failures.md new file mode 100644 index 0000000..600703e --- /dev/null +++ b/docs/guides/handling-failures.md @@ -0,0 +1,157 @@ +# xqsr3 Handling Failures + +This guide shows how to make failures informative without coupling every +caller to the same recovery policy. It combines validation, option-aware +exceptions, inspection, and cause chaining. + + +## Table of Contents + +- [Reject invalid input early](#reject-invalid-input-early) +- [Attach structured context](#attach-structured-context) +- [Preserve the original cause](#preserve-the-original-cause) +- [Produce safe diagnostics](#produce-safe-diagnostics) +- [Choose a recovery boundary](#choose-a-recovery-boundary) + + +## Reject invalid input early + +Validate values at the public boundary where their names and intended domain +are known: + +```Ruby +require 'xqsr3/quality/parameter_checking' + +def connect(port) + port = Xqsr3::Quality::ParameterChecking.check_parameter( + port, + 'port', + type: Integer, + values: [1..65_535], + ) + + # connect using the validated port +end +``` + +This produces a failure close to the invalid input instead of allowing an +unrelated lower-level operation to report a less useful error. Use +`nothrow: true` only when the caller genuinely needs a validation probe. + + +## Attach structured context + +When an exception class accepts keyword options, use +`ExceptionUtilities.raise_with_options` to retain machine-readable context: + +```Ruby +require 'xqsr3/diagnostics/exception_utilities' + +class ConfigurationError < ArgumentError + attr_reader :options + + def initialize(message = nil, **options) + super(message) + @options = options + end +end + +Xqsr3::Diagnostics::ExceptionUtilities.raise_with_options( + ConfigurationError, + 'invalid port', + option: :port, + value: 'abc', +) +``` + +Keep the human-readable message concise and put structured values in options. +Do not place credentials, tokens, or other secrets into either messages or +options if the exception may be logged. + + +## Preserve the original cause + +Use `WithCause` when a higher-level operation needs to add context while +retaining the lower-level exception: + +```Ruby +require 'xqsr3/diagnostics/exceptions/with_cause' + +class ImportError < StandardError + include Xqsr3::Diagnostics::Exceptions::WithCause +end + +begin + Integer('not-a-number') +rescue ArgumentError => cause + raise ImportError.new('could not import configuration', cause: cause) +end +``` + +At the recovery boundary, the caller can inspect both levels: + +```Ruby +rescue ImportError => error + warn error.chained_message + warn error.cause.class +end +``` + +`chained_message` combines messages from outer to inner exception. Pass +`separator` when another display format is required. `exceptions` returns the +whole chain; `chainees` returns every cause except the receiver. + +Use the explicit `cause:` keyword when constructing a chain. This is clearer +than relying on positional exception inference and documents which exception +is intentionally being retained. + + +## Produce safe diagnostics + +Use `InspectBuilder` when an object needs a stable diagnostic representation: + +```Ruby +require 'xqsr3/diagnostics/inspect_builder' + +class Request + include Xqsr3::Diagnostics::InspectBuilder + + INSPECT_HIDDEN_FIELDS = ['token'] + + def initialize(path, token) + @path = path + @token = token + end +end + +request = Request.new('/health', 'secret') +request.make_inspect(show_fields: true, no_object_id: true) +# => "#" +``` + +Use `shown_fields` for a positive allow-list when the object contains many +fields. A diagnostic inspection string is for human troubleshooting; it is +not a wire format or a persistence representation. + + +## Choose a recovery boundary + +Apply recovery at the layer that has enough context to make a decision: + +* A parser can return a default or `nil` for expected malformed input; +* A validator can raise a precise parameter or range failure; +* A component can wrap a lower-level error with `WithCause`; +* An application boundary can log, retry, or present the failure; +* A library should generally preserve the failure rather than silently log or + discard it. + +Avoid catching `Exception` broadly. Catch the specific failures the operation +can reasonably recover from, and preserve the original exception when adding +context. + +For component-level details, see [Quality](../components/quality.md), +[Diagnostics](../components/diagnostics.md), and +[Conversion](../components/conversion.md). + + + diff --git a/docs/guides/processing-collections.md b/docs/guides/processing-collections.md new file mode 100644 index 0000000..d649899 --- /dev/null +++ b/docs/guides/processing-collections.md @@ -0,0 +1,195 @@ +# xqsr3 Processing Collections + +This guide shows how to choose and combine xqsr3 collection helpers when +processing sequences, grouped values, and nested hashes. + + +## Table of Contents + +- [Count observations](#count-observations) +- [Group values by key](#group-values-by-key) +- [Find and transform an entry](#find-and-transform-an-entry) +- [Remove duplicates](#remove-duplicates) +- [Transform nested hashes](#transform-nested-hashes) +- [Match dynamic hash keys](#match-dynamic-hash-keys) +- [Choose the smallest abstraction](#choose-the-smallest-abstraction) + + +## Count observations + +Use `FrequencyMap` when every observed item contributes to one numeric count: + +```Ruby +require 'xqsr3/containers/frequency_map' + +events = %w[read write read delete read] +frequencies = Xqsr3::Containers::FrequencyMap::ByElement[*events] + +frequencies['read'] # => 3 +frequencies.count # => 5 +frequencies.size # => 3 +``` + +Use `each_by_frequency` when output should be ordered by descending count: + +```Ruby +frequencies.each_by_frequency do |event, count| + puts "#{event}: #{count}" +end +``` + +The map retains the count for each distinct key. `count` is the total number +of observations; `size` is the number of keys. + + +## Group values by key + +Use `MultiMap` when one key must retain several values: + +```Ruby +require 'xqsr3/containers/multi_map' + +files = Xqsr3::Containers::MultiMap.new +files.push :ruby, 'app.rb', 'config.rb' +files.push :test, 'app_test.rb' + +files[:ruby] # => ['app.rb', 'config.rb'] +files.size # => 2 +files.count # => 3 +``` + +`push` appends values, while `store` replaces the complete value array: + +```Ruby +files.store :ruby, 'main.rb' +files[:ruby] # => ['main.rb'] +``` + +An existing key may have an empty value array. This differs from an absent +key, for which indexing returns `nil`. + + +## Find and transform an entry + +Use `Enumerable#detect_map` when the first matching value should immediately +be transformed and returned: + +```Ruby +require 'xqsr3/extensions/enumerable/detect_map' + +records = [ + { name: 'Ada', active: false }, + { name: 'Grace', active: true }, +] + +records.detect_map do |record| + record[:name].upcase if record[:active] +end +# => 'GRACE' +``` + +Only `nil` means “continue searching”; `false` and `0` are valid successful +results. For hashes, use a two-argument block receiving the key and value. + + +## Remove duplicates + +Use `Enumerable#unique` when the first occurrence should be retained: + +```Ruby +require 'xqsr3/extensions/enumerable/unique' + +values = [1, 2, 1, 3, 2] +values.unique +# => [1, 2, 3] +``` + +Without a block, uniqueness follows Ruby hash equality. With a two-argument +block, compare each candidate with the values already retained: + +```Ruby +['Ada', 'ada', 'Grace'].unique do |kept, candidate| + kept.downcase == candidate.downcase +end +# => ['Ada', 'Grace'] +``` + +The first occurrence is retained and encounter order is preserved. +Comparator mode performs pairwise comparisons and is therefore less efficient +for large collections. + + +## Transform nested hashes + +Use `Hash#deep_transform` when keys or key/value pairs must be transformed +through nested hashes: + +```Ruby +require 'xqsr3/extensions/hash/deep_transform' + +source = { + user: { + display_name: 'Ada', + }, +} + +source.deep_transform { |key| key.to_s } +# => {'user' => {'display_name' => 'Ada'}} +``` + +Use a two-argument block to transform values as well: + +```Ruby +source.deep_transform do |key, value| + [key.to_s, value.is_a?(String) ? value.strip : value] +end +# => {'user' => {'display_name' => 'Ada'}} +``` + +The non-bang form returns a transformed copy. Use `deep_transform!` only when +in-place mutation is intended; a failed transformation can leave the +receiver partially transformed. + + +## Match dynamic hash keys + +Use `Hash#has_match?` when only the presence of a matching key matters, and +`Hash#match` when its value is needed: + +```Ruby +require 'xqsr3/extensions/hash/has_match' +require 'xqsr3/extensions/hash/match' + +settings = { + 'service.host' => 'localhost', + 'service.port' => 8080, +} + +settings.has_match?(/service\./) # => true +settings.match(/service\.port/) # => 8080 +``` + +Exact key matches take precedence. Matching a regular expression against +several keys follows hash iteration order. If a matching value may be `nil`, +use `has_match?` before interpreting `match`'s result. + + +## Choose the smallest abstraction + +Use ordinary Ruby collection methods when they already express the operation +clearly. Add an xqsr3 component when it provides a meaningful additional +contract: + +* Count observations with `FrequencyMap`; +* retain one-to-many values with `MultiMap`; +* combine detection and transformation with `detect_map`; +* retain first occurrences with `unique`; +* transform nested hashes with `deep_transform`; +* search dynamic keys with `match` or `has_match?`. + +For more detail, see the [Containers](../components/containers.md), +[Hash Utilities](../components/hash-utilities.md), and +[Extensions](../components/extensions.md) component pages. + + + From d26271d760d991389909e1c5d7a69545c06bf1fb Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Fri, 28 Aug 2026 22:26:03 +1000 Subject: [PATCH 22/23] docs(xqsr3): complete curated component catalogue - confirm coverage of all ten public component categories; - explain standalone, extension, and broad-scope loading choices; - cross-link catalogue entries to task-oriented workflows; - mark the curated component catalogue complete; --- TODO.md | 2 +- docs/components/README.md | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index 8551a09..8035306 100644 --- a/TODO.md +++ b/TODO.md @@ -9,7 +9,7 @@ ## Documentation -* [ ] Curated component catalogue under **docs/components/**; +* [x] ~~~curated component catalogue under **docs/components/**~~~; * [x] ~~~task-oriented user guide under **docs/guides/**~~~; * [ ] Expanded generated API reference; * [ ] Executable cookbook and recipe documentation; diff --git a/docs/components/README.md b/docs/components/README.md index a5a2039..a88e9dd 100644 --- a/docs/components/README.md +++ b/docs/components/README.md @@ -9,6 +9,7 @@ file. - [Using the catalogue](#using-the-catalogue) - [Categories](#categories) +- [Related workflows](#related-workflows) ## Using the catalogue @@ -19,6 +20,11 @@ Components are loaded explicitly. Each category page documents the relevant The catalogue documents the supported public surface. Internal files and implementation details are intentionally excluded. +The ten category pages correspond to the public top-level component entry +points under `lib/xqsr3/`. The `Extensions` page additionally covers the +opt-in monkey patches, while `all_extensions.rb` is documented as its +broad-scope loading choice. + ## Categories @@ -34,4 +40,22 @@ implementation details are intentionally excluded. * [String Utilities](./string-utilities.md); +## Related workflows + +Use the catalogue alongside the task-oriented guides: + +* [Choosing a Component](../guides/choosing-a-component.md) explains how to + select a category and loading scope; +* [Getting Started](../guides/getting-started.md) demonstrates a minimal + application workflow; +* [Parsing and Validating External Input](../guides/parsing-and-validating-input.md) + combines string, conversion, and quality components; +* [Processing Collections](../guides/processing-collections.md) combines + containers, hash utilities, and extensions; +* [Handling Failures](../guides/handling-failures.md) combines quality and + diagnostics components; +* [Formatting and Writing Output](../guides/formatting-and-writing-output.md) + combines array, IO, and string utilities. + + From a8a46e02c15db4d8486c6f9f7620fd4994d880de Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Sat, 29 Aug 2026 14:27:10 +1000 Subject: [PATCH 23/23] squash-commit --- .github/workflows/ruby.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 0e7ae88..a009ab3 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -7,7 +7,7 @@ # # Created: 29th August 2025 -# Updated: 28th August 2026 +# Updated: 29th August 2026 # name: Ruby @@ -19,7 +19,7 @@ on: - dev - boilerplate - doc - - doc.3 + - doc.1 - idiomatic - rc1 - rc2