diff --git a/README.md b/README.md index ff5b365..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 @@ -173,7 +178,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 @@ -201,8 +206,8 @@ Complete documentation including: Dross follows modern C++ best practices: - **Pimpl Idiom** - ABI stability through opaque pointers -- **Value Semantics** - All types are copyable and assignable -- **Error Handling** - `std::expected` and `std::optional` instead of exceptions +- **Value Semantics** - The value types are copyable and assignable; `environment` exposes only static members +- **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/api/index.rst b/docs/sphinx/source/api/index.rst index 53a04ec..7da6b11 100644 --- a/docs/sphinx/source/api/index.rst +++ b/docs/sphinx/source/api/index.rst @@ -41,25 +41,43 @@ 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 +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: .. 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()); } @@ -67,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 @@ -81,4 +100,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 daf399d..67bf0bf 100644 --- a/docs/sphinx/source/api/platform.rst +++ b/docs/sphinx/source/api/platform.rst @@ -12,28 +12,28 @@ 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 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 { + 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 +48,96 @@ 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. mkdir() + // 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 if (created.error().code()) { + std::cerr << "mkdir: " << 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; - } - - // 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; + std::cerr << "Resolve failed: " << resolved.error().what() << std::endl; } -Path Operations -~~~~~~~~~~~~~~~ - -Common path operations include: + // Check whether a path exists + if (relative.exists()) { + std::cout << "The relative path exists" << std::endl; + } -- **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 + // 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; + } -File Operations +Path Operations ~~~~~~~~~~~~~~~ -File and directory operations: - -- **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 +Building and inspecting a path, without touching the filesystem: + +- **append()** - Return a new path with a component appended +- **string()** - Get the native string representation +- **separator()** - Get the platform's path separator (static) + +Filesystem Operations +~~~~~~~~~~~~~~~~~~~~~ + +Operations that consult the filesystem: + +- **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) + +``expand()``, ``resolve()`` and ``mkdir()`` are declared to return +``std::expected``; ``home()`` returns +``std::optional``. Reading and writing file *contents* is not part of +``path`` β€” use the standard library's ```` for that. + +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, 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 + 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 --- @@ -118,47 +152,54 @@ 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()``, keeping +in mind that ``mkdir()`` reports an already-present directory as an error, +recognisable by its zero ``code()``. Example Usage ~~~~~~~~~~~~~ @@ -167,28 +208,38 @@ 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. mkdir() + // 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 && created.error().code()) { + 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; + } + + // 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 +265,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..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 ---------- @@ -20,18 +21,27 @@ 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. + + // 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; // Direct output + std::cout << num << std::endl; // number has operator<< } if (v5.is()) { @@ -52,7 +62,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 +73,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 +91,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 ~~~~~~ @@ -94,19 +114,29 @@ 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 - #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; + + // 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 + std::string result = s1; // "Hello World" + std::cout << result << std::endl; timestamp ~~~~~~~~~ @@ -121,19 +151,27 @@ The ``timestamp`` class provides comprehensive date and time handling with timez .. code-block:: cpp - #include + #include + #include + #include + + #include + #include - // Construction with timezone objects + // 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 +179,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 +197,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 +212,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 +228,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 +243,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 +271,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 +290,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 +330,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 +371,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 +395,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 +415,39 @@ 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``. 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: @@ -369,8 +472,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; @@ -389,19 +493,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 -------------- @@ -409,15 +523,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/changelog.rst b/docs/sphinx/source/changelog.rst index 86aac53..43e9f10 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 @@ -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 the classes listed above, + apart from ``environment``, which exposes only static members - 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/contributing.rst b/docs/sphinx/source/contributing.rst index edf1de2..a65fc25 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 ------- @@ -298,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 fd24584..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 @@ -20,37 +19,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 +76,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 +145,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 +258,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 +281,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 +292,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 +304,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; } @@ -345,8 +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 -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. 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 4449cb8..316896a 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; } @@ -214,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 63c9f72..7a1cb94 100644 --- a/docs/sphinx/source/index.rst +++ b/docs/sphinx/source/index.rst @@ -39,7 +39,9 @@ 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 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 @@ -122,21 +124,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,24 +156,33 @@ 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 -------- - **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 @@ -208,4 +227,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 87f0fe1..dfcdbe9 100644 --- a/docs/sphinx/source/user-guide/index.rst +++ b/docs/sphinx/source/user-guide/index.rst @@ -18,8 +18,11 @@ 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 -3. **Error Handling**: No exceptions - uses ``std::optional`` and ``std::expected`` +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 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 @@ -83,20 +86,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 +120,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 +148,16 @@ 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"); - - // Create directory if needed - if (auto result = path::create_directory(app_data); !result) { - std::cerr << "Failed to create directory: " - << result.error().message() << std::endl; - } \ No newline at end of file + 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 the directory. mkdir() succeeds only when it actually creates + // 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/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..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 @@ -69,8 +71,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. 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")) { * std::cout << "Created: " << result->string() << std::endl; @@ -87,7 +92,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, 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); @@ -119,7 +127,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(); @@ -152,6 +164,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..ddec440 100644 --- a/include/dross/type.h +++ b/include/dross/type.h @@ -6,18 +6,22 @@ * 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 - * - 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 * - 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 be54e45..425f07d 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 @@ -42,14 +45,17 @@ concept string_type = std::same_as || std::same_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. - * If the value is not of the requested type, the behavior is undefined. - * Use is() to check the type before casting. + * 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; @@ -293,9 +295,10 @@ 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. + * 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