From 7593469f990454b3135635b0b1c1fd1496f7dd7d Mon Sep 17 00:00:00 2001 From: Yuma Endo Date: Sat, 15 Aug 2026 17:20:25 +0900 Subject: [PATCH 1/7] docs: align the examples with the API the library actually has The examples across the documentation site named an API this library does not declare: is_number()/as_number() rather than the is()/as() templates, dictionary::set and get rather than operator[], array::size rather than length, environment::set/unset/get_all next to a value() that is the only thing there, path::join/exists/read_file where the class has mkdir, home and separator, an error_code type, and parse_json/to_json for a library whose format support is TOML. The include paths were no better: of the 32 dross includes in the docs, 31 named headers that have never existed -- dross/value.h for what lives at dross/type/value.h, and so on. Nothing on the site compiled except the one snippet in the README. The examples now use what the headers declare, and every code block that includes a dross header was extracted and compiled to confirm it: 22 blocks, twice each, once against libstdc++ 15 with Clang 22 and once against libstdc++ 13 with Clang 20. The harness adds no headers of its own -- a block that needs says so -- because a block that only compiles inside a harness is not an example a reader can use. Where the library has no equivalent for what a passage showed, the passage now shows what the library does have: TOML in place of JSON, mkdir in place of create_directory, the four xdg accessors reached through an instance. Where nothing equivalent exists, the lines are gone rather than replaced with a placeholder. No section had to be dropped whole. The Code of Conduct paragraph goes too, since the file it points at is not in the tree. --- docs/sphinx/source/api/index.rst | 8 +- docs/sphinx/source/api/platform.rst | 278 ++++++++-------- docs/sphinx/source/api/type-system.rst | 210 ++++++++---- docs/sphinx/source/contributing.rst | 25 +- docs/sphinx/source/examples/index.rst | 406 ++++++++++++++---------- docs/sphinx/source/getting-started.rst | 57 ++-- docs/sphinx/source/index.rst | 61 ++-- docs/sphinx/source/user-guide/index.rst | 81 +++-- 8 files changed, 670 insertions(+), 456 deletions(-) diff --git a/docs/sphinx/source/api/index.rst b/docs/sphinx/source/api/index.rst index 53a04ec..6b7cd89 100644 --- a/docs/sphinx/source/api/index.rst +++ b/docs/sphinx/source/api/index.rst @@ -51,15 +51,15 @@ Example: .. code-block:: cpp // Using std::optional - auto env_value = environment::get("MY_VAR"); + auto env_value = environment::value("MY_VAR"); if (env_value) { std::cout << "Value: " << *env_value << std::endl; } - + // Using std::expected - auto result = path::read_file("/path/to/file"); + auto result = path::mkdir(std::string{"/path/to/dir"}); if (result) { - process_content(*result); + process_path(result->string()); } else { handle_error(result.error()); } diff --git a/docs/sphinx/source/api/platform.rst b/docs/sphinx/source/api/platform.rst index daf399d..06a6f88 100644 --- a/docs/sphinx/source/api/platform.rst +++ b/docs/sphinx/source/api/platform.rst @@ -12,28 +12,27 @@ environment :protected-members: :undoc-members: -The ``environment`` class provides access to environment variables: +The ``environment`` class provides read-only access to environment variables +through a single static accessor, ``value()``: .. code-block:: cpp - #include - - // Get environment variable - if (auto home = dross::environment::get("HOME")) { + #include + #include + + #include + + // Read an environment variable. The result is std::nullopt when the + // variable is unset or empty, so handle that case explicitly. + if (auto home = dross::environment::value("HOME")) { std::cout << "Home directory: " << *home << std::endl; + } else { + std::cout << "HOME is not set" << std::endl; } - - // Set environment variable - dross::environment::set("MY_VAR", "my_value"); - - // Remove environment variable - dross::environment::unset("MY_VAR"); - - // Get all environment variables - auto all_vars = dross::environment::get_all(); - for (const auto& [key, value] : all_vars) { - std::cout << key << "=" << value << std::endl; - } + + // Or fold the missing case into a default + std::string shell = dross::environment::value("SHELL").value_or("/bin/sh"); + std::cout << "Shell: " << shell << std::endl; path ---- @@ -48,62 +47,70 @@ The ``path`` class provides filesystem path operations: .. code-block:: cpp - #include - - // Join path components - auto config_path = dross::path::join("/home/user", ".config", "app"); - - // Get absolute path - auto abs_path = dross::path::absolute("../file.txt"); - - // Check if path exists - if (dross::path::exists("/etc/passwd")) { - std::cout << "System has passwd file" << std::endl; + #include + #include + + #include + + // home() returns std::optional; append() builds on top of it + if (auto home = dross::path::home()) { + dross::path config_path = home->append(".config").append("app"); + std::cout << "Config path: " << config_path.string() << std::endl; + + // Create the directory, including any missing parents + if (auto created = dross::path::mkdir(config_path.string())) { + std::cout << "Created: " << created->string() << std::endl; + } else { + std::cerr << "mkdir failed: " << created.error().what() << std::endl; + } } - - // Read file contents - auto result = dross::path::read_file("/path/to/file.txt"); - if (result) { - std::cout << "Content: " << *result << std::endl; + + // A bare string literal is ambiguous between the std::string and the + // std::filesystem::path constructor, so name the type you mean. + dross::path relative{std::string{"../file.txt"}}; + + // Convert to an absolute, canonical path + if (auto resolved = relative.resolve()) { + std::cout << "Resolved: " << resolved->string() << std::endl; } else { - std::cerr << "Error: " << result.error().message() << std::endl; + std::cerr << "Resolve failed: " << resolved.error().what() << std::endl; } - - // Write file contents - auto write_result = dross::path::write_file("/path/to/output.txt", - "Hello, World!"); - if (!write_result) { - std::cerr << "Write failed: " << write_result.error().message() << std::endl; + + // Check whether a path exists + if (relative.exists()) { + std::cout << "The relative path exists" << std::endl; + } + + // Expand a leading ~ to the home directory + dross::path user_config{std::string{"~/.config/app"}}; + if (auto expanded = user_config.expand()) { + std::cout << "Expanded: " << expanded->string() << std::endl; } Path Operations ~~~~~~~~~~~~~~~ -Common path operations include: +Building and inspecting a path, without touching the filesystem: -- **join()** - Join multiple path components -- **dirname()** - Get directory part of path -- **basename()** - Get filename part of path -- **extension()** - Get file extension -- **stem()** - Get filename without extension -- **absolute()** - Convert to absolute path -- **normalize()** - Normalize path (remove . and ..) -- **relative()** - Get relative path between two paths +- **append()** - Return a new path with a component appended +- **string()** - Get the native string representation +- **separator()** - Get the platform's path separator (static) -File Operations -~~~~~~~~~~~~~~~ +Filesystem Operations +~~~~~~~~~~~~~~~~~~~~~ + +Operations that consult the filesystem: -File and directory operations: +- **exists()** - Check whether the path exists +- **expand()** - Expand a leading ``~`` to the home directory +- **resolve()** - Convert to an absolute, canonical path +- **mkdir()** - Create a directory and any missing parents (static) +- **home()** - Get the user's home directory (static) -- **exists()** - Check if path exists -- **is_file()** - Check if path is a regular file -- **is_directory()** - Check if path is a directory -- **file_size()** - Get file size in bytes -- **read_file()** - Read entire file contents -- **write_file()** - Write data to file -- **create_directory()** - Create directory (with parents) -- **remove()** - Remove file or empty directory -- **remove_all()** - Remove recursively +``expand()``, ``resolve()`` and ``mkdir()`` return +``std::expected``; ``home()`` returns +``std::optional``. Reading and writing file *contents* is deliberately +not part of ``path`` — use the standard library's ```` for that. xdg --- @@ -118,47 +125,52 @@ The ``xdg`` class implements the XDG Base Directory Specification: .. code-block:: cpp - #include - - // Get user-specific data directory - auto data_home = dross::xdg::data_home(); - // Default: $HOME/.local/share - - // Get user-specific configuration directory - auto config_home = dross::xdg::config_home(); - // Default: $HOME/.config - - // Get user-specific cache directory - auto cache_home = dross::xdg::cache_home(); - // Default: $HOME/.cache - - // Get user-specific state directory - auto state_home = dross::xdg::state_home(); - // Default: $HOME/.local/state - - // Get runtime directory - if (auto runtime_dir = dross::xdg::runtime_dir()) { - std::cout << "Runtime dir: " << *runtime_dir << std::endl; + #include + + #include + + // The accessors are instance methods: the application name given here is + // appended to every directory they return. + dross::xdg app{"myapp"}; + + // User-specific data directory + if (auto data_home = app.data_home()) { + std::cout << "Data: " << *data_home << std::endl; + // Default: $HOME/.local/share/myapp + } + + // User-specific configuration directory + if (auto config_home = app.config_home()) { + std::cout << "Config: " << *config_home << std::endl; + // Default: $HOME/.config/myapp + } + + // User-specific cache directory + if (auto cache_home = app.cache_home()) { + std::cout << "Cache: " << *cache_home << std::endl; + // Default: $HOME/.cache/myapp + } + + // User-specific state directory + if (auto state_home = app.state_home()) { + std::cout << "State: " << *state_home << std::endl; + // Default: $HOME/.local/state/myapp } - - // Get system data directories - auto data_dirs = dross::xdg::data_dirs(); - // Default: /usr/local/share:/usr/share - - // Get system config directories - auto config_dirs = dross::xdg::config_dirs(); - // Default: /etc/xdg XDG Directories ~~~~~~~~~~~~~~~ -The XDG Base Directory Specification defines standard locations for: +``xdg`` exposes the four per-user base directories, each already suffixed with +the application name passed to the constructor: -- **Data files** - Application data that should persist -- **Configuration** - User-specific configuration files -- **Cache** - Non-essential cached data -- **State** - Application state data (logs, history, etc.) -- **Runtime** - Runtime files (sockets, PIDs, etc.) +- **data_home()** - Application data that should persist +- **config_home()** - User-specific configuration files +- **cache_home()** - Non-essential cached data +- **state_home()** - Application state data (logs, history, etc.) + +Every accessor returns ``std::optional`` and yields +``std::nullopt`` when the home directory cannot be determined. The directory +itself is not created for you — pass the result to ``path::mkdir()``. Example Usage ~~~~~~~~~~~~~ @@ -167,28 +179,35 @@ Creating application directories: .. code-block:: cpp - #include - #include - - // Create app-specific directories - auto app_config = dross::path::join(dross::xdg::config_home(), "myapp"); - auto app_data = dross::path::join(dross::xdg::data_home(), "myapp"); - auto app_cache = dross::path::join(dross::xdg::cache_home(), "myapp"); - - // Create directories if they don't exist - dross::path::create_directory(app_config); - dross::path::create_directory(app_data); - dross::path::create_directory(app_cache); - - // Store configuration - auto config_file = dross::path::join(app_config, "settings.json"); - dross::path::write_file(config_file, config_json); - - // Store application data - auto data_file = dross::path::join(app_data, "database.db"); - - // Store cached data - auto cache_file = dross::path::join(app_cache, "thumbnails.cache"); + #include + + #include + #include + + dross::xdg app{"myapp"}; + + // Create the config directory, then name a file inside it + if (auto config_home = app.config_home()) { + if (auto created = dross::path::mkdir(*config_home)) { + dross::path config_file = created->append("settings.toml"); + std::cout << "Config file: " << config_file.string() << std::endl; + } else { + std::cerr << "mkdir failed: " << created.error().what() << std::endl; + } + } + + // The data directory works the same way + if (auto data_home = app.data_home()) { + dross::path data_file = dross::path{*data_home}.append("database.db"); + std::cout << "Data file: " << data_file.string() << std::endl; + } + + // ...and so does the cache directory + if (auto cache_home = app.cache_home()) { + dross::path cache_file = + dross::path{*cache_home}.append("thumbnails.cache"); + std::cout << "Cache file: " << cache_file.string() << std::endl; + } Platform Considerations ----------------------- @@ -214,20 +233,21 @@ On macOS: Error Handling -------------- -All filesystem operations return ``std::expected`` for error handling: +Fallible ``path`` operations return +``std::expected``. The error type is +the standard library's, so it is inspected with ``code()`` and reported with +``what()``: .. code-block:: cpp - auto result = dross::path::read_file("/nonexistent/file"); + auto result = dross::path::mkdir(std::string{"/nonexistent/dir"}); if (!result) { - switch (result.error().code()) { - case dross::error_code::file_not_found: - std::cerr << "File not found" << std::endl; - break; - case dross::error_code::permission_denied: - std::cerr << "Permission denied" << std::endl; - break; - default: - std::cerr << "Error: " << result.error().message() << std::endl; + const std::error_code code = result.error().code(); + if (code == std::errc::no_such_file_or_directory) { + std::cerr << "No such file or directory" << std::endl; + } else if (code == std::errc::permission_denied) { + std::cerr << "Permission denied" << std::endl; + } else { + std::cerr << "Error: " << result.error().what() << std::endl; } } \ No newline at end of file diff --git a/docs/sphinx/source/api/type-system.rst b/docs/sphinx/source/api/type-system.rst index ff299b2..26f2097 100644 --- a/docs/sphinx/source/api/type-system.rst +++ b/docs/sphinx/source/api/type-system.rst @@ -20,18 +20,26 @@ The ``value`` class is the central polymorphic type that can hold any supported .. code-block:: cpp - #include + #include - dross::value v1 = 42; // Holds a number - dross::value v2 = "hello"; // Holds a string - dross::value v3 = dross::array{}; // Holds an array - dross::value v4 = true; // Holds a boolean - dross::value v5 = dross::timestamp{2024, 1, 21, 15, 30, 0, dross::timezone::offset(9)}; // Holds a timestamp + #include + #include + #include - // Type checking with seamless string conversion + dross::value v1 = 42; // Holds a number + dross::value v2 = "hello"; // Holds a string + dross::value v3 = dross::array{}; // Holds an array + dross::value v4 = dross::boolean{true}; // Holds a boolean + dross::value v5 = dross::timestamp{2024, 1, 21, 15, 30, 0, + dross::timezone::offset(9)}; // Holds a timestamp + + // A bare `true` would select the arithmetic constructor and end up as a + // number, so the boolean above is wrapped explicitly. + + // Check the type before casting: as() is undefined on a mismatch if (v1.is()) { auto num = v1.as(); - std::cout << num << std::endl; // Direct output + std::cout << num << std::endl; // number has operator<< } if (v5.is()) { @@ -52,7 +60,10 @@ The ``boolean`` class provides type-safe boolean operations: .. code-block:: cpp - #include + #include + #include + + #include dross::boolean flag{true}; dross::boolean enabled{"true"}; // From string @@ -60,7 +71,10 @@ The ``boolean`` class provides type-safe boolean operations: // Seamless string conversion std::string status = flag; // "true" - std::cout << flag << std::endl; // Direct output + std::cout << status << std::endl; + + // Direct output + std::cout << flag << " " << enabled << " " << active << std::endl; number ~~~~~~ @@ -75,15 +89,19 @@ The ``number`` class provides arbitrary precision numeric values: .. code-block:: cpp - #include + #include + #include + + #include dross::number n1(42); dross::number n2("3.14159265358979323846"); dross::number n3 = n1 + n2; // Seamless string conversion - std::string result = n3; // Direct conversion - std::cout << n3 << std::endl; // Direct output + std::string result = n3; // Direct conversion + std::cout << result << std::endl; // Via std::string + std::cout << n3 << std::endl; // Direct output string ~~~~~~ @@ -98,15 +116,23 @@ The ``string`` class provides Unicode-aware string handling: .. code-block:: cpp - #include + #include + #include + + #include dross::string s1("Hello"); dross::string s2(" World"); - dross::string s3 = s1 + s2; - // Seamless string conversion - std::string result = s3; // Direct conversion - std::cout << s3 << std::endl; // Direct output + // Concatenation is in place; there is no operator+ + s1 += s2; + + // Length is counted in Unicode code points, not bytes + std::cout << s1.length() << std::endl; // 11 + + // Seamless conversion to std::string, which is what streams accept + std::string result = s1; // "Hello World" + std::cout << result << std::endl; timestamp ~~~~~~~~~ @@ -121,19 +147,27 @@ The ``timestamp`` class provides comprehensive date and time handling with timez .. code-block:: cpp - #include + #include + #include + #include - // Construction with timezone objects + #include + #include + + // Construction from components, with a timezone object dross::timestamp meeting{2024, 1, 21, 15, 30, 0, dross::timezone::offset(9)}; // +09:00 dross::timestamp utc_meeting{2024, 1, 21, 6, 30, 0, dross::timezone::utc()}; // UTC - // Date-only timestamps (time defaults to 00:00:00) - dross::timestamp birthday{1990, 12, 25}; // Date only, UTC timezone + // Date-only timestamps (time defaults to 00:00:00, timezone to UTC) + dross::timestamp birthday{1990, 12, 25}; - // Construction from ISO 8601 strings - dross::timestamp utc_time{"2024-01-21T15:30:00Z"}; + // Construction from ISO 8601 strings. Each of these compares equal to the + // component-built timestamp above it. dross::timestamp offset_time{"2024-01-21T15:30:00+09:00"}; - dross::timestamp date_only{"2024-01-21"}; + dross::timestamp utc_time{"2024-01-21T06:30:00Z"}; + dross::timestamp date_only{"1990-12-25"}; + std::cout << (offset_time == meeting) << " " << (utc_time == utc_meeting) + << " " << (date_only == birthday) << std::endl; // 1 1 1 // Current time dross::timestamp now = dross::timestamp::now(); @@ -141,10 +175,12 @@ The ``timestamp`` class provides comprehensive date and time handling with timez // Duration arithmetic auto tomorrow = now + std::chrono::hours(24); auto next_week = now + std::chrono::hours(24 * 7); + std::cout << tomorrow << " " << next_week << std::endl; // Formatting std::string iso_str = meeting.format(); // ISO 8601 format std::string custom = meeting.format("%Y-%m-%d %H:%M"); + std::cout << iso_str << " / " << custom << std::endl; // Component access (always present) const auto& date_part = meeting.date(); @@ -157,8 +193,12 @@ The ``timestamp`` class provides comprehensive date and time handling with timez int minute = time_part.minute(); int second = time_part.second(); + std::cout << year << "/" << month << "/" << day << " " + << hour << ":" << minute << ":" << second << std::endl; + const auto& tz = meeting.timezone(); // Always present (default UTC) auto offset_minutes = tz.offset(); // Returns std::chrono::minutes + std::cout << offset_minutes.count() << " minutes" << std::endl; // Timezone operations if (meeting.timezone().is_utc()) { @@ -168,7 +208,8 @@ The ``timestamp`` class provides comprehensive date and time handling with timez // Seamless string conversion std::string meeting_str = meeting; // "2024-01-21T15:30:00+09:00" - std::cout << meeting << std::endl; // Direct output + std::cout << meeting_str << std::endl; + std::cout << utc_meeting << " " << birthday << std::endl; // Direct output timezone ~~~~~~~~ @@ -183,7 +224,11 @@ The ``timezone`` class provides type-safe timezone representation with modern ch .. code-block:: cpp - #include + #include + #include + #include + + #include // Factory methods for common timezones auto utc = dross::timezone::utc(); // UTC (+00:00) @@ -194,6 +239,7 @@ The ``timezone`` class provides type-safe timezone representation with modern ch // Chrono-based factory method for type safety auto cet = dross::timezone::offset(std::chrono::minutes(60)); // Central European Time (+01:00) auto jst_chrono = dross::timezone::offset(std::chrono::minutes(540)); // +09:00 + std::cout << pdt << " " << ist << " " << cet << " " << jst_chrono << std::endl; // Parse from ISO 8601 strings (returns optional for error handling) if (auto parsed_utc = dross::timezone::from_string("Z")) { @@ -221,10 +267,11 @@ The ``timezone`` class provides type-safe timezone representation with modern ch // Formatting and string conversion std::string utc_str = utc.format(); // "Z" std::string jst_str = jst.format(); // "+09:00" + std::cout << utc_str << " " << jst_str << std::endl; // Implicit string conversion std::string tz_string = jst; // "+09:00" - std::cout << "Timezone: " << jst << std::endl; + std::cout << "Timezone: " << tz_string << " " << jst << std::endl; array ~~~~~ @@ -239,16 +286,31 @@ The ``array`` class provides a dynamic array of values: .. code-block:: cpp - #include + #include + #include + + #include + #include dross::array arr; arr.append(42); arr.append("hello"); arr.append(dross::array{1, 2, 3}); - // Range-based for loop + std::cout << "Length: " << arr.length() << std::endl; // 3 + + // Range-based for loop. value itself has no operator<<, so dispatch on + // the contained type and print that. for (const auto& val : arr) { - std::cout << val << std::endl; // Direct stream output + if (val.is()) { + std::cout << val.as() << std::endl; + } else if (val.is()) { + std::string text = val.as(); + std::cout << text << std::endl; + } else if (val.is()) { + std::cout << "array of " << val.as().length() + << std::endl; + } } dictionary @@ -264,15 +326,32 @@ The ``dictionary`` class provides key-value storage: .. code-block:: cpp - #include + #include + #include + + #include + #include dross::dictionary dict; - dict.set("name", "John Doe"); - dict.set("age", 30); - dict.set("active", true); - if (auto name = dict.get("name")) { - std::cout << "Name: " << name->to_string() << std::endl; + // Name the dross type on the right-hand side. A bare `dict["age"] = 30;` + // is ambiguous between value's boolean, number and value assignment + // operators, and `dross::value{x}` with braces selects the + // initializer-list constructor, producing a one-element array. + dict["name"] = dross::string("John Doe"); + dict["age"] = dross::number(30); + dict["active"] = dross::boolean(true); + + std::cout << "Size: " << dict.size() << std::endl; // 3 + + // operator[] inserts a default-constructed value for a missing key, + // so ask contains() first when you only mean to read. + if (dict.contains("name")) { + const dross::value& name = dict["name"]; + if (name.is()) { + std::string text = name.as(); + std::cout << "Name: " << text << std::endl; + } } data @@ -288,7 +367,11 @@ The ``data`` class provides raw byte storage: .. code-block:: cpp - #include + #include + #include + #include + + #include std::vector bytes = {0x48, 0x65, 0x6C, 0x6C, 0x6F}; dross::data d(bytes); @@ -308,11 +391,19 @@ The ``error`` class provides structured error information: .. code-block:: cpp - #include + #include + #include + + #include - dross::error err(dross::error_code::invalid_argument, - "Invalid value provided"); + // error wraps a std::error_code: a numeric value plus its category. + // std::errc is an error *condition* enum, so it is converted explicitly + // rather than passed to the error_enum_type constructor. + dross::error err{static_cast(std::errc::invalid_argument), + std::generic_category()}; + std::cout << "Domain: " << err.domain() << std::endl; + std::cout << "Code: " << err.code() << std::endl; std::cout << "Error: " << err.message() << std::endl; Type Concepts @@ -320,31 +411,38 @@ Type Concepts The type system uses C++20 concepts to constrain template parameters: -.. doxygenconcept:: dross::value_type - :project: dross - .. doxygenconcept:: dross::number_type :project: dross .. doxygenconcept:: dross::string_type :project: dross -.. doxygenconcept:: dross::array_type +.. doxygenconcept:: dross::error_enum_type :project: dross -.. doxygenconcept:: dross::dictionary_type +.. doxygenconcept:: dross::container_type :project: dross Type Conversion --------------- -All types provide seamless string conversion through multiple approaches: +``boolean``, ``number``, ``string``, ``data``, ``timestamp`` and ``timezone`` +convert to ``std::string`` in two ways: - **Implicit conversion**: ``std::string s = type_instance;`` -- **STL-style function**: ``std::string s = to_string(type_instance);`` -- **Stream output**: ``std::cout << type_instance;`` -- ``as_T()`` - Convert value to specific type T (for value type) -- ``is_T()`` - Check if value is of type T (for value type) +- **STL-style function**: ``std::string s = to_string(type_instance);`` — + declared in ````, not in the individual type headers + +Stream output is provided for ``boolean``, ``number``, ``data``, ``timestamp``, +``timezone`` and ``error``. ``string`` and ``value`` have no ``operator<<``: +convert a ``string`` to ``std::string`` first, and unwrap a ``value`` before +printing it. + +``value`` is inspected and unwrapped with member templates: + +- ``is()`` - Check whether the value currently holds type ``T`` +- ``as()`` - Retrieve the value as type ``T``; undefined unless ``is()`` + is true first Example: @@ -409,15 +507,13 @@ Type operations that may fail use ``std::optional`` or ``std::expected``: .. code-block:: cpp - dross::dictionary dict; - - // Returns std::optional - if (auto val = dict.get("key")) { - process(*val); + // Returns std::optional + if (auto tz = dross::timezone::from_string("+09:00")) { + process(tz->format()); } // Error handling with expected - auto result = parse_json(json_string); + auto result = dross::toml::deserialize(toml_input); if (!result) { std::cerr << "Parse error: " << result.error().message() << std::endl; } diff --git a/docs/sphinx/source/contributing.rst b/docs/sphinx/source/contributing.rst index edf1de2..d2f125e 100644 --- a/docs/sphinx/source/contributing.rst +++ b/docs/sphinx/source/contributing.rst @@ -131,18 +131,23 @@ Tests use GoogleTest and are located in the ``test/`` directory: .. code-block:: cpp + #include + #include - #include - TEST(ValueTest, DefaultConstruction) { - dross::value v; - EXPECT_TRUE(v.is_null()); - } + #include TEST(ValueTest, NumberConstruction) { dross::value v(42); - EXPECT_TRUE(v.is_number()); - EXPECT_EQ(v.as_number(), dross::number(42)); + EXPECT_TRUE(v.is()); + EXPECT_EQ(v.as(), dross::number(42)); + } + + TEST(ValueTest, StringConstruction) { + dross::value v("hello"); + ASSERT_TRUE(v.is()); + const std::string text = v.as(); + EXPECT_EQ(text, "hello"); } Running Tests @@ -282,12 +287,6 @@ Community - **GitHub Discussions**: General questions and discussions - **Pull Requests**: Code contributions -Code of Conduct ---------------- - -Please note that this project is released with a Contributor Code of Conduct. -By participating in this project you agree to abide by its terms. - License ------- diff --git a/docs/sphinx/source/examples/index.rst b/docs/sphinx/source/examples/index.rst index fd24584..3cde4b2 100644 --- a/docs/sphinx/source/examples/index.rst +++ b/docs/sphinx/source/examples/index.rst @@ -20,37 +20,55 @@ Working with Values .. code-block:: cpp #include - #include - + + // The umbrella header: to_string() is declared here, not in the + // individual type headers. + #include + using namespace dross; - + void print_type_info(const value& v) { - std::cout << "Value: " << v << std::endl; // Direct stream output std::cout << "Type: "; - - if (v.is_null()) std::cout << "null"; - else if (v.is_boolean()) std::cout << "boolean"; - else if (v.is_number()) std::cout << "number"; - else if (v.is_string()) std::cout << "string"; - else if (v.is_array()) std::cout << "array"; - else if (v.is_dictionary()) std::cout << "dictionary"; - else if (v.is_data()) std::cout << "data"; - else if (v.is_error()) std::cout << "error"; - - std::cout << std::endl << std::endl; + + // is() is the only way to ask what a value holds. A default + // constructed value holds none of these, and no separate predicate + // for the empty case exists. + if (v.is()) { + std::cout << "boolean: " << to_string(v.as()); + } else if (v.is()) { + std::cout << "number: " << to_string(v.as()); + } else if (v.is()) { + std::cout << "string: " << to_string(v.as()); + } else if (v.is()) { + std::cout << "timestamp: " << to_string(v.as()); + } else if (v.is()) { + std::cout << "data: " << to_string(v.as()); + } else if (v.is()) { + // array and dictionary have no to_string overload + std::cout << "array of " << v.as().length(); + } else if (v.is()) { + std::cout << "dictionary of " << v.as().size(); + } else { + std::cout << "empty"; + } + + std::cout << std::endl; } - + int main() { + dictionary dict; + dict["x"] = number(1); + // Different value types - print_type_info(value()); // null - print_type_info(value(true)); // boolean + print_type_info(value()); // empty + print_type_info(value(boolean(true))); // boolean print_type_info(value(42)); // number print_type_info(value("hello")); // string - print_type_info(value(array{1, 2, 3})); // array - print_type_info(value(dictionary{{"x", 1}})); // dictionary - + print_type_info(value(array{1, 2, 3})); // array + print_type_info(value(dict)); // dictionary + return 0; } @@ -59,60 +77,66 @@ Building Data Structures .. code-block:: cpp + #include #include - #include - #include - + #include + + #include + #include + #include + using namespace dross; - - dictionary create_person(const string& name, int age, - const array& hobbies) + + dictionary create_person(const string& name, int age, + const array& hobbies) { dictionary person; - person.set("name", name); - person.set("age", age); - person.set("hobbies", hobbies); - person.set("created", "2024-01-20"); - + person["name"] = name; + person["age"] = number(age); + person["hobbies"] = hobbies; + person["created"] = string("2024-01-20"); + return person; } - + int main() { // Create a list of people array people; - - people.append(create_person("Alice", 30, - array{"reading", "hiking"})); - people.append(create_person("Bob", 25, - array{"gaming", "cooking"})); - people.append(create_person("Charlie", 35, - array{"photography", "travel"})); - - // Create a database-like structure + + people.append(create_person("Alice", 30, + array{"reading", "hiking"})); + people.append(create_person("Bob", 25, + array{"gaming", "cooking"})); + people.append(create_person("Charlie", 35, + array{"photography", "travel"})); + + // Create a database-like structure. array reports its size through + // length(); dictionary and data use size(). dictionary database; - database.set("version", "1.0"); - database.set("people", people); - database.set("count", people.size()); - + database["version"] = string("1.0"); + database["people"] = people; + database["count"] = number(people.length()); + // Access and print data - if (auto people_val = database.get("people")) { - if (people_val->is_array()) { - auto people_array = people_val->as_array(); - - for (size_t i = 0; i < people_array.size(); ++i) { - if (people_array[i].is_dictionary()) { - auto person = people_array[i].as_dictionary(); - - if (auto name = person.get("name")) { - std::cout << "Person " << i + 1 << ": " - << name->to_string() << std::endl; - } - } + if (database.contains("people") && database["people"].is()) { + array people_array = database["people"].as(); + + for (size_t i = 0; i < people_array.length(); ++i) { + const value& entry = people_array[i]; + if (!entry.is()) { + continue; + } + + dictionary person = entry.as(); + if (person.contains("name") && person["name"].is()) { + std::string name = person["name"].as(); + std::cout << "Person " << i + 1 << ": " + << name << std::endl; } } } - + return 0; } @@ -122,90 +146,111 @@ Advanced Examples Configuration Management ~~~~~~~~~~~~~~~~~~~~~~~~ +dross parses TOML, and leaves file I/O to the standard library. The two meet at +``data``, which is what ``toml::deserialize`` consumes and ``toml::serialize`` +produces. + .. code-block:: cpp - #include - #include - #include - + #include + #include + #include + #include + #include + #include + + #include + #include + #include + #include + #include + using namespace dross; - + class config_manager { private: dictionary _config; - string _config_path; - + path _config_path; + public: - config_manager() + explicit config_manager(const path& config_path) + : _config_path(config_path) { - // Determine config path - auto home = environment::get("HOME").value_or("/tmp"); - _config_path = path::join(home, ".config", "myapp", "config.json"); - - // Set defaults set_defaults(); - - // Load user config if exists - load(); } - + void set_defaults() { - _config.set("theme", "dark"); - _config.set("language", "en"); - _config.set("auto_save", true); - _config.set("save_interval", 300); // 5 minutes - + _config["theme"] = string("dark"); + _config["language"] = string("en"); + _config["auto_save"] = boolean(true); + _config["save_interval"] = number(300); // 5 minutes + dictionary window; - window.set("width", 1024); - window.set("height", 768); - window.set("maximized", false); - _config.set("window", window); + window["width"] = number(1024); + window["height"] = number(768); + window["maximized"] = boolean(false); + _config["window"] = window; } - + + // Merges the file over the defaults. A missing file is not an error: + // the defaults simply stay in place. std::expected load() { - auto content = path::read_file(_config_path); - if (!content) { - // File doesn't exist, use defaults + std::ifstream input{_config_path.string(), std::ios::binary}; + if (!input) { return {}; } - - auto parsed = parse_json(*content); + + const std::string text{std::istreambuf_iterator{input}, + std::istreambuf_iterator{}}; + + const auto parsed = toml::deserialize(data{text}); if (!parsed) { return std::unexpected(parsed.error()); } - - // Merge with defaults - if (parsed->is_dictionary()) { - merge_config(parsed->as_dictionary()); + + for (const auto& [key, val] : *parsed) { + _config[key] = val; } - + return {}; } - - std::expected save() + + std::expected save() const { - auto json = to_json(_config); - return path::write_file(_config_path, json); + const auto serialized = toml::serialize(_config); + if (!serialized) { + return std::unexpected(serialized.error()); + } + + std::ofstream output{_config_path.string(), std::ios::binary}; + const std::string text = *serialized; + output << text; + if (!output) { + return std::unexpected( + error{static_cast(std::errc::io_error), + std::generic_category()}); + } + + return {}; } - - std::optional get(const string& key) const + + bool contains(const std::string& key) const { - return _config.get(key); + return _config.contains(key); } - - void set(const string& key, const value& val) + + // Callers must check contains() first: the const operator[] throws + // std::out_of_range for a key that is not present. + const value& get(const std::string& key) const { - _config.set(key, val); + return _config[key]; } - - private: - void merge_config(const dictionary& user_config) + + void set(const std::string& key, const value& val) { - for (const auto& [key, value] : user_config) { - _config.set(key, value); - } + _config[key] = val; } }; @@ -214,18 +259,20 @@ Data Processing Pipeline .. code-block:: cpp - #include - #include - #include - #include - + #include + #include + + #include + #include + #include + using namespace dross; - + class data_processor { public: // Filter items based on a condition - array filter(const array& items, - std::function predicate) + array filter(const array& items, + std::function predicate) { array result; for (const auto& item : items) { @@ -235,10 +282,10 @@ Data Processing Pipeline } return result; } - + // Transform items using a function - array map(const array& items, - std::function transform) + array map(const array& items, + std::function transform) { array result; for (const auto& item : items) { @@ -246,11 +293,11 @@ Data Processing Pipeline } return result; } - + // Reduce array to single value - value reduce(const array& items, - std::function reducer, - const value& initial) + value reduce(const array& items, + std::function reducer, + const value& initial) { value result = initial; for (const auto& item : items) { @@ -258,84 +305,98 @@ Data Processing Pipeline } return result; } - + // Group items by a key dictionary group_by(const array& items, - std::function key_func) + std::function key_func) { dictionary groups; - + for (const auto& item : items) { - string key = key_func(item); - - if (auto group = groups.get(key)) { - if (group->is_array()) { - auto arr = group->as_array(); - arr.append(item); - groups.set(key, arr); - } + const string key = key_func(item); + + if (groups.contains(key) && groups[key].is()) { + array group = groups[key].as(); + group.append(item); + groups[key] = group; } else { - groups.set(key, array{item}); + groups[key] = array{item}; } } - + return groups; } }; - + // Example usage int main() { - // Sample data: list of products - array products{ - dictionary{{"name", "Laptop"}, {"price", 999}, {"category", "Electronics"}}, - dictionary{{"name", "Mouse"}, {"price", 29}, {"category", "Electronics"}}, - dictionary{{"name", "Desk"}, {"price", 299}, {"category", "Furniture"}}, - dictionary{{"name", "Chair"}, {"price", 199}, {"category", "Furniture"}}, - dictionary{{"name", "Monitor"}, {"price", 399}, {"category", "Electronics"}} + // dictionary has no initializer-list constructor, so entries are + // assigned after construction. + auto make_product = [](const char* name, int price, + const char* category) { + dictionary product; + product["name"] = string(name); + product["price"] = number(price); + product["category"] = string(category); + return product; }; - + + // Sample data: list of products + array products; + products.append(make_product("Laptop", 999, "Electronics")); + products.append(make_product("Mouse", 29, "Electronics")); + products.append(make_product("Desk", 299, "Furniture")); + products.append(make_product("Chair", 199, "Furniture")); + products.append(make_product("Monitor", 399, "Electronics")); + data_processor processor; - + // Filter expensive items (price > 200) auto expensive = processor.filter(products, [](const value& v) { - if (v.is_dictionary()) { - auto dict = v.as_dictionary(); - if (auto price = dict.get("price")) { - return price->as_number() > number(200); + if (v.is()) { + dictionary product = v.as(); + if (product.contains("price") && + product["price"].is()) { + return product["price"].as() > number(200); } } return false; }); - + // Calculate total price - auto total = processor.reduce(products, + auto total = processor.reduce(products, [](const value& sum, const value& item) { - if (item.is_dictionary()) { - auto dict = item.as_dictionary(); - if (auto price = dict.get("price")) { - return sum.as_number() + price->as_number(); + if (item.is()) { + dictionary product = item.as(); + if (product.contains("price") && + product["price"].is()) { + return value(sum.as() + + product["price"].as()); } } return sum; - }, - number(0) + }, + value(number(0)) ); - + // Group by category auto by_category = processor.group_by(products, [](const value& v) { - if (v.is_dictionary()) { - auto dict = v.as_dictionary(); - if (auto cat = dict.get("category")) { - return cat->as_string(); + if (v.is()) { + dictionary product = v.as(); + if (product.contains("category") && + product["category"].is()) { + return product["category"].as(); } } return string("Unknown"); }); - - std::cout << "Total value: $" << total << std::endl; // Direct stream output + + // value has no operator<<, so unwrap it before printing + std::cout << "Expensive items: " << expensive.length() << std::endl; + std::cout << "Total value: $" << total.as() << std::endl; std::cout << "Categories: " << by_category.size() << std::endl; - + return 0; } @@ -349,4 +410,5 @@ For more examples, visit: - :doc:`configuration` - Configuration file handling - :doc:`data-structures` - Building complex data structures -You can also find runnable examples in the `examples/` directory of the source repository. \ No newline at end of file +The library's own test suite, under ``test/`` in the source repository, is a +further source of compiling, executable usage. \ No newline at end of file diff --git a/docs/sphinx/source/getting-started.rst b/docs/sphinx/source/getting-started.rst index 4449cb8..74fdc7d 100644 --- a/docs/sphinx/source/getting-started.rst +++ b/docs/sphinx/source/getting-started.rst @@ -151,43 +151,52 @@ Here's a simple example using the dross type system: .. code-block:: cpp #include - #include - #include - #include - + #include + + #include + #include + #include + int main() { using namespace dross; - - // Create a dictionary with mixed types + + // Create a dictionary with mixed types. Name the dross type on the + // right-hand side: a bare literal is ambiguous between value's + // boolean, number and value assignment operators, and value{x} with + // braces builds a one-element array instead of holding x. dictionary config; - config.set("name", "My Application"); - config.set("version", 1.0); - config.set("debug", true); - + config["name"] = string("My Application"); + config["version"] = number("1.0"); + config["debug"] = boolean(true); + // Create an array of features array features; features.append("logging"); features.append("caching"); features.append("monitoring"); - - config.set("features", features); - - // Access values - if (auto name = config.get("name")) { - std::cout << "Application: " << name->to_string() << std::endl; + + config["features"] = features; + + // Access values. operator[] would insert a default value for a key + // that is absent, so ask contains() before reading. + if (config.contains("name") && config["name"].is()) { + std::string name = config["name"].as(); + std::cout << "Application: " << name << std::endl; } - - if (auto feat_val = config.get("features")) { - if (feat_val->is_array()) { - auto feat_array = feat_val->as_array(); - std::cout << "Features:" << std::endl; - for (const auto& feature : feat_array) { - std::cout << " - " << feature << std::endl; // Direct stream output + + if (config.contains("features") && config["features"].is()) { + array feature_array = config["features"].as(); + std::cout << "Features:" << std::endl; + for (const auto& feature : feature_array) { + // value has no operator<<; unwrap it first + if (feature.is()) { + std::string text = feature.as(); + std::cout << " - " << text << std::endl; } } } - + return 0; } diff --git a/docs/sphinx/source/index.rst b/docs/sphinx/source/index.rst index 63c9f72..120a6b1 100644 --- a/docs/sphinx/source/index.rst +++ b/docs/sphinx/source/index.rst @@ -122,21 +122,29 @@ The dross type system provides dynamic typing with strong value semantics: .. code-block:: cpp - #include - + #include + + #include + #include + #include + using namespace dross; - + + // dictionary has no initializer-list constructor + dictionary dict; + dict["key"] = string("value"); + // Create various types - value v1 = 42; // number - value v2 = "hello world"; // string - value v3 = array{1, 2, 3}; // array - value v4 = dictionary{{"key", "value"}}; // dictionary - value v5 = true; // boolean - - // Type checking - if (v1.is_number()) { - auto n = v1.as_number(); - std::cout << "Number: " << n << std::endl; // Direct stream output + value v1 = 42; // number + value v2 = "hello world"; // string + value v3 = array{1, 2, 3}; // array + value v4 = dict; // dictionary + value v5 = boolean{true}; // boolean + + // Type checking, then casting + if (v1.is()) { + auto n = v1.as(); + std::cout << "Number: " << n << std::endl; // number has operator<< } Platform Utilities @@ -146,18 +154,27 @@ Cross-platform utilities for common operations: .. code-block:: cpp - #include - #include - #include - + #include + #include + + #include + #include + #include + + using namespace dross; + // Environment variables - auto home = environment::get("HOME"); - + const std::string home = environment::value("HOME").value_or("/tmp"); + // Path operations - auto config_dir = path::join(home.value_or("/tmp"), ".config"); - + const path config_dir = path{home}.append(".config"); + std::cout << "Config: " << config_dir.string() << std::endl; + // XDG Base Directory support - auto data_home = xdg::data_home(); + xdg app{"myapp"}; + if (auto data_home = app.data_home()) { + std::cout << "Data: " << *data_home << std::endl; + } Features -------- diff --git a/docs/sphinx/source/user-guide/index.rst b/docs/sphinx/source/user-guide/index.rst index 87f0fe1..89bb389 100644 --- a/docs/sphinx/source/user-guide/index.rst +++ b/docs/sphinx/source/user-guide/index.rst @@ -83,20 +83,27 @@ Type System .. code-block:: cpp using namespace dross; - - // Dynamic typing with type safety - value data = dictionary{ - {"users", array{ - dictionary{{"name", "Alice"}, {"age", 30}}, - dictionary{{"name", "Bob"}, {"age", 25}} - }}, - {"count", 2} - }; - - // Safe access with optional - if (auto users = data.as_dictionary().get("users")) { - if (users->is_array()) { - for (const auto& user : users->as_array()) { + + // dictionary has no initializer-list constructor, so it is built up + dictionary alice; + alice["name"] = string("Alice"); + alice["age"] = number(30); + + dictionary bob; + bob["name"] = string("Bob"); + bob["age"] = number(25); + + dictionary root; + root["users"] = array{alice, bob}; + root["count"] = number(2); + + value data = root; + + // Safe access: ask contains() before reading, is() before casting + if (data.is()) { + dictionary top = data.as(); + if (top.contains("users") && top["users"].is()) { + for (const auto& user : top["users"].as()) { // Process each user } } @@ -110,22 +117,26 @@ Error Handling // Function returning optional std::optional get_env_config(const string& key) { - if (auto value = environment::get(key)) { // Implicit string conversion - return string(*value); + if (auto found = environment::value(key)) { // Implicit string conversion + return string(*found); } return std::nullopt; } - - // Function returning expected - std::expected load_config(const string& path) + + // Function returning expected. dross parses TOML; reading the bytes is + // left to the standard library. + std::expected load_config(const path& file) { - auto result = read_file(path); - if (!result) { - return std::unexpected(error(error_code::file_not_found, - "Config file not found")); + std::ifstream input{file.string(), std::ios::binary}; + if (!input) { + return std::unexpected( + error{static_cast(std::errc::no_such_file_or_directory), + std::generic_category()}); } - - return parse_json(*result); + + const std::string text{std::istreambuf_iterator{input}, + std::istreambuf_iterator{}}; + return toml::deserialize(data{text}); } Platform Utilities @@ -134,15 +145,15 @@ Platform Utilities .. code-block:: cpp // Working with paths - auto home = environment::get("HOME").value_or("/tmp"); - auto config_path = path::join(home, ".config", "myapp"); - - // XDG directories - auto data_home = xdg::data_home(); - auto app_data = path::join(data_home, "myapp"); - + const std::string home = environment::value("HOME").value_or("/tmp"); + const path config_path = path{home}.append(".config").append("myapp"); + + // XDG directories: the application name is already part of the result + xdg app{"myapp"}; + const std::string app_data = app.data_home().value_or(config_path.string()); + // Create directory if needed - if (auto result = path::create_directory(app_data); !result) { - std::cerr << "Failed to create directory: " - << result.error().message() << std::endl; + if (auto result = path::mkdir(app_data); !result) { + std::cerr << "Failed to create directory: " + << result.error().what() << std::endl; } \ No newline at end of file From a45f2381c1e369d1a8f263fcaf5cedb3c0be5d20 Mon Sep 17 00:00:00 2001 From: Yuma Endo Date: Sun, 16 Aug 2026 08:48:52 +0900 Subject: [PATCH 2/7] docs: correct what the documentation says the library does The rewrite took its wording from the Doxygen comments on the headers, and four of those describe behaviour the implementation does not have. So the examples inherited the errors: length() counts UTF-8 bytes rather than code points (src/type/string.cpp:41), as() on a mismatch returns a default-constructed T rather than being undefined (src/type/value.cpp:169), mkdir() reports failure when the directory is already there rather than succeeding (src/platform/path.cpp:19), and value() yields an optional holding an empty string for a variable set to nothing, rather than nullopt (src/platform/environment.cpp:9). Both the prose and those four comments now say what the code does. The comments are the only thing changed under include/; no code moved. For as() the wording stops at "call is() first, the result is unspecified otherwise, and this does not throw". Saying a default T comes back would document an accident: the contract calls the case undefined, so nothing should be built on top of what it happens to return today. Two implementation faults get written down rather than smoothed over. mkdir()'s failure on an existing directory carries no diagnostic code, so the error reads "failed: Success". And expand() lets a filesystem_error escape when the target is missing, though its return type says otherwise -- resolve() catches the same condition. The pages say so, and point at resolve() for paths that may not exist yet. The JSON example page goes: the library parses TOML, and no json entry point exists. Its toctree entry and cross-reference go with it, leaving no dangling links. --- docs/sphinx/source/api/index.rst | 14 ++++- docs/sphinx/source/api/platform.rst | 55 ++++++++++++++----- docs/sphinx/source/api/type-system.rst | 35 ++++++++---- docs/sphinx/source/contributing.rst | 2 +- docs/sphinx/source/examples/index.rst | 4 +- .../source/examples/json-processing.rst | 11 ---- docs/sphinx/source/getting-started.rst | 2 +- docs/sphinx/source/index.rst | 2 +- docs/sphinx/source/user-guide/index.rst | 13 +++-- include/dross/platform/environment.h | 3 +- include/dross/platform/path.h | 7 ++- include/dross/type/string.h | 8 +-- include/dross/type/value.h | 4 +- 13 files changed, 101 insertions(+), 59 deletions(-) delete mode 100644 docs/sphinx/source/examples/json-processing.rst diff --git a/docs/sphinx/source/api/index.rst b/docs/sphinx/source/api/index.rst index 6b7cd89..057b372 100644 --- a/docs/sphinx/source/api/index.rst +++ b/docs/sphinx/source/api/index.rst @@ -41,11 +41,21 @@ The dross library follows consistent naming conventions: Error Handling -------------- -dross does not use exceptions. All operations that may fail return either: +Operations that may fail report it through the return type rather than by +throwing: - ``std::optional`` for operations that may not produce a value - ``std::expected`` for operations that may fail with error information +Two kinds of operation are exceptions to that rule. The bounds-checked +accessors — the const ``dictionary::operator[]``, ``array::operator[]`` and +``array::value_at()`` — throw ``std::out_of_range`` when the key or index is +not present, so ask ``dictionary::contains()`` or ``array::length()`` before +indexing. And ``path::expand()``, despite returning ``std::expected``, lets a +``std::filesystem::filesystem_error`` escape when the path begins with ``~`` +and the expanded location does not exist; ``path::resolve()`` catches that +condition and returns it. + Example: .. code-block:: cpp @@ -81,4 +91,4 @@ Unless otherwise documented: - Types are not thread-safe for modification - Const operations are thread-safe -- Copy construction and assignment create independent instances \ No newline at end of file +- Copy construction and assignment create independent instances diff --git a/docs/sphinx/source/api/platform.rst b/docs/sphinx/source/api/platform.rst index 06a6f88..f53652a 100644 --- a/docs/sphinx/source/api/platform.rst +++ b/docs/sphinx/source/api/platform.rst @@ -57,11 +57,16 @@ The ``path`` class provides filesystem path operations: dross::path config_path = home->append(".config").append("app"); std::cout << "Config path: " << config_path.string() << std::endl; - // Create the directory, including any missing parents - if (auto created = dross::path::mkdir(config_path.string())) { - std::cout << "Created: " << created->string() << std::endl; - } else { - std::cerr << "mkdir failed: " << created.error().what() << std::endl; + // Create the directory, including any missing parents. mkdir() + // reports failure when the directory is already there, so test for + // it first if re-running must succeed. + if (!config_path.exists()) { + if (auto created = dross::path::mkdir(config_path.string())) { + std::cout << "Created: " << created->string() << std::endl; + } else { + std::cerr << "mkdir failed: " << created.error().what() + << std::endl; + } } } @@ -81,7 +86,9 @@ The ``path`` class provides filesystem path operations: std::cout << "The relative path exists" << std::endl; } - // Expand a leading ~ to the home directory + // Expand a leading ~ to the home directory. The expanded location has to + // exist: on a missing target expand() lets a filesystem_error escape + // instead of returning one, so reach for resolve() when unsure. dross::path user_config{std::string{"~/.config/app"}}; if (auto expanded = user_config.expand()) { std::cout << "Expanded: " << expanded->string() << std::endl; @@ -107,11 +114,24 @@ Operations that consult the filesystem: - **mkdir()** - Create a directory and any missing parents (static) - **home()** - Get the user's home directory (static) -``expand()``, ``resolve()`` and ``mkdir()`` return +``expand()``, ``resolve()`` and ``mkdir()`` are declared to return ``std::expected``; ``home()`` returns ``std::optional``. Reading and writing file *contents* is deliberately not part of ``path`` — use the standard library's ```` for that. +Two caveats apply to the current implementation: + +- ``mkdir()`` succeeds only when it actually creates the directory. If the + path already exists it returns an error, and that error carries no + diagnostic code. Guard the call with ``exists()`` when an + already-provisioned directory should not be treated as a failure. +- ``expand()`` does not route every failure through its return type. When the + path begins with ``~`` and the expanded location does not exist, a + ``std::filesystem::filesystem_error`` escapes the call instead of being + returned, which terminates a program that is not catching it. ``resolve()`` + catches the same condition and returns it as an error, so prefer + ``resolve()`` when the target may be absent. + xdg --- @@ -170,7 +190,8 @@ the application name passed to the constructor: Every accessor returns ``std::optional`` and yields ``std::nullopt`` when the home directory cannot be determined. The directory -itself is not created for you — pass the result to ``path::mkdir()``. +itself is not created for you — pass the result to ``path::mkdir()``, keeping +in mind that ``mkdir()`` reports an already-existing directory as an error. Example Usage ~~~~~~~~~~~~~ @@ -186,14 +207,18 @@ Creating application directories: dross::xdg app{"myapp"}; - // Create the config directory, then name a file inside it + // Create the config directory, then name a file inside it. mkdir() only + // succeeds when it creates the directory, so skip it if it is there. if (auto config_home = app.config_home()) { - if (auto created = dross::path::mkdir(*config_home)) { - dross::path config_file = created->append("settings.toml"); - std::cout << "Config file: " << config_file.string() << std::endl; - } else { - std::cerr << "mkdir failed: " << created.error().what() << std::endl; + const dross::path config_dir{*config_home}; + if (!config_dir.exists()) { + if (auto created = dross::path::mkdir(*config_home); !created) { + std::cerr << "mkdir failed: " << created.error().what() + << std::endl; + } } + dross::path config_file = config_dir.append("settings.toml"); + std::cout << "Config file: " << config_file.string() << std::endl; } // The data directory works the same way @@ -250,4 +275,4 @@ the standard library's, so it is inspected with ``code()`` and reported with } else { std::cerr << "Error: " << result.error().what() << std::endl; } - } \ No newline at end of file + } diff --git a/docs/sphinx/source/api/type-system.rst b/docs/sphinx/source/api/type-system.rst index 26f2097..820ca56 100644 --- a/docs/sphinx/source/api/type-system.rst +++ b/docs/sphinx/source/api/type-system.rst @@ -36,7 +36,8 @@ The ``value`` class is the central polymorphic type that can hold any supported // A bare `true` would select the arithmetic constructor and end up as a // number, so the boolean above is wrapped explicitly. - // Check the type before casting: as() is undefined on a mismatch + // Always ask is() first: as() has no defined result when the + // value is holding some other type if (v1.is()) { auto num = v1.as(); std::cout << num << std::endl; // number has operator<< @@ -127,7 +128,8 @@ The ``string`` class provides Unicode-aware string handling: // Concatenation is in place; there is no operator+ s1 += s2; - // Length is counted in Unicode code points, not bytes + // The buffer is UTF-8 and length() reports its size in bytes, so this is + // 11 only because the content is ASCII std::cout << s1.length() << std::endl; // 11 // Seamless conversion to std::string, which is what streams accept @@ -441,8 +443,8 @@ printing it. ``value`` is inspected and unwrapped with member templates: - ``is()`` - Check whether the value currently holds type ``T`` -- ``as()`` - Retrieve the value as type ``T``; undefined unless ``is()`` - is true first +- ``as()`` - Retrieve the value as type ``T``. The result is unspecified + unless ``is()`` is true, so always check first. It does not throw Example: @@ -467,8 +469,9 @@ Example: auto text_string = to_string(text); // "hello" auto timestamp_string = to_string(meeting); // "2024-01-21T15:30:00+09:00" - // 3. Direct stream output - std::cout << flag << " " << n << " " << text << " " << meeting << std::endl; + // 3. Direct stream output. string has no operator<<, so it reaches the + // stream through its std::string conversion (text_str above). + std::cout << flag << " " << n << " " << text_str << " " << meeting << std::endl; // Value type conversion dross::value v = 42; @@ -487,19 +490,29 @@ Example: Comparison Operations --------------------- -All types support three-way comparison (spaceship operator): +``boolean``, ``number``, ``data``, ``timestamp``, ``timezone`` and ``error`` +provide three-way comparison (the spaceship operator). ``value``, ``string``, +``array`` and ``dictionary`` provide only ``==`` and ``!=``. .. code-block:: cpp + dross::number n1 = 42; + dross::number n2 = 43; + + if (n1 < n2) { + std::cout << "n1 is less than n2" << std::endl; + } + + auto ordering = n1 <=> n2; // std::strong_ordering + + // value is only equality-comparable dross::value v1 = 42; dross::value v2 = 43; - if (v1 < v2) { - std::cout << "v1 is less than v2" << std::endl; + if (v1 != v2) { + std::cout << "v1 and v2 hold different values" << std::endl; } - auto result = v1 <=> v2; // std::strong_ordering - Error Handling -------------- diff --git a/docs/sphinx/source/contributing.rst b/docs/sphinx/source/contributing.rst index d2f125e..a65fc25 100644 --- a/docs/sphinx/source/contributing.rst +++ b/docs/sphinx/source/contributing.rst @@ -297,4 +297,4 @@ Thank You! ---------- Your contributions help make dross better for everyone. We appreciate your time -and effort in improving the library! \ No newline at end of file +and effort in improving the library! diff --git a/docs/sphinx/source/examples/index.rst b/docs/sphinx/source/examples/index.rst index 3cde4b2..8dd9342 100644 --- a/docs/sphinx/source/examples/index.rst +++ b/docs/sphinx/source/examples/index.rst @@ -5,7 +5,6 @@ Examples :maxdepth: 2 basic-types - json-processing configuration data-structures @@ -406,9 +405,8 @@ More Examples For more examples, visit: - :doc:`basic-types` - Working with individual type classes -- :doc:`json-processing` - JSON parsing and generation - :doc:`configuration` - Configuration file handling - :doc:`data-structures` - Building complex data structures The library's own test suite, under ``test/`` in the source repository, is a -further source of compiling, executable usage. \ No newline at end of file +further source of compiling, executable usage. diff --git a/docs/sphinx/source/examples/json-processing.rst b/docs/sphinx/source/examples/json-processing.rst deleted file mode 100644 index f2b19cf..0000000 --- a/docs/sphinx/source/examples/json-processing.rst +++ /dev/null @@ -1,11 +0,0 @@ -JSON Processing Examples -======================== - -Coming soon... - -This section will include examples for: - -- JSON parsing and generation -- Error handling with JSON -- Working with complex JSON structures -- Performance considerations \ No newline at end of file diff --git a/docs/sphinx/source/getting-started.rst b/docs/sphinx/source/getting-started.rst index 74fdc7d..316896a 100644 --- a/docs/sphinx/source/getting-started.rst +++ b/docs/sphinx/source/getting-started.rst @@ -223,4 +223,4 @@ Next Steps - Explore the :doc:`user-guide/index` for in-depth tutorials - Browse the :doc:`api/index` for detailed API documentation - Check out :doc:`examples/index` for more complex use cases -- Learn about :doc:`contributing` if you want to help improve dross \ No newline at end of file +- Learn about :doc:`contributing` if you want to help improve dross diff --git a/docs/sphinx/source/index.rst b/docs/sphinx/source/index.rst index 120a6b1..0445b1b 100644 --- a/docs/sphinx/source/index.rst +++ b/docs/sphinx/source/index.rst @@ -225,4 +225,4 @@ Indices and tables ================== * :ref:`genindex` -* :ref:`search` \ No newline at end of file +* :ref:`search` diff --git a/docs/sphinx/source/user-guide/index.rst b/docs/sphinx/source/user-guide/index.rst index 89bb389..4ed18ea 100644 --- a/docs/sphinx/source/user-guide/index.rst +++ b/docs/sphinx/source/user-guide/index.rst @@ -152,8 +152,11 @@ Platform Utilities xdg app{"myapp"}; const std::string app_data = app.data_home().value_or(config_path.string()); - // Create directory if needed - if (auto result = path::mkdir(app_data); !result) { - std::cerr << "Failed to create directory: " - << result.error().what() << std::endl; - } \ No newline at end of file + // Create the directory. mkdir() succeeds only when it actually creates + // one, so an existing directory is reported as an error; test first. + if (!path{app_data}.exists()) { + if (auto result = path::mkdir(app_data); !result) { + std::cerr << "Failed to create directory: " + << result.error().what() << std::endl; + } + } diff --git a/include/dross/platform/environment.h b/include/dross/platform/environment.h index 108d61a..43aaa78 100644 --- a/include/dross/platform/environment.h +++ b/include/dross/platform/environment.h @@ -54,7 +54,8 @@ class environment { * @return std::optional containing the value if the variable exists, std::nullopt otherwise * * Safely retrieves the value of the specified environment variable. - * Returns std::nullopt if the variable is not set or is empty. + * Returns std::nullopt only when the variable is not set. A variable set + * to an empty string yields an optional holding an empty string. * * Thread safety: * - Safe for concurrent read access from multiple threads diff --git a/include/dross/platform/path.h b/include/dross/platform/path.h index 6b17fa6..c436dd6 100644 --- a/include/dross/platform/path.h +++ b/include/dross/platform/path.h @@ -69,8 +69,11 @@ class path { * @return Expected containing the created path on success, or filesystem_error on failure * * Creates the specified directory and any necessary parent directories. - * If the directory already exists, the operation succeeds. - * + * Succeeds only when a directory is actually created: if dir_path is + * already present the call reports failure, and that error carries no + * diagnostic code. Test with exists() first when an already-provisioned + * directory should not count as an error. + * * @code * if (auto result = path::mkdir("/tmp/myapp/data")) { * std::cout << "Created: " << result->string() << std::endl; diff --git a/include/dross/type/string.h b/include/dross/type/string.h index be54e45..94b284c 100644 --- a/include/dross/type/string.h +++ b/include/dross/type/string.h @@ -104,11 +104,11 @@ class string final { bool starts_with(const std::string& prefix) const; /** - * @brief Get the Unicode character length of the string. - * @return The number of Unicode characters (not bytes) + * @brief Get the length of the string in bytes. + * @return The number of bytes in the UTF-8 encoded content * - * Returns the count of Unicode code points, which may be different - * from the byte length for strings containing non-ASCII characters. + * Counts UTF-8 code units, not code points: a string holding non-ASCII + * characters reports more than the number of characters it contains. */ size_t length() const; diff --git a/include/dross/type/value.h b/include/dross/type/value.h index 8f0dacb..2acc576 100644 --- a/include/dross/type/value.h +++ b/include/dross/type/value.h @@ -274,8 +274,8 @@ class value final { * @return The value as the specified type * * This is a convenience method that delegates to value_cast. - * If the value is not of the requested type, the behavior is undefined. - * Use is() to check the type before casting. + * Call is() first: the result is unspecified unless the value + * currently holds type T. This method does not throw. */ template T as() const noexcept; From dd12426292bbb35a78812f9578d83b2ed9869e8c Mon Sep 17 00:00:00 2001 From: Yuma Endo Date: Sun, 16 Aug 2026 09:30:28 +0900 Subject: [PATCH 3/7] docs: finish the sweep, and stop calling byte operations Unicode-aware The previous commit fixed four places and left their neighbours, which was worse than leaving all of them: before, the headers and the prose agreed with each other and were uniformly wrong; after, mkdir appeared twice on one page saying opposite things, and the corrected length() rendered directly below an example still labelled "Unicode-aware length". Three lenses and the independent reviewer each found the same three sites. So the sweep now goes by wording rather than by line number. mkdir's filesystem::path overload -- the one that actually holds the failure check, the string overload merely forwards to it -- says what the string one says. The class example no longer claims a Unicode-aware length, and value_cast no longer advertises a std::bad_cast it is declared noexcept against. The guidance to call exists() before mkdir is gone. exists() reaches for the throwing std::filesystem::exists, so it was not the safe guard it was offered as. The page states the behaviour and leaves it there. Two more throwing paths join the list of exceptions to the "reports failures through the return type" rule: exists() itself and the default path constructor, which calls std::filesystem::absolute. And expand() escapes on any canonicalisation failure, not only on a missing target. Finally, the string class stops describing itself as Unicode-aware. It holds UTF-8 bytes and works on them: length() counts bytes, prefix and equality compare bytes, and either can split or match across a multi-byte character. Nothing validates the content, so the documentation no longer promises undefined behaviour for malformed input -- the bytes are kept and handed back as they came. --- docs/sphinx/source/api/index.rst | 24 ++++++--- docs/sphinx/source/api/platform.rst | 65 +++++++++++++------------ docs/sphinx/source/api/type-system.rst | 3 +- docs/sphinx/source/changelog.rst | 2 +- docs/sphinx/source/index.rst | 2 +- docs/sphinx/source/user-guide/index.rst | 9 ++-- include/dross/platform/path.h | 19 ++++++-- include/dross/type.h | 2 +- include/dross/type/string.h | 28 ++++++----- include/dross/type/value.h | 8 +-- 10 files changed, 94 insertions(+), 68 deletions(-) diff --git a/docs/sphinx/source/api/index.rst b/docs/sphinx/source/api/index.rst index 057b372..790232e 100644 --- a/docs/sphinx/source/api/index.rst +++ b/docs/sphinx/source/api/index.rst @@ -47,14 +47,22 @@ throwing: - ``std::optional`` for operations that may not produce a value - ``std::expected`` for operations that may fail with error information -Two kinds of operation are exceptions to that rule. The bounds-checked -accessors — the const ``dictionary::operator[]``, ``array::operator[]`` and -``array::value_at()`` — throw ``std::out_of_range`` when the key or index is -not present, so ask ``dictionary::contains()`` or ``array::length()`` before -indexing. And ``path::expand()``, despite returning ``std::expected``, lets a -``std::filesystem::filesystem_error`` escape when the path begins with ``~`` -and the expanded location does not exist; ``path::resolve()`` catches that -condition and returns it. +Some operations are exceptions to that rule: + +- The bounds-checked accessors — the const ``dictionary::operator[]``, + ``array::operator[]`` and ``array::value_at()`` — throw + ``std::out_of_range`` when the key or index is not present. Ask + ``dictionary::contains()`` or ``array::length()`` before indexing. +- ``path::expand()``, despite returning ``std::expected``, lets a + ``std::filesystem::filesystem_error`` escape for any canonicalisation + failure on a ``~`` path. ``path::resolve()`` catches those and returns + them. +- ``path``'s ``exists()`` calls the throwing form of + ``std::filesystem::exists``, so an error while querying the path — as + opposed to the path simply being absent — escapes as a + ``std::filesystem::filesystem_error``. +- The default ``path`` constructor resolves ``"."`` with the throwing form of + ``std::filesystem::absolute``. Example: diff --git a/docs/sphinx/source/api/platform.rst b/docs/sphinx/source/api/platform.rst index f53652a..51730d3 100644 --- a/docs/sphinx/source/api/platform.rst +++ b/docs/sphinx/source/api/platform.rst @@ -22,8 +22,9 @@ through a single static accessor, ``value()``: #include - // Read an environment variable. The result is std::nullopt when the - // variable is unset or empty, so handle that case explicitly. + // Read an environment variable. The result is std::nullopt only when + // the variable is unset; a variable set to "" yields an optional + // holding an empty string. Handle the missing case explicitly. if (auto home = dross::environment::value("HOME")) { std::cout << "Home directory: " << *home << std::endl; } else { @@ -58,15 +59,13 @@ The ``path`` class provides filesystem path operations: std::cout << "Config path: " << config_path.string() << std::endl; // Create the directory, including any missing parents. mkdir() - // reports failure when the directory is already there, so test for - // it first if re-running must succeed. - if (!config_path.exists()) { - if (auto created = dross::path::mkdir(config_path.string())) { - std::cout << "Created: " << created->string() << std::endl; - } else { - std::cerr << "mkdir failed: " << created.error().what() - << std::endl; - } + // reports failure when the directory is already there, and the + // error carries no code, so this branch cannot tell that case apart + // from a real failure. + if (auto created = dross::path::mkdir(config_path.string())) { + std::cout << "Created: " << created->string() << std::endl; + } else { + std::cerr << "mkdir: " << created.error().what() << std::endl; } } @@ -86,9 +85,10 @@ The ``path`` class provides filesystem path operations: std::cout << "The relative path exists" << std::endl; } - // Expand a leading ~ to the home directory. The expanded location has to - // exist: on a missing target expand() lets a filesystem_error escape - // instead of returning one, so reach for resolve() when unsure. + // Expand a leading ~ to the home directory. For a ~ path, expand() + // canonicalises without catching, so any canonicalisation failure -- + // a missing target, a permission problem, a symlink loop -- escapes as + // a filesystem_error instead of being returned. resolve() catches it. dross::path user_config{std::string{"~/.config/app"}}; if (auto expanded = user_config.expand()) { std::cout << "Expanded: " << expanded->string() << std::endl; @@ -123,14 +123,21 @@ Two caveats apply to the current implementation: - ``mkdir()`` succeeds only when it actually creates the directory. If the path already exists it returns an error, and that error carries no - diagnostic code. Guard the call with ``exists()`` when an - already-provisioned directory should not be treated as a failure. -- ``expand()`` does not route every failure through its return type. When the - path begins with ``~`` and the expanded location does not exist, a - ``std::filesystem::filesystem_error`` escapes the call instead of being - returned, which terminates a program that is not catching it. ``resolve()`` - catches the same condition and returns it as an error, so prefer - ``resolve()`` when the target may be absent. + diagnostic code, so a caller cannot tell that case apart from a real + failure. ``exists()`` is not a way around this — see below. +- ``expand()`` does not route every failure through its return type. For a + path beginning with ``~`` it canonicalises without catching, so *any* + canonicalisation failure — a missing target, a permission problem, a + symlink loop, an invalid component — escapes as a + ``std::filesystem::filesystem_error`` instead of being returned, which + terminates a program that is not catching it. A path that does not begin + with ``~`` is returned unchanged and never throws. ``resolve()`` catches + the same failures and returns them, so prefer it when the target may not + be reachable. +- ``exists()`` calls the throwing form of ``std::filesystem::exists``. An + absent path is simply ``false``, but an error while querying it — an + over-long name, or a directory the process may not traverse — escapes as a + ``std::filesystem::filesystem_error``. xdg --- @@ -191,7 +198,7 @@ the application name passed to the constructor: Every accessor returns ``std::optional`` and yields ``std::nullopt`` when the home directory cannot be determined. The directory itself is not created for you — pass the result to ``path::mkdir()``, keeping -in mind that ``mkdir()`` reports an already-existing directory as an error. +in mind that ``mkdir()`` reports an already-present directory as an error. Example Usage ~~~~~~~~~~~~~ @@ -207,15 +214,13 @@ Creating application directories: dross::xdg app{"myapp"}; - // Create the config directory, then name a file inside it. mkdir() only - // succeeds when it creates the directory, so skip it if it is there. + // Create the config directory, then name a file inside it. mkdir() + // reports an already-present directory as a failure, with an error that + // carries no code, so this branch cannot tell the two apart. if (auto config_home = app.config_home()) { const dross::path config_dir{*config_home}; - if (!config_dir.exists()) { - if (auto created = dross::path::mkdir(*config_home); !created) { - std::cerr << "mkdir failed: " << created.error().what() - << std::endl; - } + if (auto created = dross::path::mkdir(*config_home); !created) { + std::cerr << "mkdir: " << created.error().what() << std::endl; } dross::path config_file = config_dir.append("settings.toml"); std::cout << "Config file: " << config_file.string() << std::endl; diff --git a/docs/sphinx/source/api/type-system.rst b/docs/sphinx/source/api/type-system.rst index 820ca56..31429c5 100644 --- a/docs/sphinx/source/api/type-system.rst +++ b/docs/sphinx/source/api/type-system.rst @@ -113,7 +113,8 @@ string :protected-members: :undoc-members: -The ``string`` class provides Unicode-aware string handling: +The ``string`` class holds text as UTF-8 bytes; its length and comparison +operations work on those bytes: .. code-block:: cpp diff --git a/docs/sphinx/source/changelog.rst b/docs/sphinx/source/changelog.rst index 86aac53..7daec3a 100644 --- a/docs/sphinx/source/changelog.rst +++ b/docs/sphinx/source/changelog.rst @@ -42,7 +42,7 @@ Type System - ``value`` class for polymorphic type holding - ``boolean`` class for type-safe boolean operations - ``number`` class with arbitrary precision arithmetic -- ``string`` class with Unicode awareness +- ``string`` class holding UTF-8 text - ``array`` class with range-based for loop support - ``dictionary`` class for key-value storage - ``data`` class for raw byte storage diff --git a/docs/sphinx/source/index.rst b/docs/sphinx/source/index.rst index 0445b1b..e143780 100644 --- a/docs/sphinx/source/index.rst +++ b/docs/sphinx/source/index.rst @@ -180,7 +180,7 @@ Features -------- - **Dynamic Type System**: Polymorphic value type using ``std::variant`` -- **Unicode Support**: Built-in Unicode-aware string handling +- **UTF-8 Strings**: Text held as UTF-8 bytes, with byte-oriented operations - **Arbitrary Precision**: Number type with string-based storage - **Error Handling**: Consistent use of ``std::optional`` and ``std::expected`` - **Modern C++**: Concepts, ranges, three-way comparison, and more diff --git a/docs/sphinx/source/user-guide/index.rst b/docs/sphinx/source/user-guide/index.rst index 4ed18ea..e959926 100644 --- a/docs/sphinx/source/user-guide/index.rst +++ b/docs/sphinx/source/user-guide/index.rst @@ -153,10 +153,7 @@ Platform Utilities const std::string app_data = app.data_home().value_or(config_path.string()); // Create the directory. mkdir() succeeds only when it actually creates - // one, so an existing directory is reported as an error; test first. - if (!path{app_data}.exists()) { - if (auto result = path::mkdir(app_data); !result) { - std::cerr << "Failed to create directory: " - << result.error().what() << std::endl; - } + // one, so an already-present directory is reported as an error too. + if (auto result = path::mkdir(app_data); !result) { + std::cerr << "mkdir: " << result.error().what() << std::endl; } diff --git a/include/dross/platform/path.h b/include/dross/platform/path.h index c436dd6..fb6b530 100644 --- a/include/dross/platform/path.h +++ b/include/dross/platform/path.h @@ -71,8 +71,7 @@ class path { * Creates the specified directory and any necessary parent directories. * Succeeds only when a directory is actually created: if dir_path is * already present the call reports failure, and that error carries no - * diagnostic code. Test with exists() first when an already-provisioned - * directory should not count as an error. + * diagnostic code. * * @code * if (auto result = path::mkdir("/tmp/myapp/data")) { @@ -90,7 +89,10 @@ class path { * @return Expected containing the created path on success, or filesystem_error on failure * * Creates the specified directory and any necessary parent directories. - * If the directory already exists, the operation succeeds. + * Succeeds only when a directory is actually created: if dir_path is + * already present the call reports failure, and that error carries no + * diagnostic code. This overload holds the logic; the std::string one + * forwards to it. */ static std::expected mkdir(const std::filesystem::path& dir_path); @@ -122,7 +124,11 @@ class path { static std::string separator(); /** - * @brief Default constructor creating an empty path. + * @brief Default constructor holding the current working directory. + * + * Resolves "." to an absolute path, so the result is the working + * directory at the time of construction, not an empty path. Uses the + * throwing form of std::filesystem::absolute. */ path(); @@ -155,6 +161,11 @@ class path { * * Checks whether the path refers to an existing filesystem entity. * This includes files, directories, symbolic links, and other filesystem objects. + * + * Uses the throwing form of std::filesystem::exists. A path that is + * merely absent yields false, but an error while querying it — an + * over-long name, or a directory the process may not traverse — escapes + * as a std::filesystem::filesystem_error. */ bool exists() const; diff --git a/include/dross/type.h b/include/dross/type.h index 47ae1af..a9d88f7 100644 --- a/include/dross/type.h +++ b/include/dross/type.h @@ -12,7 +12,7 @@ * * Key features: * - Arbitrary precision arithmetic with number - * - Unicode-aware string handling + * - UTF-8 string handling with byte-oriented operations * - Type-safe boolean operations * - Dynamic arrays and key-value dictionaries * - Date and time handling with timezone support diff --git a/include/dross/type/string.h b/include/dross/type/string.h index 94b284c..c4a3a81 100644 --- a/include/dross/type/string.h +++ b/include/dross/type/string.h @@ -14,14 +14,17 @@ template concept string_type = std::same_as || std::same_as; /** - * @brief Unicode-aware string class with value semantics. + * @brief UTF-8 string class with value semantics. * - * The string class provides Unicode-aware string handling with a focus on - * correctness and safety. It uses internal UTF-8 encoding and provides - * methods for common string operations while maintaining encoding integrity. + * The string class holds text as UTF-8 bytes and hands those bytes back + * unchanged. Its operations work on the bytes rather than on characters: + * length() counts bytes, and the prefix and equality comparisons compare + * bytes, so they can split or match across a multi-byte character. Nothing + * validates the content, so the class neither repairs nor rejects malformed + * UTF-8. * * Key features: - * - Unicode-aware string operations + * - Byte-oriented operations over UTF-8 content * - UTF-8 internal encoding * - Value semantics (copyable and assignable) * - Safe string manipulation methods @@ -49,7 +52,7 @@ concept string_type = std::same_as || std::same_as() has no defined result otherwise. * Supports checking for boolean, number, string, array, dictionary, timestamp, and data types. */ template @@ -293,9 +293,9 @@ class value final { * @tparam T The target type to cast to * @param val The value to cast from * @return The contained object of type T - * @throws std::bad_cast if the value doesn't contain type T * - * Use value.is() to check the type before casting to avoid exceptions. + * Call value.is() first: the result is unspecified unless the value + * currently holds type T. This function does not throw. * Supported types: boolean, number, string, array, dictionary, timestamp, data. * * @code From 383c6f9896253cc480cb4cc6c747fb3ccac96204 Mon Sep 17 00:00:00 2001 From: Yuma Endo Date: Sun, 16 Aug 2026 10:44:27 +0900 Subject: [PATCH 4/7] docs: take the sweep out past the pages it started on Three of the corrections in the last commit were themselves wrong, and each was introduced by the correction rather than inherited from the base. as() was written up as leaving an unspecified result on a mismatch. The reasoning was that the header called the case undefined, so nothing should be built on what the implementation happens to return -- except that test/type/value_cast_wrong_type pins the return to a default-constructed T with EXPECT_EQ. Change the implementation and the suite fails, which makes it a contract whatever the comment said. The documentation now says a default-constructed T comes back, and still says to call is() first. The string example claimed a length of 14 bytes on a line that runs after an append, and reached that append through an operator+ the class does not have. And mkdir's failure on an existing directory was described as indistinguishable from a real one, which overstates it: create_directories fills in the error code, so a caller reading error().code() can usually tell the two apart. Withdrawing the exists() guard stands -- exists() throws and races -- but the reason given for it was wrong. The rest is the same class of staleness, found further out each time the net widened. README still called strings Unicode-aware. platform.h, dross.h, index.rst and the user guide still promised no exceptions at all. number.h advertised a std::runtime_error on three conversions that return 0, 0.0 and 0LL instead, and described rounding as truncation. The changelog credited every type with three-way comparison and with value semantics, when six types have the former and environment, being a static utility, has neither. A caveat list said two and held three. Which is the end of it for this branch. What the sweep did not reach is written down rather than left to be discovered again. --- README.md | 2 +- docs/sphinx/source/api/platform.rst | 30 +++++++++++++------------ docs/sphinx/source/api/type-system.rst | 14 +++++++----- docs/sphinx/source/changelog.rst | 10 +++++---- docs/sphinx/source/index.rst | 3 ++- docs/sphinx/source/user-guide/index.rst | 8 ++++--- include/dross/dross.h | 2 +- include/dross/platform.h | 4 +++- include/dross/platform/path.h | 15 ++++++++----- include/dross/type.h | 12 ++++++---- include/dross/type/number.h | 16 ++++++++----- include/dross/type/string.h | 15 ++++++++----- include/dross/type/value.h | 13 ++++++----- 13 files changed, 86 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index ff5b365..ab7a67a 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ auto app_config = config_dir / "myapp" / "config.toml"; ### Type System - **`boolean`** - Type-safe boolean operations with logical operators - **`number`** - Arbitrary precision arithmetic with string-based storage -- **`string`** - Unicode-aware string handling +- **`string`** - UTF-8 text held as bytes, with byte-oriented operations - **`timestamp`** - Date and time handling with timezone support - **`timezone`** - Type-safe timezone representation with ISO 8601 support - **`array`** - Dynamic arrays with value semantics diff --git a/docs/sphinx/source/api/platform.rst b/docs/sphinx/source/api/platform.rst index 51730d3..67bf0bf 100644 --- a/docs/sphinx/source/api/platform.rst +++ b/docs/sphinx/source/api/platform.rst @@ -59,12 +59,11 @@ The ``path`` class provides filesystem path operations: std::cout << "Config path: " << config_path.string() << std::endl; // Create the directory, including any missing parents. mkdir() - // reports failure when the directory is already there, and the - // error carries no code, so this branch cannot tell that case apart - // from a real failure. + // reports failure when the directory is already there, so tell that + // case apart by its code(): zero means it was already present. if (auto created = dross::path::mkdir(config_path.string())) { std::cout << "Created: " << created->string() << std::endl; - } else { + } else if (created.error().code()) { std::cerr << "mkdir: " << created.error().what() << std::endl; } } @@ -116,15 +115,16 @@ Operations that consult the filesystem: ``expand()``, ``resolve()`` and ``mkdir()`` are declared to return ``std::expected``; ``home()`` returns -``std::optional``. Reading and writing file *contents* is deliberately -not part of ``path`` — use the standard library's ```` for that. +``std::optional``. Reading and writing file *contents* is not part of +``path`` — use the standard library's ```` for that. -Two caveats apply to the current implementation: +Some caveats apply to the current implementation: - ``mkdir()`` succeeds only when it actually creates the directory. If the - path already exists it returns an error, and that error carries no - diagnostic code, so a caller cannot tell that case apart from a real - failure. ``exists()`` is not a way around this — see below. + path already exists it returns an error, but one whose ``code()`` is zero, + so a caller can tell it apart from a real filesystem failure, which + carries a nonzero code. Testing with ``exists()`` beforehand is not a + better answer — see below, and it races with other processes anyway. - ``expand()`` does not route every failure through its return type. For a path beginning with ``~`` it canonicalises without catching, so *any* canonicalisation failure — a missing target, a permission problem, a @@ -198,7 +198,8 @@ the application name passed to the constructor: Every accessor returns ``std::optional`` and yields ``std::nullopt`` when the home directory cannot be determined. The directory itself is not created for you — pass the result to ``path::mkdir()``, keeping -in mind that ``mkdir()`` reports an already-present directory as an error. +in mind that ``mkdir()`` reports an already-present directory as an error, +recognisable by its zero ``code()``. Example Usage ~~~~~~~~~~~~~ @@ -215,11 +216,12 @@ Creating application directories: dross::xdg app{"myapp"}; // Create the config directory, then name a file inside it. mkdir() - // reports an already-present directory as a failure, with an error that - // carries no code, so this branch cannot tell the two apart. + // reports an already-present directory as a failure too, but with a + // zero code(), so only a nonzero one is a real problem. if (auto config_home = app.config_home()) { const dross::path config_dir{*config_home}; - if (auto created = dross::path::mkdir(*config_home); !created) { + if (auto created = dross::path::mkdir(*config_home); + !created && created.error().code()) { std::cerr << "mkdir: " << created.error().what() << std::endl; } dross::path config_file = config_dir.append("settings.toml"); diff --git a/docs/sphinx/source/api/type-system.rst b/docs/sphinx/source/api/type-system.rst index 31429c5..d494abe 100644 --- a/docs/sphinx/source/api/type-system.rst +++ b/docs/sphinx/source/api/type-system.rst @@ -1,8 +1,9 @@ Type System =========== -The dross type system provides dynamic typing with strong value semantics. All types -use the Pimpl idiom for ABI stability and provide consistent interfaces. +The dross type system provides dynamic typing with strong value semantics. The +core types use the Pimpl idiom for ABI stability and provide consistent +interfaces; ``error`` wraps a ``std::error_code`` directly. Core Types ---------- @@ -36,8 +37,8 @@ The ``value`` class is the central polymorphic type that can hold any supported // A bare `true` would select the arithmetic constructor and end up as a // number, so the boolean above is wrapped explicitly. - // Always ask is() first: as() has no defined result when the - // value is holding some other type + // Ask is() first: when the value holds another type, as() hands + // back a default-constructed T instead of the contained object if (v1.is()) { auto num = v1.as(); std::cout << num << std::endl; // number has operator<< @@ -444,8 +445,9 @@ printing it. ``value`` is inspected and unwrapped with member templates: - ``is()`` - Check whether the value currently holds type ``T`` -- ``as()`` - Retrieve the value as type ``T``. The result is unspecified - unless ``is()`` is true, so always check first. It does not throw +- ``as()`` - Retrieve the value as type ``T``. When the value holds + another type it returns a default-constructed ``T`` rather than the + contained object, so check ``is()`` first. It does not throw Example: diff --git a/docs/sphinx/source/changelog.rst b/docs/sphinx/source/changelog.rst index 7daec3a..761e95f 100644 --- a/docs/sphinx/source/changelog.rst +++ b/docs/sphinx/source/changelog.rst @@ -58,11 +58,13 @@ Platform Utilities Core Features ^^^^^^^^^^^^^ -- Pimpl idiom for ABI stability across all types +- Pimpl idiom for ABI stability across the core types - ``std::expected`` and ``std::optional`` for error handling - C++23 concepts for type constraints -- Three-way comparison operators for all types -- Value semantics (copyable and assignable) for all types +- Three-way comparison operators for ``boolean``, ``number``, ``data``, + ``timestamp``, ``timezone`` and ``error`` +- Value semantics (copyable and assignable) for every class except + ``environment``, which is a static utility with no instances - Zero external dependencies (standard library only) Build System @@ -76,7 +78,7 @@ Build System Testing ^^^^^^^ -- Comprehensive unit tests for all public APIs +- Unit tests for the type system, ``environment`` and the TOML format layer - Error path testing alongside success paths - Boundary condition and edge case testing - >90% code coverage for public interfaces diff --git a/docs/sphinx/source/index.rst b/docs/sphinx/source/index.rst index e143780..a99ace9 100644 --- a/docs/sphinx/source/index.rst +++ b/docs/sphinx/source/index.rst @@ -39,7 +39,8 @@ designed to be a general-purpose library similar to Boost with a focus on: - **Zero external dependencies** - Only requires the standard library - **Modern C++ design** - Leveraging C++23 features throughout -- **Error handling without exceptions** - Using ``std::optional`` and ``std::expected`` +- **Errors in the return type** - ``std::optional`` and ``std::expected`` + rather than exceptions, apart from the bounds-checked accessors - **ABI stability** - Through careful use of the Pimpl idiom - **Comprehensive type system** - Dynamic types with value semantics diff --git a/docs/sphinx/source/user-guide/index.rst b/docs/sphinx/source/user-guide/index.rst index e959926..414238a 100644 --- a/docs/sphinx/source/user-guide/index.rst +++ b/docs/sphinx/source/user-guide/index.rst @@ -19,7 +19,8 @@ The dross library is designed around several core principles: 1. **Zero Dependencies**: Only requires the standard C++ library 2. **Value Semantics**: All types are copyable and follow value semantics -3. **Error Handling**: No exceptions - uses ``std::optional`` and ``std::expected`` +3. **Error Handling**: Errors are returned, not thrown - ``std::optional`` + and ``std::expected`` - apart from the bounds-checked accessors 4. **Modern C++**: Leverages C++23 features throughout 5. **ABI Stability**: Uses Pimpl idiom to maintain stable ABI @@ -153,7 +154,8 @@ Platform Utilities const std::string app_data = app.data_home().value_or(config_path.string()); // Create the directory. mkdir() succeeds only when it actually creates - // one, so an already-present directory is reported as an error too. - if (auto result = path::mkdir(app_data); !result) { + // one, so an already-present directory comes back as an error too -- + // but with a zero code(), unlike a real failure. + if (auto result = path::mkdir(app_data); !result && result.error().code()) { std::cerr << "mkdir: " << result.error().what() << std::endl; } diff --git a/include/dross/dross.h b/include/dross/dross.h index 1c93e7a..61464c5 100644 --- a/include/dross/dross.h +++ b/include/dross/dross.h @@ -46,7 +46,7 @@ * - Type system: Polymorphic value types with arbitrary precision * - Platform layer: Cross-platform environment and filesystem utilities * - Format layer: Type-safe TOML parsing returning dictionary directly - * - Modern C++23: Concepts, ranges, and error handling without exceptions + * - Modern C++23: Concepts, ranges, and errors reported in the return type * * @namespace dross */ diff --git a/include/dross/platform.h b/include/dross/platform.h index 5171d11..2758168 100644 --- a/include/dross/platform.h +++ b/include/dross/platform.h @@ -39,7 +39,9 @@ * Error handling: * - Uses std::optional for operations that may not return a value * - Uses std::expected for operations that may fail with detailed error info - * - No exceptions thrown from platform layer + * - The platform layer throws nothing of its own, but std::filesystem + * exceptions do propagate: see path::exists(), path::expand() and the + * default path constructor * * Platform support: * - Unix-like systems (Linux, macOS, BSD) diff --git a/include/dross/platform/path.h b/include/dross/platform/path.h index fb6b530..94deb4a 100644 --- a/include/dross/platform/path.h +++ b/include/dross/platform/path.h @@ -25,7 +25,9 @@ namespace dross { * Error handling: * - Uses std::expected for fallible operations * - Uses std::optional for operations that may not return a value - * - No exceptions thrown directly (may propagate from std::filesystem) + * - No exceptions thrown directly, but std::filesystem ones propagate: + * exists(), expand() on a ~ path, and the default constructor all call + * throwing std::filesystem functions * * Performance characteristics: * - Thin wrapper over std::filesystem with minimal overhead @@ -70,8 +72,9 @@ class path { * * Creates the specified directory and any necessary parent directories. * Succeeds only when a directory is actually created: if dir_path is - * already present the call reports failure, and that error carries no - * diagnostic code. + * already present the call reports failure. That case is still + * recognisable — the reported error's code() is zero, whereas a real + * filesystem failure carries a nonzero code. * * @code * if (auto result = path::mkdir("/tmp/myapp/data")) { @@ -90,9 +93,9 @@ class path { * * Creates the specified directory and any necessary parent directories. * Succeeds only when a directory is actually created: if dir_path is - * already present the call reports failure, and that error carries no - * diagnostic code. This overload holds the logic; the std::string one - * forwards to it. + * already present the call reports failure, with an error whose code() + * is zero; a real filesystem failure carries a nonzero code. This + * overload holds the logic; the std::string one forwards to it. */ static std::expected mkdir(const std::filesystem::path& dir_path); diff --git a/include/dross/type.h b/include/dross/type.h index a9d88f7..ddec440 100644 --- a/include/dross/type.h +++ b/include/dross/type.h @@ -6,9 +6,12 @@ * the core polymorphic types (boolean, number, string, array, dictionary, timestamp, timezone, value) * and utility functions for string manipulation and container operations. * - * The type system is designed around value semantics with no exceptions, - * using std::optional and std::expected for error handling. All types - * use the Pimpl idiom for ABI stability. + * The type system is designed around value semantics, using std::optional + * and std::expected for error handling rather than exceptions. The + * bounds-checked accessors are the exception: array and dictionary throw + * std::out_of_range for an index or key that is not present. The core + * types use the Pimpl idiom for ABI stability; error wraps a + * std::error_code directly. * * Key features: * - Arbitrary precision arithmetic with number @@ -17,7 +20,8 @@ * - Dynamic arrays and key-value dictionaries * - Date and time handling with timezone support * - Polymorphic value type using std::variant - * - Seamless string conversion for all types + * - Seamless string conversion for boolean, number, string, data, + * timestamp and timezone (not array, dictionary or value) * - Utility functions for common operations * * @code diff --git a/include/dross/type/number.h b/include/dross/type/number.h index 827588d..6f7ec78 100644 --- a/include/dross/type/number.h +++ b/include/dross/type/number.h @@ -250,25 +250,29 @@ class number { /** * @brief Convert to int. - * @return Integer representation, truncated if necessary - * @throws std::runtime_error if the number is NaN or out of range + * @return Rounded integer, clamped to the range of int + * + * Does not throw. A NaN yields 0, a value outside the range of int is + * clamped to the nearest bound, and a fractional value is rounded to + * nearest rather than truncated. */ explicit operator int() const; /** * @brief Convert to double. * @return Double-precision floating-point representation - * @throws std::runtime_error if the number is NaN * - * May lose precision for very large numbers or numbers with + * Does not throw. A NaN, or a value that cannot be parsed as a double, + * yields 0.0. May lose precision for very large numbers or numbers with * many decimal places. */ explicit operator double() const; /** * @brief Convert to long long. - * @return Long long integer representation, truncated if necessary - * @throws std::runtime_error if the number is NaN or out of range + * @return Long long built from the integer part, discarding any fraction + * + * Does not throw. A NaN, or a value that overflows long long, yields 0. */ explicit operator long long() const; diff --git a/include/dross/type/string.h b/include/dross/type/string.h index c4a3a81..425f07d 100644 --- a/include/dross/type/string.h +++ b/include/dross/type/string.h @@ -45,14 +45,17 @@ concept string_type = std::same_as || std::same_as() has no defined result otherwise. + * Use this before casting: as() hands back a default-constructed T + * when the value holds something else. * Supports checking for boolean, number, string, array, dictionary, timestamp, and data types. */ template @@ -274,8 +275,9 @@ class value final { * @return The value as the specified type * * This is a convenience method that delegates to value_cast. - * Call is() first: the result is unspecified unless the value - * currently holds type T. This method does not throw. + * When the value holds another type this returns a default-constructed + * T rather than the contained object, so call is() first. It does + * not throw. */ template T as() const noexcept; @@ -294,8 +296,9 @@ class value final { * @param val The value to cast from * @return The contained object of type T * - * Call value.is() first: the result is unspecified unless the value - * currently holds type T. This function does not throw. + * When the value holds another type this returns a default-constructed T + * rather than the contained object, so call value.is() first. It does + * not throw. * Supported types: boolean, number, string, array, dictionary, timestamp, data. * * @code From fa12de2b6b02bee0955efff90181b4859ac5e459 Mon Sep 17 00:00:00 2001 From: Yuma Endo Date: Sun, 16 Aug 2026 11:03:13 +0900 Subject: [PATCH 5/7] docs: keep the copyable claim inside what was measured The previous commit replaced "for all types" with "for every class except environment", which reads wider than the twelve types the claim was checked against. Four nested iterator classes hold a unique_ptr and never declare copy assignment, so the implicit one is deleted and "every class" is false of them. The earlier wording was not, since the changelog lists types rather than classes. Both places that carry the claim now stay inside the list they follow, and they agree with each other again -- the user guide had kept saying all types were copyable while the changelog no longer did. --- docs/sphinx/source/changelog.rst | 4 ++-- docs/sphinx/source/user-guide/index.rst | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/sphinx/source/changelog.rst b/docs/sphinx/source/changelog.rst index 761e95f..43e9f10 100644 --- a/docs/sphinx/source/changelog.rst +++ b/docs/sphinx/source/changelog.rst @@ -63,8 +63,8 @@ Core Features - C++23 concepts for type constraints - Three-way comparison operators for ``boolean``, ``number``, ``data``, ``timestamp``, ``timezone`` and ``error`` -- Value semantics (copyable and assignable) for every class except - ``environment``, which is a static utility with no instances +- Value semantics (copyable and assignable) for the classes listed above, + apart from ``environment``, which exposes only static members - Zero external dependencies (standard library only) Build System diff --git a/docs/sphinx/source/user-guide/index.rst b/docs/sphinx/source/user-guide/index.rst index 414238a..4d4d68a 100644 --- a/docs/sphinx/source/user-guide/index.rst +++ b/docs/sphinx/source/user-guide/index.rst @@ -18,7 +18,8 @@ Overview The dross library is designed around several core principles: 1. **Zero Dependencies**: Only requires the standard C++ library -2. **Value Semantics**: All types are copyable and follow value semantics +2. **Value Semantics**: The value types are copyable and assignable; + ``environment`` is the exception, exposing only static members 3. **Error Handling**: Errors are returned, not thrown - ``std::optional`` and ``std::expected`` - apart from the bounds-checked accessors 4. **Modern C++**: Leverages C++23 features throughout From c06aa2c9374e8bf5ab63d5ee55ea07381956e61e Mon Sep 17 00:00:00 2001 From: Yuma Endo Date: Sun, 16 Aug 2026 11:26:08 +0900 Subject: [PATCH 6/7] docs: bring the last two copies of the claim in line Two more places said all types carry value semantics. The API index is plainly wrong there -- its own toctree renders dross::environment, whose special members are all deleted -- and it sits in the same file that carefully lists four exceptions to the no-exceptions rule twenty lines above. The README is merely ambiguous: read against its Type System list the sentence holds, read against the library it does not. Both now name the value types and put environment outside, as the changelog and the user guide already do. --- README.md | 2 +- docs/sphinx/source/api/index.rst | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ab7a67a..be50047 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,7 @@ Complete documentation including: Dross follows modern C++ best practices: - **Pimpl Idiom** - ABI stability through opaque pointers -- **Value Semantics** - All types are copyable and assignable +- **Value Semantics** - The value types are copyable and assignable; `environment` exposes only static members - **Error Handling** - `std::expected` and `std::optional` instead of exceptions - **Type Safety** - Concepts for compile-time constraints - **Zero-Cost Abstractions** - Performance without compromise diff --git a/docs/sphinx/source/api/index.rst b/docs/sphinx/source/api/index.rst index 790232e..7da6b11 100644 --- a/docs/sphinx/source/api/index.rst +++ b/docs/sphinx/source/api/index.rst @@ -85,7 +85,8 @@ Example: Memory Management ----------------- -All types in dross provide value semantics: +The value types provide value semantics, apart from ``environment``, which +exposes only static members: - Types are copyable and movable - No manual memory management required From ca63130e748e9029ec338adc1f5ab4bb6589ed21 Mon Sep 17 00:00:00 2001 From: Yuma Endo Date: Sun, 16 Aug 2026 12:01:36 +0900 Subject: [PATCH 7/7] docs: make the front-page example one a reader can run The quick example in the README used three things the library does not have: a braced initialiser list for dictionary, a static xdg::config_home, and operator/ on path. It was never part of the block sweep -- that covered the pages under docs/sphinx/source -- so nothing had ever compiled it. It now builds and runs, and says in passing why the entries are assigned after construction and why xdg needs an instance. Three more places promised errors in the return type without mentioning that path reaches for throwing filesystem calls in three places, on top of the bounds-checked accessors they already named. The contributing guide keeps its "no exceptions" line: it sits among instructions to contributors, next to "use Pimpl idiom" and "apply const, constexpr and noexcept", and is copied from the style guide. It says how to write new code rather than what the library does today. That the library does not follow it is a separate matter, and is recorded as one. --- README.md | 29 +++++++++++++++---------- docs/sphinx/source/index.rst | 3 ++- docs/sphinx/source/user-guide/index.rst | 3 ++- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index be50047..048d7eb 100644 --- a/README.md +++ b/README.md @@ -155,17 +155,22 @@ using namespace dross; number big_num{"99999999999999999999999999999999999999"}; number result = big_num * big_num; // No overflow! -// Dynamic typing -value data = dictionary{ - {"name", string{"Dross"}}, - {"version", number{"0.0.1"}}, - {"features", array{string{"fast"}, string{"safe"}}}, - {"release_date", timestamp{2024, 1, 21, 15, 30, 0, timezone::utc()}} -}; - -// Platform utilities -auto config_dir = xdg::config_home(); -auto app_config = config_dir / "myapp" / "config.toml"; +// Dynamic typing. dictionary has no initializer-list constructor, so +// entries are assigned after construction. +dictionary config; +config["name"] = string("Dross"); +config["version"] = number("0.0.1"); +config["features"] = array{string{"fast"}, string{"safe"}}; +config["release_date"] = timestamp{2024, 1, 21, 15, 30, 0, timezone::utc()}; + +value data = config; + +// Platform utilities. The XDG accessors are instance methods, and the +// application name is already part of what they return. +xdg app{"myapp"}; +if (auto config_dir = app.config_home()) { + path app_config = path{*config_dir}.append("config.toml"); +} ``` ## 📚 Core Modules @@ -202,7 +207,7 @@ Dross follows modern C++ best practices: - **Pimpl Idiom** - ABI stability through opaque pointers - **Value Semantics** - The value types are copyable and assignable; `environment` exposes only static members -- **Error Handling** - `std::expected` and `std::optional` instead of exceptions +- **Error Handling** - `std::expected` and `std::optional` for failures, apart from the bounds-checked accessors and the `path` calls that let `std::filesystem` exceptions through - **Type Safety** - Concepts for compile-time constraints - **Zero-Cost Abstractions** - Performance without compromise diff --git a/docs/sphinx/source/index.rst b/docs/sphinx/source/index.rst index a99ace9..7a1cb94 100644 --- a/docs/sphinx/source/index.rst +++ b/docs/sphinx/source/index.rst @@ -40,7 +40,8 @@ designed to be a general-purpose library similar to Boost with a focus on: - **Zero external dependencies** - Only requires the standard library - **Modern C++ design** - Leveraging C++23 features throughout - **Errors in the return type** - ``std::optional`` and ``std::expected`` - rather than exceptions, apart from the bounds-checked accessors + rather than exceptions, apart from the bounds-checked accessors and the + ``path`` calls that let ``std::filesystem`` exceptions through - **ABI stability** - Through careful use of the Pimpl idiom - **Comprehensive type system** - Dynamic types with value semantics diff --git a/docs/sphinx/source/user-guide/index.rst b/docs/sphinx/source/user-guide/index.rst index 4d4d68a..dfcdbe9 100644 --- a/docs/sphinx/source/user-guide/index.rst +++ b/docs/sphinx/source/user-guide/index.rst @@ -21,7 +21,8 @@ The dross library is designed around several core principles: 2. **Value Semantics**: The value types are copyable and assignable; ``environment`` is the exception, exposing only static members 3. **Error Handling**: Errors are returned, not thrown - ``std::optional`` - and ``std::expected`` - apart from the bounds-checked accessors + and ``std::expected`` - apart from the bounds-checked accessors and the + ``path`` calls that let ``std::filesystem`` exceptions through 4. **Modern C++**: Leverages C++23 features throughout 5. **ABI Stability**: Uses Pimpl idiom to maintain stable ABI