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. + + +