From 0bdb699b424f3f0ada1e3318df845c6688b8ba74 Mon Sep 17 00:00:00 2001 From: Matt Wilson <152443343+mwsis@users.noreply.github.com> Date: Sat, 29 Aug 2026 14:12:49 +1000 Subject: [PATCH 01/17] Docs (0) (#30) * squash-commit * chore(doc): update documentation metadata for 0.39.11 * 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; * 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): 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): 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): 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): 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): 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): 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): 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): 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): 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(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): 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): 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; --- .github/workflows/ruby.yml | 3 +- CHANGES.md | 11 + NEWS.md | 1 + README.md | 38 +-- TODO.md | 9 + docs/components/README.md | 37 +++ docs/components/array-utilities.md | 120 ++++++++ docs/components/command-line-utilities.md | 151 ++++++++++ docs/components/containers.md | 143 ++++++++++ docs/components/conversion.md | 166 +++++++++++ docs/components/diagnostics.md | 210 ++++++++++++++ docs/components/extensions.md | 232 ++++++++++++++++ docs/components/hash-utilities.md | 192 +++++++++++++ docs/components/io.md | 188 +++++++++++++ docs/components/quality.md | 251 +++++++++++++++++ docs/components/string-utilities.md | 218 +++++++++++++++ docs/guides/README.md | 17 ++ docs/guides/choosing-a-component.md | 183 ++++++++++++ docs/guides/getting-started.md | 181 ++++++++++++ docs/guides/parsing-and-validating-input.md | 262 ++++++++++++++++++ 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 +- xqsr3.gemspec | 1 + 26 files changed, 2681 insertions(+), 101 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 create mode 100644 docs/guides/choosing-a-component.md create mode 100644 docs/guides/getting-started.md create mode 100644 docs/guides/parsing-and-validating-input.md 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/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/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..1135be7 --- /dev/null +++ b/docs/components/array-utilities.md @@ -0,0 +1,120 @@ +# 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) +- [When to use it](#when-to-use-it) +- [JoinWithOr](#joinwithor) +- [Formatting options](#formatting-options) +- [Standalone and extension forms](#standalone-and-extension-forms) + + +## Loading + +```Ruby +require 'xqsr3/array_utilities' +``` + +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. + + +## `JoinWithOr` + +`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 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`. + + + diff --git a/docs/components/command-line-utilities.md b/docs/components/command-line-utilities.md new file mode 100644 index 0000000..1ff7a07 --- /dev/null +++ b/docs/components/command-line-utilities.md @@ -0,0 +1,151 @@ +# xqsr3 Command-line Utilities + +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) +- [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`. + + +## When to use it + +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 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`. + + + diff --git a/docs/components/containers.md b/docs/components/containers.md new file mode 100644 index 0000000..6d55ea6 --- /dev/null +++ b/docs/components/containers.md @@ -0,0 +1,143 @@ +# xqsr3 Containers + +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) +- [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' +``` + +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`. + + +## Choosing a container + +* 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. + + +## `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 +``` + +`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`. + +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/`. + + + diff --git a/docs/components/conversion.md b/docs/components/conversion.md new file mode 100644 index 0000000..4a3863b --- /dev/null +++ b/docs/components/conversion.md @@ -0,0 +1,166 @@ +# xqsr3 Conversion + +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) +- [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`. + + +## Choosing a parser + +* 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. + + +## `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 +``` + +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: + +* `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/`. + + + diff --git a/docs/components/diagnostics.md b/docs/components/diagnostics.md new file mode 100644 index 0000000..9490ca7 --- /dev/null +++ b/docs/components/diagnostics.md @@ -0,0 +1,210 @@ +# xqsr3 Diagnostics + +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) +- [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`. + + +## Choosing a diagnostic tool + +* 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. + + +## `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', +) +``` + +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` + +`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) +# => "#" +``` + +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. + +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/`. + + + diff --git a/docs/components/extensions.md b/docs/components/extensions.md new file mode 100644 index 0000000..96788bc --- /dev/null +++ b/docs/components/extensions.md @@ -0,0 +1,232 @@ +# xqsr3 Ruby Extensions + +Ruby extensions add focused methods to standard-library classes and modules. +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) +- [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' +``` + +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. + +`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. + +```Ruby +require 'xqsr3/extensions/hash/except' + +settings = { host: 'localhost', port: 80, secret: 'hidden' } +settings.except(:secret) +# => { host: 'localhost', port: 80 } +settings +# remains unchanged +``` + + +## Integer extensions + +`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' + +1234567.to_s_grp(3) # => '1,234,567' +``` + + +## IO 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. + +```Ruby +require 'xqsr3/extensions/io/writelines' + +IO.writelines('output.txt', ['first', 'second']) +``` + + +## Kernel extensions + +The Kernel extensions are: + +* `Kernel::Integer`, which provides the xqsr3 integer conversion entry point; +* `Kernel#raise_with_options`, which raises option-aware exception classes. + +The conversion and diagnostics pages describe their behaviour in detail. + + +## String extensions + +The String extension group provides: + +* `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'`. + + +## 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. + +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. + +For executable behavioural examples, see the extension tests under +`test/unit/extensions/`. + + + diff --git a/docs/components/hash-utilities.md b/docs/components/hash-utilities.md new file mode 100644 index 0000000..c319c17 --- /dev/null +++ b/docs/components/hash-utilities.md @@ -0,0 +1,192 @@ +# xqsr3 Hash Utilities + +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) +- [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. + + +## `DeepTransform` + +`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/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'}} +``` + +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'}} +``` + +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} +``` + +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. + + + diff --git a/docs/components/io.md b/docs/components/io.md new file mode 100644 index 0000000..0b25568 --- /dev/null +++ b/docs/components/io.md @@ -0,0 +1,188 @@ +# xqsr3 IO + +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) +- [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/extensions/io/writelines' +``` + + +## 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' + +Xqsr3::IO.writelines('output.txt', ['first', 'second']) +# creates: +# first +# second +``` + +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" +``` + +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`. + + + diff --git a/docs/components/quality.md b/docs/components/quality.md new file mode 100644 index 0000000..7a59f7a --- /dev/null +++ b/docs/components/quality.md @@ -0,0 +1,251 @@ +# xqsr3 Quality + +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) +- [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`. + + +## A boundary-checking pattern + +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 +``` + +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`. + + + diff --git a/docs/components/string-utilities.md b/docs/components/string-utilities.md new file mode 100644 index 0000000..aa95887 --- /dev/null +++ b/docs/components/string-utilities.md @@ -0,0 +1,218 @@ +# xqsr3 String Utilities + +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) +- [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' +``` + +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)`. + + +## Extensions and standalone forms + +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. + +For executable behavioural examples, see the string extension tests in +`test/unit/extensions/string/` and the standalone truncation tests in +`test/unit/string_utilities/`. + + + diff --git a/docs/guides/README.md b/docs/guides/README.md new file mode 100644 index 0000000..da2b6e3 --- /dev/null +++ b/docs/guides/README.md @@ -0,0 +1,17 @@ +# 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. + +* [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; +* [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/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. + + + 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. + + + 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. + + + 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: 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 4f42c1fa1803c1cba25d080472cb05301b9ee041 Mon Sep 17 00:00:00 2001 From: synesissoftware Date: Sat, 29 Aug 2026 14:15:42 +1000 Subject: [PATCH 02/17] Boilerplate (#29) * chore(ci): tidying * chore(gemspec): wire gemspec URLs through PROJECT_URL From 5c91f391d89bee0f6e9e5df8c1efac1cfd6f2675 Mon Sep 17 00:00:00 2001 From: Matt Wilson <152443343+mwsis@users.noreply.github.com> Date: Sat, 29 Aug 2026 14:18:50 +1000 Subject: [PATCH 03/17] Docs (3) (#31) * squash-commit * chore(doc): update documentation metadata for 0.39.11 * 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; * 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): 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): 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): 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): 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): 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): 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): 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): 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): 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(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): 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): 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(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): 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; * 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; * 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; --- .github/workflows/ruby.yml | 24 ++++++++++++ docs/reference/README.md | 37 +++++++++++++++++++ generate_rdoc.sh | 4 +- lib/xqsr3/conversion/bool_parser.rb | 4 +- lib/xqsr3/doc_.rb | 23 ++++++++++-- lib/xqsr3/extensions/hash/slice.rb | 2 + lib/xqsr3/extensions/integer/to_s_grp.rb | 3 +- .../extensions/string/map_option_string.rb | 2 + 8 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 docs/reference/README.md diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index a641a80..7ef66b7 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -18,6 +18,10 @@ on: - master - dev - boilerplate + - doc + - doc.1 + - doc.2 + - doc.3 - idiomatic - rc1 - rc2 @@ -32,6 +36,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: 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/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/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 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 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 b26020688ed52ee605796b9d881bf07aa8b75914 Mon Sep 17 00:00:00 2001 From: Matt Wilson <152443343+mwsis@users.noreply.github.com> Date: Sat, 29 Aug 2026 14:22:07 +1000 Subject: [PATCH 04/17] Docs (2) (#32) * squash-commit * chore(doc): update documentation metadata for 0.39.11 * 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; * 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): 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): 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): 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): 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): 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): 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): 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): 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): 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(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): 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): 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(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): 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; * 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; * 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; * 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 97a4e3449fb7c978735f7ce78408169fb177c356 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Sat, 29 Aug 2026 14:24:15 +1000 Subject: [PATCH 05/17] empty From b299e06e8d076f65a9549ca839b90876a071cc23 Mon Sep 17 00:00:00 2001 From: Matt Wilson <152443343+mwsis@users.noreply.github.com> Date: Sat, 29 Aug 2026 14:30:41 +1000 Subject: [PATCH 06/17] Docs (1) (#33) * squash-commit * chore(doc): update documentation metadata for 0.39.11 * 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; * 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): 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): 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): 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): 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): 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): 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): 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): 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): 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(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): 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): 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(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): 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; * 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; * 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; * 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; * 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; * squash-commit --- .github/workflows/ruby.yml | 4 +--- TODO.md | 2 +- docs/components/README.md | 24 ++++++++++++++++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 7ef66b7..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 @@ -20,8 +20,6 @@ on: - boilerplate - doc - doc.1 - - doc.2 - - doc.3 - idiomatic - rc1 - rc2 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 0056053fd527d690c01b68a3b62b11826bcc3015 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Sat, 29 Aug 2026 14:33:54 +1000 Subject: [PATCH 07/17] 0.39.11 --- CHANGES.md | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b7c6021..16d5dfb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,9 +3,27 @@ ## 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; +* Added a user-oriented component catalogue under **docs/components/**, + covering standalone components, extensions, loading paths, API summaries, + and representative usage; +* Added task-oriented guides under **docs/guides/** for getting started, + component selection, input parsing and validation, collection processing, + failure handling, and output formatting; +* Added **docs/reference/README.md** describing the generated RDoc reference, + and updated **generate_rdoc.sh** to exclude authored **docs/** and + **examples/** content from generated output; +* Expanded source-level RDoc documentation and visibility annotations for the + public API, including **FrequencyMap**, **BoolParser**, + **ExceptionUtilities**, **WithCause**, **ParameterChecking**, and extension + classes; +* Extended **README.md** with linked component categories and navigation to + the component catalogue and user guides; +* Added a GitHub Actions Documentation job to + **.github/workflows/ruby.yml** that generates an RDoc coverage report under + Ruby 3.4, and replaced the **bp-3** branch trigger with **doc** and + **doc.1**; +* Added documentation follow-up items to **TODO.md**; +* Removed the **.vscode/** ignore rule from **.gitignore**; * Refreshed `Updated:` fields and copyright date ranges in modified library sources; * Bumped the library version to 0.39.11 and recorded the release in From cdb53591c9d0fa9fde974ad6f46f3f49dc61fc56 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Sat, 29 Aug 2026 14:40:54 +1000 Subject: [PATCH 08/17] CI triggers now canonical --- .github/workflows/ruby.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index a009ab3..5f8d36f 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -18,8 +18,6 @@ on: - master - dev - boilerplate - - doc - - doc.1 - idiomatic - rc1 - rc2 From cc01dd303e48cbf8051480adb724dce15edc2a80 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Sat, 29 Aug 2026 14:42:53 +1000 Subject: [PATCH 09/17] fix: copyright / updated --- lib/xqsr3/doc_.rb | 4 ++-- lib/xqsr3/extensions/integer/to_s_grp.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/xqsr3/doc_.rb b/lib/xqsr3/doc_.rb index 6a1b677..7957b59 100644 --- a/lib/xqsr3/doc_.rb +++ b/lib/xqsr3/doc_.rb @@ -5,13 +5,13 @@ # Purpose: Documentation of the ::Xqsr3 modules # # Created: 10th June 2016 -# Updated: 28th August 2026 +# Updated: 29th 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) 2016-2019, Matthew Wilson and Synesis Software # All rights reserved. # diff --git a/lib/xqsr3/extensions/integer/to_s_grp.rb b/lib/xqsr3/extensions/integer/to_s_grp.rb index fcbcb97..667a1f7 100644 --- a/lib/xqsr3/extensions/integer/to_s_grp.rb +++ b/lib/xqsr3/extensions/integer/to_s_grp.rb @@ -5,13 +5,13 @@ # Purpose: Adds a to_s_grp() method to the Integer class # # Created: 29th March 2024 -# Updated: 28th August 2026 +# Updated: 29th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # # Author: Matthew Wilson # -# Copyright (c) 2024, Matthew Wilson and Synesis Information Systems +# Copyright (c) 2024-2026, Matthew Wilson and Synesis Information Systems # All rights reserved. # # Redistribution and use in source and binary forms, with or without From 0c713a8bb6ff5a720ed1a2fb392d4626e5ee0507 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Sat, 29 Aug 2026 14:46:20 +1000 Subject: [PATCH 10/17] Docs: align Windows RDoc generation with Unix helper * Exclude authored docs and examples from Windows RDoc output; * Refresh generate_rdoc.cmd metadata for 29th August 2026; --- generate_rdoc.cmd | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/generate_rdoc.cmd b/generate_rdoc.cmd index 4f6f7ad..2aa4e78 100644 --- a/generate_rdoc.cmd +++ b/generate_rdoc.cmd @@ -6,7 +6,7 @@ REM REM Purpose: Generates documentation REM REM Created: 14th August 2026 -REM Updated: 14th August 2026 +REM Updated: 29th August 2026 REM REM ######################################################################## @@ -19,6 +19,8 @@ rdoc ^ -x run_all_unit_tests.sh ^ -x *.gemspec ^ -x doc/ ^ + -x docs/ ^ + -x examples/ ^ -x gems/ ^ -x old-gems/ ^ -x test/performance/ ^ From 3a0f8ddfe91b2a845a625c1332752c1103e86cb5 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Sat, 29 Aug 2026 14:48:39 +1000 Subject: [PATCH 11/17] CI/docs: add Ruby 4.0 coverage and update release notes * Exercise Ruby 4.0 across the supported operating-system matrix; * Record the expanded coverage and canonical CI trigger state in CHANGES.md; --- .github/workflows/ruby.yml | 1 + CHANGES.md | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 5f8d36f..11e8532 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -65,6 +65,7 @@ jobs: ruby-version: # - head + - '4.0' - '3.4' - '3.3' - '3.2' diff --git a/CHANGES.md b/CHANGES.md index 16d5dfb..9ce74c6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -20,8 +20,9 @@ the component catalogue and user guides; * Added a GitHub Actions Documentation job to **.github/workflows/ruby.yml** that generates an RDoc coverage report under - Ruby 3.4, and replaced the **bp-3** branch trigger with **doc** and - **doc.1**; + Ruby 3.4, and aligned its push branch triggers with the canonical set; +* Added Ruby 4.0 to the GitHub Actions test matrix across the supported + operating systems; * Added documentation follow-up items to **TODO.md**; * Removed the **.vscode/** ignore rule from **.gitignore**; * Refreshed `Updated:` fields and copyright date ranges in modified library From 9281cae4f948997546e2477a82ec7f3fbfa336ff Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Sat, 29 Aug 2026 15:17:45 +1000 Subject: [PATCH 12/17] RDoc: modernise metadata-aware helpers * Add `.sis/script_info_lines.txt` for shared project help metadata; * Make Unix and Windows helpers support `--help` and project-aware usage; * Run helpers from their own directory by default, with `--pwd` override; * Support `SIS_RDOC_DOC_DIR` for configurable generated documentation; * Enforce complete RDoc coverage reports in the Unix helper; * Record helper behaviour and coverage policy in release notes; --- .sis/script_info_lines.txt | 3 + CHANGES.md | 7 ++ generate_rdoc.cmd | 68 +++++++++++++- generate_rdoc.sh | 175 ++++++++++++++++++++++++++++++++----- 4 files changed, 228 insertions(+), 25 deletions(-) create mode 100644 .sis/script_info_lines.txt diff --git a/.sis/script_info_lines.txt b/.sis/script_info_lines.txt new file mode 100644 index 0000000..6568798 --- /dev/null +++ b/.sis/script_info_lines.txt @@ -0,0 +1,3 @@ +xqsr3 is a Ruby library of extensions for the standard Ruby and 3rd-party libraries +Copyright (c) 2019-2026, Matthew Wilson and Synesis Information Systems +Copyright (c) 2016-2019, Matthew Wilson and Synesis Software diff --git a/CHANGES.md b/CHANGES.md index 9ce74c6..eb7617b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,13 @@ Ruby 3.4, and aligned its push branch triggers with the canonical set; * Added Ruby 4.0 to the GitHub Actions test matrix across the supported operating systems; +* Made the RDoc coverage check fail when a documentable API entity is + undocumented; +* Added `--help` support to the Unix and Windows RDoc helpers, using project + metadata from **.sis/**; +* Made the Unix and Windows RDoc helpers runnable from any working directory, + with `--pwd` selecting the caller's directory and `SIS_RDOC_DOC_DIR` + controlling the generated-document directory; * Added documentation follow-up items to **TODO.md**; * Removed the **.vscode/** ignore rule from **.gitignore**; * Refreshed `Updated:` fields and copyright date ranges in modified library diff --git a/generate_rdoc.cmd b/generate_rdoc.cmd index 2aa4e78..7c4e655 100644 --- a/generate_rdoc.cmd +++ b/generate_rdoc.cmd @@ -10,15 +10,73 @@ REM Updated: 29th August 2026 REM REM ######################################################################## -IF EXIST doc RMDIR /S /Q doc +SETLOCAL +SET "ProjectDir=%~dp0" +SET "ProjectNameFile=%~dp0.sis\project_name.txt" +SET "ProjectName=" +IF EXIST "%ProjectNameFile%" FOR /F "usebackq tokens=* delims=" %%A IN ("%ProjectNameFile%") DO IF NOT DEFINED ProjectName SET "ProjectName=%%A" +IF "%ProjectName%"=="" SET "ProjectName=Ruby project" +SET "DocDir=%SIS_RDOC_DOC_DIR%" +IF "%DocDir%"=="" SET "DocDir=doc" +SET "RDocArgs=" + +:parse_args +IF "%~1"=="" GOTO parsed_args +IF /I "%~1"=="--help" GOTO show_help +IF /I "%~1"=="--pwd" GOTO use_pwd +SET "RDocArgs=%RDocArgs% "%~1"" +GOTO next_arg + +:use_pwd +SET "ProjectDir=%CD%" + +:next_arg +SHIFT +GOTO parse_args + +:show_help +IF EXIST "%~dp0.sis\script_info_lines.txt" TYPE "%~dp0.sis\script_info_lines.txt" +ECHO Generates RDoc documentation for %ProjectName% +ECHO. +ECHO %~nx0 [ ... flags/options ... ] +ECHO. +ECHO Flags/options: +ECHO. +ECHO --pwd +ECHO operates in the caller's current directory instead of the +ECHO script's directory +ECHO. +ECHO -C +ECHO --coverage-report +ECHO generates an RDoc coverage report and fails if the report is +ECHO less than 100%% documented +ECHO. +ECHO --help +ECHO displays this help and terminates +ECHO. +ECHO Environment variables: +ECHO. +ECHO SIS_RDOC_DOC_DIR +ECHO sets the generated-document directory (default: doc) +EXIT /B 0 + +:parsed_args + +PUSHD "%ProjectDir%" || ( + ECHO %~nx0: project directory "%ProjectDir%" not found 1>&2 + EXIT /B 1 +) + +IF EXIST "%DocDir%" RMDIR /S /Q "%DocDir%" rdoc ^ + --op "%DocDir%" ^ -x build_gem.cmd ^ -x build_gem.sh ^ -x generate_rdoc.cmd ^ -x generate_rdoc.sh ^ -x run_all_unit_tests.sh ^ -x *.gemspec ^ - -x doc/ ^ + -x "%DocDir%/" ^ -x docs/ ^ -x examples/ ^ -x gems/ ^ @@ -27,4 +85,8 @@ rdoc ^ -x test/scratch/ ^ -x tc_.*\.rb ^ -x ts_all.rb ^ - %* + %RDocArgs% + +SET "RDocResult=%ERRORLEVEL%" +POPD +ENDLOCAL & EXIT /B %RDocResult% diff --git a/generate_rdoc.sh b/generate_rdoc.sh index 2293738..807af7c 100755 --- a/generate_rdoc.sh +++ b/generate_rdoc.sh @@ -6,28 +6,159 @@ # Purpose: Generates documentation # # Created: 11th June 2016 -# Updated: 28th August 2026 +# Updated: 29th August 2026 # ############################################################################# -rm -rfd doc -rdoc \ - -x build_gem.cmd \ - -x build_gem.sh \ - -x generate_rdoc.cmd \ - -x generate_rdoc.sh \ - -x run_all_unit_tests.sh \ - -x *.gemspec \ - \ - -x doc/ \ - -x docs/ \ - -x examples/ \ - -x gems/ \ - -x old-gems/ \ - -x test/performance/ \ - -x test/scratch/ \ - \ - -x tc_.*\.rb \ - -x ts_all.rb \ - \ - $* +ScriptPath="${BASH_SOURCE[0]}" +while [ -h "$ScriptPath" ]; do + + ScriptDir="$(cd -P "$(dirname "$ScriptPath")" && pwd)" + ScriptPath="$(readlink "$ScriptPath")" + [[ "$ScriptPath" != /* ]] && ScriptPath="$ScriptDir/$ScriptPath" +done +ScriptDir="$(cd -P "$(dirname "$ScriptPath")" && pwd)" +ProjectNameFile="$ScriptDir/.sis/project_name.txt" +if [ -f "$ProjectNameFile" ]; then + + ProjectName=$(tr -d '[:space:]' < "$ProjectNameFile") +else + + ProjectName=$(basename "$ScriptDir") +fi + +ProjectDir="$ScriptDir" +ForwardArgs=() +FoundHelp= + +print_help() { + + if [ -f "$ScriptDir/.sis/script_info_lines.txt" ]; then + + cat "$ScriptDir/.sis/script_info_lines.txt" + fi + + cat << EOF +Generates RDoc documentation for $ProjectName + +$ScriptPath [ ... flags/options ... ] + +Flags/options: + + --pwd + operates in the caller's current directory instead of the + script's directory + + -C + --coverage-report + generates an RDoc coverage report and fails if the report is + less than 100% documented + + --help + displays this help and terminates + +Environment variables: + + SIS_RDOC_DOC_DIR + sets the generated-document directory (default: doc) + +EOF +} + +for arg in "$@" +do + + case "$arg" in + --pwd) + + ProjectDir="$(pwd)" + ;; + --help) + + FoundHelp=1 + ;; + *) + + ForwardArgs+=("$arg") + ;; + esac +done + +if [ -n "$FoundHelp" ]; then + + print_help + exit 0 +fi + +if ! cd "$ProjectDir"; then + + >&2 echo "$0: project directory '$ProjectDir' not found" + exit 1 +fi + +DocDir="${SIS_RDOC_DOC_DIR:-doc}" +rm -rfd "$DocDir" + +run_rdoc() { + + rdoc \ + --op "$DocDir" \ + -x build_gem.cmd \ + -x build_gem.sh \ + -x generate_rdoc.cmd \ + -x generate_rdoc.sh \ + -x run_all_unit_tests.sh \ + -x *.gemspec \ + \ + -x "$DocDir/" \ + -x docs/ \ + -x examples/ \ + -x gems/ \ + -x old-gems/ \ + -x test/performance/ \ + -x test/scratch/ \ + \ + -x tc_.*\.rb \ + -x ts_all.rb \ + \ + "${ForwardArgs[@]}" +} + +RDocCoverage= +for arg in "${ForwardArgs[@]}" +do + + case "$arg" in + -C|-C[0-9]*|--dcov|--coverage-report|--coverage-report=*) + + RDocCoverage=1 + ;; + esac +done + +if [ -z "$RDocCoverage" ]; then + + run_rdoc "${ForwardArgs[@]}" +else + + RDocOutput=$(run_rdoc "${ForwardArgs[@]}") + RDocResult=$? + + printf '%s\n' "$RDocOutput" + + if [ 0 -ne "$RDocResult" ]; then + + exit "$RDocResult" + fi + + case "$RDocOutput" in + *'100.00% documented'*) + + ;; + *) + + >&2 echo "$0: RDoc coverage is incomplete" + exit 1 + ;; + esac +fi From d8ce880c722ae7ce76499ced794d7b82e12393ea Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Sat, 29 Aug 2026 15:19:24 +1000 Subject: [PATCH 13/17] Docs: correct task-guide availability status * Update README.md to link the available task-oriented guides; * Remove the stale statement that the guides are forthcoming; --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 57b1016..f2100cc 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Use is via specific APIs or groups. For example, in order to use the require 'xqsr3/containers/frequency_map' ``` -Alternatively, to use all **test/unit** extensions you would ``require`` all +Alternatively, to use _all_ **test/unit** extensions you would ``require`` all relative via the file: ```Ruby @@ -94,8 +94,8 @@ and extensions to the following standard library components: * [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). +loading instructions and initial API summaries. The task-oriented guides are +available under [docs/guides/](./docs/guides/README.md). ## Examples From ad0c3eef5adbd5ff6560914cd7df6b30494117c5 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Sat, 29 Aug 2026 15:24:31 +1000 Subject: [PATCH 14/17] Docs: clean up legacy Markdown debt * Replace legacy inline-code delimiters in README and example docs; * Align the EXAMPLES.md table and remove FAQ's duplicate H1; * Add the required EOF marker to the example documentation; * Normalize completed TODO checklist entries and close the stale docs item; * Record the Markdown cleanup in CHANGES.md; --- CHANGES.md | 44 +++++++++--------------------- EXAMPLES.md | 9 +++--- FAQ.md | 3 -- README.md | 6 ++-- TODO.md | 19 +++++++------ examples/count_word_frequencies.md | 5 ++-- 6 files changed, 33 insertions(+), 53 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index eb7617b..3ec3539 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,39 +3,21 @@ ## 0.39.11 - 29th August 2026 -* Added a user-oriented component catalogue under **docs/components/**, - covering standalone components, extensions, loading paths, API summaries, - and representative usage; -* Added task-oriented guides under **docs/guides/** for getting started, - component selection, input parsing and validation, collection processing, - failure handling, and output formatting; -* Added **docs/reference/README.md** describing the generated RDoc reference, - and updated **generate_rdoc.sh** to exclude authored **docs/** and - **examples/** content from generated output; -* Expanded source-level RDoc documentation and visibility annotations for the - public API, including **FrequencyMap**, **BoolParser**, - **ExceptionUtilities**, **WithCause**, **ParameterChecking**, and extension - classes; -* Extended **README.md** with linked component categories and navigation to - the component catalogue and user guides; -* Added a GitHub Actions Documentation job to - **.github/workflows/ruby.yml** that generates an RDoc coverage report under - Ruby 3.4, and aligned its push branch triggers with the canonical set; -* Added Ruby 4.0 to the GitHub Actions test matrix across the supported - operating systems; -* Made the RDoc coverage check fail when a documentable API entity is - undocumented; -* Added `--help` support to the Unix and Windows RDoc helpers, using project - metadata from **.sis/**; -* Made the Unix and Windows RDoc helpers runnable from any working directory, - with `--pwd` selecting the caller's directory and `SIS_RDOC_DOC_DIR` - controlling the generated-document directory; +* Added a user-oriented component catalogue under **docs/components/**, covering standalone components, extensions, loading paths, API summaries, and representative usage; +* Added task-oriented guides under **docs/guides/** for getting started, component selection, input parsing and validation, collection processing, failure handling, and output formatting; +* Normalised legacy Markdown inline-code, checklist, table, and heading markup in **README.md**, **EXAMPLES.md**, **FAQ.md**, **TODO.md**, and the example guide; +* Added **docs/reference/README.md** describing the generated RDoc reference, and updated **generate_rdoc.sh** to exclude authored **docs/** and **examples/** content from generated output; +* Expanded source-level RDoc documentation and visibility annotations for the public API, including **FrequencyMap**, **BoolParser**, **ExceptionUtilities**, **WithCause**, **ParameterChecking**, and extension classes; +* Extended **README.md** with linked component categories and navigation to the component catalogue and user guides; +* Added a GitHub Actions Documentation job to **.github/workflows/ruby.yml** that generates an RDoc coverage report under Ruby 3.4, and aligned its push branch triggers with the canonical set; +* Added Ruby 4.0 to the GitHub Actions test matrix across the supported operating systems; +* Made the RDoc coverage check fail when a documentable API entity is undocumented; +* Added `--help` support to the Unix and Windows RDoc helpers, using project metadata from **.sis/**; +* Made the Unix and Windows RDoc helpers runnable from any working directory, with `--pwd` selecting the caller's directory and `SIS_RDOC_DOC_DIR` controlling the generated-document directory; * Added documentation follow-up items to **TODO.md**; * Removed the **.vscode/** ignore rule from **.gitignore**; -* 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**; +* 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 diff --git a/EXAMPLES.md b/EXAMPLES.md index 3500e67..ca278cf 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -1,9 +1,8 @@ # xqsr3 - Examples -|Name|Source & Description|Summary| -|---|---|---| -|**count_word_frequencies**|[examples/count_word_frequencies.rb](./examples/count_word_frequencies.rb)
[examples/count_word_frequencies.md](./examples/count_word_frequencies.md)|Simple example supporting ```--help``` and ```--version```| +| Name | Source & Description | Summary | +| -------------------------- | -------------------- | ------- | +| **count_word_frequencies** | [examples/count_word_frequencies.rb](./examples/count_word_frequencies.rb)
[examples/count_word_frequencies.md](./examples/count_word_frequencies.md) | Simple example supporting `--help` and `--version` | - - + \ No newline at end of file diff --git a/FAQ.md b/FAQ.md index f2a3020..e4df16a 100644 --- a/FAQ.md +++ b/FAQ.md @@ -10,9 +10,6 @@ it will be used to create one. - [Q1: "How do I install this library?"](#q1-how-do-i-install-this-library) -# FAQs: - - ## Q1: "How do I install this library?" Install via **gem**: diff --git a/README.md b/README.md index f2100cc..b4c49bc 100644 --- a/README.md +++ b/README.md @@ -49,13 +49,13 @@ gem install xqsr3 or add it to your `Gemfile`. Use is via specific APIs or groups. For example, in order to use the -``FrequencyMap`` class you would ``require`` the source file, as in: +`FrequencyMap` class you would `require` the source file, as in: ```Ruby require 'xqsr3/containers/frequency_map' ``` -Alternatively, to use _all_ **test/unit** extensions you would ``require`` all +Alternatively, to use _all_ **test/unit** extensions you would `require` all relative via the file: ```Ruby @@ -100,7 +100,7 @@ available under [docs/guides/](./docs/guides/README.md). ## Examples -Examples are provided in the ```examples``` directory, along with a markdown description for each. A detailed list TOC of them is provided in [EXAMPLES.md](./EXAMPLES.md). +Examples are provided in the `examples` directory, along with a markdown description for each. A detailed list TOC of them is provided in [EXAMPLES.md](./EXAMPLES.md). ## Project Information diff --git a/TODO.md b/TODO.md index 8035306..5ca01a3 100644 --- a/TODO.md +++ b/TODO.md @@ -3,14 +3,15 @@ ## Functional improvements -* [x] ~~~prepare `IO.writelines` (and related helpers) for frozen-string-literal defaults (Ruby 3.4+ warnings under `-W`)~~~; -* [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**~~~; +* [x] ~~~prepare `IO.writelines` (and related helpers) for frozen-string-literal defaults (Ruby 3.4+ warnings under `-W`)~~~ - ✅; +* [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 -* [x] ~~~curated component catalogue under **docs/components/**~~~; -* [x] ~~~task-oriented user guide under **docs/guides/**~~~; +* [x] ~~~curated component catalogue under **docs/components/**~~~ - ✅; +* [x] ~~~task-oriented user guide under **docs/guides/**~~~ - ✅; +* [ ] Additional example programs covering the public component categories; * [ ] Expanded generated API reference; * [ ] Executable cookbook and recipe documentation; * [ ] Static documentation website with search and versioned references; @@ -23,11 +24,11 @@ ## Packaging improvements -* [ ] **README.md** and **docs/*.md** introductory elements of main features; -* [x] ~~~remove **Gemfile.lock**~~~; -* [x] ~~~obtain a **run_all_unit_tests.sh** (from **misc-dev-scripts**) that skips `tput` when `$TERM` is unset or stdout is not a TTY (CI: `tput: No value for $TERM and no -T specified`)~~~; -* [x] ~~~after the packaging/boilerplate/CI baseline release: bump **VERSION**, drop gemspec `required_ruby_version` `< 4` upper bound, and align **CHANGES**~~~; -* [x] ~~~gemspec polish: `https` homepage, Rubygems `metadata` URIs, stop using `Date.today`, include **CHANGES.md** in packaged files~~~; +* [x] ~~~**README.md** and **docs/*.md** introductory elements of main features~~~ - ✅; +* [x] ~~~remove **Gemfile.lock**~~~ - ✅; +* [x] ~~~obtain a **run_all_unit_tests.sh** (from **misc-dev-scripts**) that skips `tput` when `$TERM` is unset or stdout is not a TTY (CI: `tput: No value for $TERM and no -T specified`)~~~ - ✅; +* [x] ~~~after the packaging/boilerplate/CI baseline release: bump **VERSION**, drop gemspec `required_ruby_version` `< 4` upper bound, and align **CHANGES**~~~ - ✅; +* [x] ~~~gemspec polish: `https` homepage, Rubygems `metadata` URIs, stop using `Date.today`, include **CHANGES.md** in packaged files~~~ - ✅; diff --git a/examples/count_word_frequencies.md b/examples/count_word_frequencies.md index 286b9a3..c27f54c 100644 --- a/examples/count_word_frequencies.md +++ b/examples/count_word_frequencies.md @@ -2,7 +2,7 @@ ## Summary -Simple example illustrating use of ``FrequencyMap`` class. +Simple example illustrating use of `FrequencyMap` class. ## Source @@ -57,7 +57,7 @@ __END__ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ``` -The code is pretty self-explanatory. I've grabbed some **Lorem Ipsum** and placed into an ``__END__`` section, which is then read (via ``DATA``) and ``split`` into ``words``. These words are then pushed into ``FrequencyMap`` instances in two ways: manually; and via ``FrequencyMap::ByElement``. Each list is written out, omitting single occurrences, inside an ``each_by_frequency`` loop. +The code is pretty self-explanatory. I've grabbed some **Lorem Ipsum** and placed into an `__END__` section, which is then read (via `DATA`) and `split` into `words`. These words are then pushed into `FrequencyMap` instances in two ways: manually; and via `FrequencyMap::ByElement`. Each list is written out, omitting single occurrences, inside an `each_by_frequency` loop. ## Usage @@ -80,3 +80,4 @@ Analyse DATA words via FrequencyMap ``` + From 77239815c8e9e29d74d3dab59d1badd7a7dd2f66 Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Sat, 29 Aug 2026 15:34:03 +1000 Subject: [PATCH 15/17] Improve xqsr3 RDoc reference quality - Give generated RDoc an xqsr3-specific title and API-only navigation; - Exclude Markdown, project boilerplate, gemspecs, and gem archives; - Replace placeholder and legacy markup in public API descriptions; - Document the completed reference work and align the Ruby helper standard; --- CHANGES.md | 2 ++ TODO.md | 2 +- docs/reference/README.md | 7 +++-- generate_rdoc.cmd | 8 ++++- generate_rdoc.sh | 8 ++++- lib/xqsr3/array_utilities/join_with_or.rb | 5 ++-- .../map_option_string.rb | 5 ++-- lib/xqsr3/containers/frequency_map.rb | 12 ++++---- lib/xqsr3/containers/multi_map.rb | 9 +++--- lib/xqsr3/conversion/bool_parser.rb | 7 +++-- lib/xqsr3/conversion/integer_parser.rb | 8 ++--- .../diagnostics/exceptions/with_cause.rb | 30 ++++++++++++------- lib/xqsr3/diagnostics/inspect_builder.rb | 4 +-- lib/xqsr3/hash_utilities/key_matching.rb | 6 ++-- lib/xqsr3/quality/parameter_checking.rb | 18 +++++------ lib/xqsr3/string_utilities/ends_with.rb | 6 ++-- lib/xqsr3/string_utilities/nil_if_empty.rb | 6 ++-- .../string_utilities/nil_if_whitespace.rb | 6 ++-- lib/xqsr3/string_utilities/quote_if.rb | 5 ++-- lib/xqsr3/string_utilities/starts_with.rb | 6 ++-- 20 files changed, 94 insertions(+), 66 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3ec3539..b4ae6ff 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,7 +7,9 @@ * Added task-oriented guides under **docs/guides/** for getting started, component selection, input parsing and validation, collection processing, failure handling, and output formatting; * Normalised legacy Markdown inline-code, checklist, table, and heading markup in **README.md**, **EXAMPLES.md**, **FAQ.md**, **TODO.md**, and the example guide; * Added **docs/reference/README.md** describing the generated RDoc reference, and updated **generate_rdoc.sh** to exclude authored **docs/** and **examples/** content from generated output; +* Set the generated RDoc title to the **xqsr3** API reference, and excluded project boilerplate pages from the reference; * Expanded source-level RDoc documentation and visibility annotations for the public API, including **FrequencyMap**, **BoolParser**, **ExceptionUtilities**, **WithCause**, **ParameterChecking**, and extension classes; +* Replaced a remaining placeholder example and legacy RDoc markup in public API comments; * Extended **README.md** with linked component categories and navigation to the component catalogue and user guides; * Added a GitHub Actions Documentation job to **.github/workflows/ruby.yml** that generates an RDoc coverage report under Ruby 3.4, and aligned its push branch triggers with the canonical set; * Added Ruby 4.0 to the GitHub Actions test matrix across the supported operating systems; diff --git a/TODO.md b/TODO.md index 5ca01a3..1b74752 100644 --- a/TODO.md +++ b/TODO.md @@ -12,7 +12,7 @@ * [x] ~~~curated component catalogue under **docs/components/**~~~ - ✅; * [x] ~~~task-oriented user guide under **docs/guides/**~~~ - ✅; * [ ] Additional example programs covering the public component categories; -* [ ] Expanded generated API reference; +* [x] ~~~Expanded generated API reference~~~ - ✅; * [ ] Executable cookbook and recipe documentation; * [ ] Static documentation website with search and versioned references; diff --git a/docs/reference/README.md b/docs/reference/README.md index 0abe7ac..34a60fa 100644 --- a/docs/reference/README.md +++ b/docs/reference/README.md @@ -12,15 +12,16 @@ catalogue: ## Generate the reference -From the project root, run: +From any directory, 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. +to `doc/` by default. Use `--pwd` to operate in the caller's current directory, +or set `SIS_RDOC_DOC_DIR` to choose another output directory. The generated +files are build output and should not be edited by hand. ## Reading the reference diff --git a/generate_rdoc.cmd b/generate_rdoc.cmd index 7c4e655..80bd8f4 100644 --- a/generate_rdoc.cmd +++ b/generate_rdoc.cmd @@ -69,13 +69,19 @@ PUSHD "%ProjectDir%" || ( IF EXIST "%DocDir%" RMDIR /S /Q "%DocDir%" rdoc ^ + --title "%ProjectName% API Reference" ^ --op "%DocDir%" ^ -x build_gem.cmd ^ -x build_gem.sh ^ -x generate_rdoc.cmd ^ -x generate_rdoc.sh ^ -x run_all_unit_tests.sh ^ - -x *.gemspec ^ + -x .*\.gemspec ^ + -x .*\.gem ^ + -x .*\.md ^ + -x Gemfile ^ + -x LICENSE ^ + -x Rakefile ^ -x "%DocDir%/" ^ -x docs/ ^ -x examples/ ^ diff --git a/generate_rdoc.sh b/generate_rdoc.sh index 807af7c..dcf1951 100755 --- a/generate_rdoc.sh +++ b/generate_rdoc.sh @@ -102,13 +102,19 @@ rm -rfd "$DocDir" run_rdoc() { rdoc \ + --title "$ProjectName API Reference" \ --op "$DocDir" \ -x build_gem.cmd \ -x build_gem.sh \ -x generate_rdoc.cmd \ -x generate_rdoc.sh \ -x run_all_unit_tests.sh \ - -x *.gemspec \ + -x '.*\.gemspec' \ + -x '.*\.gem' \ + -x '.*\.md' \ + -x Gemfile \ + -x LICENSE \ + -x Rakefile \ \ -x "$DocDir/" \ -x docs/ \ diff --git a/lib/xqsr3/array_utilities/join_with_or.rb b/lib/xqsr3/array_utilities/join_with_or.rb index b830417..e0b7a70 100644 --- a/lib/xqsr3/array_utilities/join_with_or.rb +++ b/lib/xqsr3/array_utilities/join_with_or.rb @@ -5,7 +5,7 @@ # Purpose: Definition of the ::Xqsr3::ArrayUtilities::JoinWithOr module # # Created: 7th December 2017 -# Updated: 19th August 2026 +# Updated: 29th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # @@ -54,7 +54,8 @@ module Xqsr3 module ArrayUtilities - # +include+-able module that provides sequence-joining functionality + # include-able module that provides sequence-joining + # functionality. module JoinWithOr extend self diff --git a/lib/xqsr3/command_line_utilities/map_option_string.rb b/lib/xqsr3/command_line_utilities/map_option_string.rb index f84d119..ef5a13c 100644 --- a/lib/xqsr3/command_line_utilities/map_option_string.rb +++ b/lib/xqsr3/command_line_utilities/map_option_string.rb @@ -6,7 +6,7 @@ # module # # Created: 15th April 2016 -# Updated: 19th August 2026 +# Updated: 29th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # @@ -55,7 +55,8 @@ module Xqsr3 module CommandLineUtilities - # +include+-able module providing facilities for mapping strings to options + # include-able module providing facilities for mapping strings + # to options. # # === Components of interest # * ::Xqsr3::CommandLineUtilities::MapOptionString.map_option_string_from_string diff --git a/lib/xqsr3/containers/frequency_map.rb b/lib/xqsr3/containers/frequency_map.rb index 28075d8..c162129 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: 28th August 2026 +# Updated: 29th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # @@ -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,7 +308,7 @@ def each_by_key end end - # Enumerates each entry pair - element + frequency - in descending order + # Enumerates each entry pair (element, frequency) in descending order # of frequency. # # Note: this method is expensive, as it must create a new dictionary @@ -593,8 +593,8 @@ def shift # (consistent with +#push+ reducing a count to zero). # # === Return - # +true+ if the element was inserted; +false+ if the element was - # overwritten + # Returns true if the element was inserted; returns + # false if it was overwritten. def store key, count raise TypeError, "'count' parameter must be of type #{::Integer}, but was of type #{count.class}" unless Integer === count @@ -614,7 +614,7 @@ 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 diff --git a/lib/xqsr3/containers/multi_map.rb b/lib/xqsr3/containers/multi_map.rb index 24549cc..682d934 100644 --- a/lib/xqsr3/containers/multi_map.rb +++ b/lib/xqsr3/containers/multi_map.rb @@ -5,7 +5,7 @@ # Purpose: multimap container # # Created: 21st March 2007 -# Updated: 19th August 2026 +# Updated: 29th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # @@ -137,7 +137,8 @@ def initialize @inner = Hash.new end - # Deep-copies +@inner+ (and each values array) after +dup+/+clone+ + # Deep-copies @inner (and each values array) after + # +dup+/+clone+. def initialize_copy other @merge_is_multi = other.instance_variable_get(:@merge_is_multi) @@ -156,7 +157,7 @@ def [] key return @inner[key] end - # Adds/assigns a new key+values pair. Equivalent to + # Adds/assigns a new key/value pair. Equivalent to # # store(key, *values) # @@ -704,7 +705,7 @@ def store key, *values @inner[key] = values end - # Converts instance to an array of +[key,value]+ pairs + # Converts instance to an array of [key, value] pairs. def to_a self.flatten diff --git a/lib/xqsr3/conversion/bool_parser.rb b/lib/xqsr3/conversion/bool_parser.rb index 9505b0c..a1447bc 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: 28th August 2026 +# Updated: 29th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # @@ -51,7 +51,7 @@ module Xqsr3 module Conversion - # +include-able module that provides Boolean parsing + # include-able module that provides Boolean parsing. module BoolParser private @@ -66,7 +66,8 @@ def self.matches_to_ s, expr # :nodoc: end # Resolves an option that may be named by one or more keys, preserving - # explicitly supplied +nil+/+false+ values (unlike +Hash#fetch+ with +||+). + # explicitly supplied nil/false values (unlike + # Hash#fetch with ||). def self.option_value_ options, *keys, default_value keys.each do |key| diff --git a/lib/xqsr3/conversion/integer_parser.rb b/lib/xqsr3/conversion/integer_parser.rb index 3ac3455..cd7604e 100644 --- a/lib/xqsr3/conversion/integer_parser.rb +++ b/lib/xqsr3/conversion/integer_parser.rb @@ -5,7 +5,7 @@ # Purpose: Definition of the ::Xqsr3::Conversion::IntegerParser module # # Created: 21st November 2017 -# Updated: 19th August 2026 +# Updated: 29th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # @@ -51,7 +51,7 @@ module Xqsr3 module Conversion - # +include-able module that provides Integer parsing + # include-able module that provides Integer parsing. module IntegerParser private @@ -161,8 +161,8 @@ def self.to_integer_ arg, base, options, &block # - +block+ An optional caller-supplied 4-parameter block - taking the exception, +arg+, +base+, and +options+ - that will be invoked with the +ArgumentError+ exception, allowing the caller to take additional action. If the block returns then its return value will be returned to the caller; # # * *Options:* - # - +:default+ A default value to be used when +arg+ is +nil+ or cannot be converted by (the original) +Kernel#Integer+; - # - +:nil+ Returns +nil+ if +arg+ is +nil+ or cannot be converted by (the original) +Kernel#Integer+. Ignored if +:default+ is specified; + # - +:default+ A default value to be used when +arg+ is +nil+ or cannot be converted by (the original) Kernel#Integer; + # - +:nil+ Returns +nil+ if +arg+ is +nil+ or cannot be converted by (the original) Kernel#Integer. Ignored if +:default+ is specified; def self.to_integer arg, base = 0, **options, &block IntegerParser_Helper_.to_integer_ arg, base, options, &block diff --git a/lib/xqsr3/diagnostics/exceptions/with_cause.rb b/lib/xqsr3/diagnostics/exceptions/with_cause.rb index 7d0d330..beae109 100644 --- a/lib/xqsr3/diagnostics/exceptions/with_cause.rb +++ b/lib/xqsr3/diagnostics/exceptions/with_cause.rb @@ -5,7 +5,7 @@ # Purpose: Definition of the WithCause inclusion module # # Created: 16th December 2017 -# Updated: 28th August 2026 +# Updated: 29th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # @@ -53,14 +53,22 @@ module Diagnostics 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. + # cause (aka inner-exception), which is then exposed with the + # cause attribute. # # *Examples:* # # Passing an exception cause as a parameter # - # T.B.C. + # class ConfigurationError < StandardError + # include Xqsr3::Diagnostics::Exceptions::WithCause + # end + # + # cause = ArgumentError.new 'port is not numeric' + # error = ConfigurationError.new cause + # + # error.cause # => cause + # error.chained_message # => 'port is not numeric' # module WithCause @@ -69,7 +77,7 @@ module WithCause # 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,13 +141,13 @@ 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. + # The options passed to the initialiser, with :cause removed, + # if present. attr_reader :options # Message obtained by concatenation of all chained exceptions' messages. @@ -168,7 +176,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 diff --git a/lib/xqsr3/diagnostics/inspect_builder.rb b/lib/xqsr3/diagnostics/inspect_builder.rb index a82d96e..55a3c02 100644 --- a/lib/xqsr3/diagnostics/inspect_builder.rb +++ b/lib/xqsr3/diagnostics/inspect_builder.rb @@ -5,7 +5,7 @@ # Purpose: ::Xqsr3::Diagnostics::InspectBuilder module # # Created: 4th September 2018 -# Updated: 19th August 2026 +# Updated: 29th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # @@ -63,7 +63,7 @@ module InspectBuilder_Utilities # :nodoc: all NORMALISE_FUNCTION = lambda { |ar| ar.map { |v| v.to_s }.map { |v| '@' == v[0] ? v : "@#{v}" } } end # module InspectBuilder_Utilities - # Generates an inspect string for the +include+-ing class + # Generates an inspect string for the include-ing class. # # === Signature # diff --git a/lib/xqsr3/hash_utilities/key_matching.rb b/lib/xqsr3/hash_utilities/key_matching.rb index dab27d9..dc2ebe1 100644 --- a/lib/xqsr3/hash_utilities/key_matching.rb +++ b/lib/xqsr3/hash_utilities/key_matching.rb @@ -5,7 +5,7 @@ # Purpose: Definition of the ::Xqsr3::HashUtilities::KeyMatching module # # Created: 15th November 2017 -# Updated: 19th August 2026 +# Updated: 29th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # @@ -54,8 +54,8 @@ module Xqsr3 module HashUtilities - # +include+-able module that provides ::has_match?, #has_match?, ::match, - # and #match methods + # include-able module that provides ::has_match?, #has_match?, + # ::match, and #match methods module KeyMatching private diff --git a/lib/xqsr3/quality/parameter_checking.rb b/lib/xqsr3/quality/parameter_checking.rb index 4f50be5..01abaf8 100644 --- a/lib/xqsr3/quality/parameter_checking.rb +++ b/lib/xqsr3/quality/parameter_checking.rb @@ -5,7 +5,7 @@ # Purpose: Definition of the ParameterChecking module # # Created: 12th February 2015 -# Updated: 28th August 2026 +# Updated: 29th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # @@ -52,8 +52,8 @@ 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. + # check_option() and check_parameter() that may be + # used to check option/parameter values and types. module ParameterChecking private @@ -140,13 +140,13 @@ 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 + # Specific form of check_parameter() that is used to check # options, taking instead the hash and the key. # # === Signature @@ -154,7 +154,7 @@ def check_param value, name, options = {}, &block # * *Parameters:* # - +h+ (+Hash+) The options hash from which the named element is to be tested. May not be +nil+; # - +name+ (+String+, +Symbol+, +[ String, Symbol ]+) The options key name, or an array of names. May not be +nil+; - # - +options+ (+Hash+) Options that control the behaviour of the method in the same way as for +check_parameter()+ except that the +:treat_as_option+ option (with the value +true+) is merged in before calling +check_parameter()+; + # - +options+ (+Hash+) Options that control the behaviour of the method in the same way as for check_parameter() except that the +:treat_as_option+ option (with the value +true+) is merged in before calling check_parameter(); # # * *Options:* def check_option h, name, options = {}, &block @@ -196,13 +196,13 @@ 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 + # Specific form of check_parameter() that is used to check # options, taking instead the hash and the key. # # === Signature @@ -210,7 +210,7 @@ def self.check_param value, name, options = {}, &block # * *Parameters:* # - +h+ (+Hash+) The options hash from which the named element is to be tested. May not be +nil+; # - +name+ (+String+, +Symbol+, +[ String, Symbol ]+) The options key name, or an array of names. May not be +nil+; - # - +options+ (+Hash+) Options that control the behaviour of the method in the same way as for +check_parameter()+ except that the +:treat_as_option+ option (with the value +true+) is merged in before calling +check_parameter()+; + # - +options+ (+Hash+) Options that control the behaviour of the method in the same way as for check_parameter() except that the +:treat_as_option+ option (with the value +true+) is merged in before calling check_parameter(); # # * *Options:* def self.check_option h, name, options = {}, &block diff --git a/lib/xqsr3/string_utilities/ends_with.rb b/lib/xqsr3/string_utilities/ends_with.rb index 74fa91b..bbdeb50 100644 --- a/lib/xqsr3/string_utilities/ends_with.rb +++ b/lib/xqsr3/string_utilities/ends_with.rb @@ -5,7 +5,7 @@ # Purpose: Definition of the ::Xqsr3::StringUtilities::EndsWith module # # Created: 13th April 2016 -# Updated: 19th August 2026 +# Updated: 29th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # @@ -51,8 +51,8 @@ module Xqsr3 module StringUtilities - # +include+-able module that provides ::string_ends_with? and #ends_with? - # methods + # include-able module that provides ::string_ends_with? and + # #ends_with? methods. module EndsWith private diff --git a/lib/xqsr3/string_utilities/nil_if_empty.rb b/lib/xqsr3/string_utilities/nil_if_empty.rb index 7b9c271..870b1f6 100644 --- a/lib/xqsr3/string_utilities/nil_if_empty.rb +++ b/lib/xqsr3/string_utilities/nil_if_empty.rb @@ -5,7 +5,7 @@ # Purpose: Definition of the ::Xqsr3::StringUtilities::NilIfEmpty module # # Created: 25th January 2018 -# Updated: 19th August 2026 +# Updated: 29th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # @@ -51,8 +51,8 @@ module Xqsr3 module StringUtilities - # +include+-able module that provides ::string_nil_if_empty and - # #nil_if_empty methods + # include-able module that provides ::string_nil_if_empty and + # #nil_if_empty methods. module NilIfEmpty private diff --git a/lib/xqsr3/string_utilities/nil_if_whitespace.rb b/lib/xqsr3/string_utilities/nil_if_whitespace.rb index 315e066..dd89094 100644 --- a/lib/xqsr3/string_utilities/nil_if_whitespace.rb +++ b/lib/xqsr3/string_utilities/nil_if_whitespace.rb @@ -6,7 +6,7 @@ # module # # Created: 25th January 2018 -# Updated: 19th August 2026 +# Updated: 29th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # @@ -52,8 +52,8 @@ module Xqsr3 module StringUtilities - # +include+-able module that provides ::string_nil_if_whitespace and - # #nil_if_whitespace methods + # include-able module that provides ::string_nil_if_whitespace + # and #nil_if_whitespace methods. module NilIfWhitespace private diff --git a/lib/xqsr3/string_utilities/quote_if.rb b/lib/xqsr3/string_utilities/quote_if.rb index d72afa9..91e6dbd 100644 --- a/lib/xqsr3/string_utilities/quote_if.rb +++ b/lib/xqsr3/string_utilities/quote_if.rb @@ -5,7 +5,7 @@ # Purpose: Definition of the ::Xqsr3::StringUtilities::QuoteIf module # # Created: 3rd June 2017 -# Updated: 19th August 2026 +# Updated: 29th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # @@ -51,7 +51,8 @@ module Xqsr3 module StringUtilities - # +include+-able module that provides ::quote_if and #quote_if methods + # include-able module that provides ::quote_if and #quote_if + # methods. module QuoteIf private diff --git a/lib/xqsr3/string_utilities/starts_with.rb b/lib/xqsr3/string_utilities/starts_with.rb index 9804c78..f4b0c1e 100644 --- a/lib/xqsr3/string_utilities/starts_with.rb +++ b/lib/xqsr3/string_utilities/starts_with.rb @@ -5,7 +5,7 @@ # Purpose: Definition of the ::Xqsr3::StringUtilities::StartsWith module # # Created: 13th April 2016 -# Updated: 19th August 2026 +# Updated: 29th August 2026 # # Home: https://github.com/synesissoftware/xqsr3 # @@ -51,8 +51,8 @@ module Xqsr3 module StringUtilities - # +include+-able module that provides ::string_starts_with? and #starts_with? - # methods + # include-able module that provides ::string_starts_with? and + # #starts_with? methods. module StartsWith private From f08ccfd43d5c51827e5a5fd035c98e0946e9dc4d Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Sat, 29 Aug 2026 15:41:18 +1000 Subject: [PATCH 16/17] Package xqsr3 authored documentation - Include the component catalogue, guides, and reference README in the gem; - Keep generated RDoc output excluded from package contents; - Record the documentation packaging change in the release notes; --- CHANGES.md | 1 + xqsr3.gemspec | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b4ae6ff..7c7380e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ * Added task-oriented guides under **docs/guides/** for getting started, component selection, input parsing and validation, collection processing, failure handling, and output formatting; * Normalised legacy Markdown inline-code, checklist, table, and heading markup in **README.md**, **EXAMPLES.md**, **FAQ.md**, **TODO.md**, and the example guide; * Added **docs/reference/README.md** describing the generated RDoc reference, and updated **generate_rdoc.sh** to exclude authored **docs/** and **examples/** content from generated output; +* Included authored **docs/** content in the gem package while continuing to exclude generated **doc/** output; * Set the generated RDoc title to the **xqsr3** API reference, and excluded project boilerplate pages from the reference; * Expanded source-level RDoc documentation and visibility annotations for the public API, including **FrequencyMap**, **BoolParser**, **ExceptionUtilities**, **WithCause**, **ParameterChecking**, and extension classes; * Replaced a remaining placeholder example and legacy RDoc markup in public API comments; diff --git a/xqsr3.gemspec b/xqsr3.gemspec index 93b29e6..ada42a8 100644 --- a/xqsr3.gemspec +++ b/xqsr3.gemspec @@ -4,7 +4,7 @@ # Purpose: Gemspec for xqsr3 library # # Created: 14th February 2014 -# Updated: 28th August 2026 +# Updated: 29th August 2026 # # ######################################################################## # @@ -48,7 +48,7 @@ END_DESC spec.files = Dir[ 'Rakefile', - '{bin,examples,lib,man,spec,test}/**/*', + '{bin,docs,examples,lib,man,spec,test}/**/*', 'AUTHORS*', 'CHANGES*', 'CONTRIBUTING*', From 321ce5fe59b14406095578b6f0ab80c28a66fbbc Mon Sep 17 00:00:00 2001 From: Matt Wilson Date: Sat, 29 Aug 2026 15:43:48 +1000 Subject: [PATCH 17/17] Finalize xqsr3 0.39.11 release notes - Record component guides, RDoc improvements, CI coverage, and metadata updates. - Document authored documentation packaging while excluding generated output; - Correct release-note wording for cross-platform RDoc helper changes; --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 7c7380e..5349d10 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,7 +6,7 @@ * Added a user-oriented component catalogue under **docs/components/**, covering standalone components, extensions, loading paths, API summaries, and representative usage; * Added task-oriented guides under **docs/guides/** for getting started, component selection, input parsing and validation, collection processing, failure handling, and output formatting; * Normalised legacy Markdown inline-code, checklist, table, and heading markup in **README.md**, **EXAMPLES.md**, **FAQ.md**, **TODO.md**, and the example guide; -* Added **docs/reference/README.md** describing the generated RDoc reference, and updated **generate_rdoc.sh** to exclude authored **docs/** and **examples/** content from generated output; +* Added **docs/reference/README.md** describing the generated RDoc reference, and updated the Unix and Windows RDoc helpers to exclude authored **docs/** and **examples/** content from generated output; * Included authored **docs/** content in the gem package while continuing to exclude generated **doc/** output; * Set the generated RDoc title to the **xqsr3** API reference, and excluded project boilerplate pages from the reference; * Expanded source-level RDoc documentation and visibility annotations for the public API, including **FrequencyMap**, **BoolParser**, **ExceptionUtilities**, **WithCause**, **ParameterChecking**, and extension classes;