diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 957b3c7..f6ac0ba 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,13 +1,59 @@ import taskLists from "markdown-it-task-lists"; -import { withMermaid } from "vitepress-plugin-mermaid"; +import defineVersionedConfig from "vitepress-versioning-plugin"; +import {withMermaid} from "vitepress-plugin-mermaid"; const BASE_PATH = '/' // https://vitepress.dev/reference/site-config -export default withMermaid({ +const defaultSidebar = [ + { + "text": "Get Started", + "items": [ + { "text": "Installation", "link": "./install" }, + { "text": "Basic Usage", "link": "./basic-usage" } + ] + }, + { + "text": "Using Bag", + "items": [ + { "text": "Collections", "link": "./collections" }, + { "text": "Casting Values", "link": "./casting" }, + { "text": "Mapping", "link": "./mapping" }, + { "text": "Variadics", "link": "./variadics" }, + { "text": "Hiding Properties", "link": "./hidden" }, + { "text": "Transformers", "link": "./transformers" }, + { "text": "Validation", "link": "./validation" }, + { "text": "Computed Properties", "link": "./computed-properties" }, + { "text": "Output", "link": "./output" }, + { "text": "Wrapping", "link": "./wrapping" }, + { "text": "Factories / Testing", "link": "./testing" } + ] + }, + { + "text": "Laravel Integration", + "items": [ + { "text": "Controller Injection", "link": "./laravel-controller-injection" }, + { "text": "Route Parameter Binding", "link": "./laravel-route-parameter-binding" }, + { "text": "Eloquent Casting", "link": "./laravel-eloquent-casting" }, + { "text": "Generating Bag Classes", "link": "./laravel-artisan-make-bag-command" } + ] + }, + { + "text": "Other", + "items": [ + { "text": "Creating Bags from Objects", "link": "./object-to-bag" }, + { "text": "Why Bag?", "link": "./why" }, + { "text": "How Bag Works", "link": "./how-bag-works" }, + ] + } +]; +export default withMermaid(defineVersionedConfig({ title: "Bag", description: "Immutable Value Objects for PHP 8.3+", base: BASE_PATH, + versioning: { + latestVersion: '2.1', + }, head: [ [ 'meta', @@ -41,57 +87,46 @@ export default withMermaid({ }, search: { provider: 'local', + options: { + locales: { + "root": { + translations: { + button: { + buttonText: "Search latest version…" + } + } + } + }, + async _render(src, env, md) { + const html = md.render(src, env) + if (env.frontmatter?.search === false) return '' + if (env.relativePath.match(/\d+\.(\d+|x)/) !== null) return '' + return html + } + }, }, // https://vitepress.dev/reference/default-theme-config nav: [ - { text: 'Home', link: '/' }, - { text: 'Documentation', link: '/install' } - ], - - sidebar: [ + { text: 'Home', link: './' }, + { text: 'Documentation', link: './install' }, { - text: 'Get Started', - items: [ - { text: 'Installation', link: '/install' }, - { text: 'Basic Usage', link: '/basic-usage' }, - ] - }, - { - text: 'Using Bag', - items: [ - { text: 'Collections', link: '/collections' }, - { text: 'Casting Values', link: '/casting' }, - { text: 'Mapping', link: '/mapping' }, - { text: 'Variadics', link: '/variadics' }, - { text: 'Hiding Properties', link: '/hidden' }, - { text: 'Transformers', link: '/transformers' }, - { text: 'Validation', link: '/validation' }, - { text: 'Computed Properties', link: '/computed-properties' }, - { text: 'Output', link: '/output' }, - { text: 'Wrapping', link: '/wrapping' }, - { text: 'Factories / Testing', link: '/testing' }, - ] - }, - { - text: 'Laravel Integration', - items: [ - { text: 'Controller Injection', link: '/laravel-controller-injection' }, - { text: 'Route Parameter Binding', link: '/laravel-route-parameter-binding' }, - { text: 'Eloquent Casting', link: '/laravel-eloquent-casting' }, - { text: 'Generating Bag Classes', link: '/laravel-artisan-make-bag-command' }, - ] - }, - { - text: 'Other', - items: [ - { text: 'Creating Bags from Objects', link: '/object-to-bag' }, - { text: 'Why Bag?', link: '/why' }, - { text: 'How Bag Works', link: '/how-bag-works' }, - { text: 'Roadmap', link: '/roadmap' }, - ] + component: 'VersionSwitcher', } ], + sidebar: { + "/": [ + ...defaultSidebar, + {"text": "What's New", "link": "./whats-new"}, + {"text": "Upgrading to Bag 2", "link": "./upgrading"} + ], + "/2.0/": [ + ...defaultSidebar, + {"text": "Upgrading to Bag 2", "link": "./upgrading"} + ], + "/1.x/": defaultSidebar, + }, + footer: { message: "Made with πŸ¦πŸ’–πŸ³οΈβ€πŸŒˆ by Davey Shafik.", copyright: "Released under the MIT License. Copyright Β© 2024 Davey Shafik.", @@ -99,7 +134,9 @@ export default withMermaid({ socialLinks: [ { icon: 'github', link: 'https://github.com/dshafik/bag' } - ] + ], + + versionSwitcher: false, }, markdown: { theme: { @@ -111,4 +148,4 @@ export default withMermaid({ md.use(taskLists) } }, -}) +}, __dirname)) diff --git a/docs/.vitepress/theme/OldVersionWarning.vue b/docs/.vitepress/theme/OldVersionWarning.vue new file mode 100644 index 0000000..f8a5f4e --- /dev/null +++ b/docs/.vitepress/theme/OldVersionWarning.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/docs/.vitepress/theme/VersionSwitcher.vue b/docs/.vitepress/theme/VersionSwitcher.vue new file mode 100644 index 0000000..d3e8f22 --- /dev/null +++ b/docs/.vitepress/theme/VersionSwitcher.vue @@ -0,0 +1,165 @@ + + + + + + + diff --git a/docs/.vitepress/theme/index.js b/docs/.vitepress/theme/index.js index b306d28..e6d8b64 100644 --- a/docs/.vitepress/theme/index.js +++ b/docs/.vitepress/theme/index.js @@ -1,5 +1,20 @@ // .vitepress/theme/index.js import DefaultTheme from 'vitepress/theme-without-fonts' import './custom.css' +import OldVersionWarning from './OldVersionWarning.vue' +import { h } from 'vue' +import VersionSwitcher from "./VersionSwitcher.vue"; -export default DefaultTheme +export default { + extends: DefaultTheme, + enhanceApp({ app }) { + app.component('VersionSwitcher', VersionSwitcher) + }, + Layout() { + return h(DefaultTheme.Layout, null, { + 'home-hero-before': () => h(OldVersionWarning), + 'doc-before': () => h(OldVersionWarning), + 'not-found': () => h(OldVersionWarning), + }) + } +} diff --git a/docs/basic-usage.md b/docs/basic-usage.md index a33185b..516374b 100644 --- a/docs/basic-usage.md +++ b/docs/basic-usage.md @@ -17,7 +17,7 @@ readonly class MyValue extends Bag { ``` > [!TIP] -> You can add an `@method` annotation to your class to provide auto-complete for the `::from()` method, or use the [Artisan Command with the --doc option](laravel-artisan-make-bag-command#updating-documentation) to generate it for you. +> You can add an `@method` annotation to your class to provide auto-complete for the `::from()` method, or use the [Artisan Command with the --doc option](laravel-artisan-make-bag-command.md#updating-documentation) to generate it for you. ## Instantiating a Value Object @@ -62,7 +62,7 @@ $value = MyValue::from('Davey Shafik', 40); > If you use positional arguments, you must ensure they are in the same order as the constructor. > [!WARNING] -> If you have a single array argument, **and** an array [transformer](transformers.md), the transformer will be applied to the array, potentially causing unwanted side-effects. +> If you have a single array argument, **and** an array [transformer](./transformers), the transformer will be applied to the array, potentially causing unwanted side-effects. ## Type Casting @@ -141,7 +141,7 @@ $value = MyValue::from([ ``` > [!TIP] -> You can also use the `StripExtraParameters` attribute when [injecting a Bag object into a controller](/laravel-controller-injection#avoiding-extra-parameters). +> You can also use the `StripExtraParameters` attribute when [injecting a Bag object into a controller](./laravel-controller-injection.md#avoiding-extra-parameters). ### Modifying a Value Object diff --git a/docs/casters.md b/docs/casters.md index de07fbe..70aa6e6 100644 --- a/docs/casters.md +++ b/docs/casters.md @@ -4,7 +4,7 @@ The following built-in explicit casters are available: ## Bag Collections -`\Bag\Casts\CollectionOf` casts an array to a [Collection](/collections) of Bag objects. +`\Bag\Casts\CollectionOf` casts an array to a [Collection](./collections) of Bag objects. ```php use Bag\Bag; diff --git a/docs/casting.md b/docs/casting.md index 0cc8894..424acf1 100644 --- a/docs/casting.md +++ b/docs/casting.md @@ -58,7 +58,7 @@ dump($value->toArray()); // ['name' => 'Davey Shafik', 'age' => 40, 'dateOfBirth ### Available Casts -Bag supports several [built-in casters](/casters), and you can create your own by implementing `CastsPropertySet` and/or `CastsPropertyGet`. +Bag supports several [built-in casters](./casters), and you can create your own by implementing `CastsPropertySet` and/or `CastsPropertyGet`. ## Automatic Casting diff --git a/docs/index.md b/docs/index.md index 2788036..7de2165 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,7 +10,7 @@ hero: actions: - theme: brand text: Get Started - link: /install + link: ./install features: - title: Immutable & Strongly typed diff --git a/docs/laravel-controller-injection.md b/docs/laravel-controller-injection.md index 788a48f..e73906d 100644 --- a/docs/laravel-controller-injection.md +++ b/docs/laravel-controller-injection.md @@ -31,7 +31,7 @@ class MyController extends Controller { ``` > [!TIP] -> You can also add the `StripExtraParameters` attribute to [the Bag class itself](/basic-usage#stripping-extra-parameters). +> You can also add the `StripExtraParameters` attribute to [the Bag class itself](./basic-usage.md#stripping-extra-parameters). ## Automatic Validation diff --git a/docs/mapping.md b/docs/mapping.md index edcfa1a..c7ac101 100644 --- a/docs/mapping.md +++ b/docs/mapping.md @@ -128,7 +128,7 @@ class MyValue extends Bag { Input mapping is applied when calling `Bag::from()` or `Bag::withoutValidation()`. You can use either the original property name _or_ the mapped name when creating a Bag. > [!TIP] -> [Validation](validation) is applied to the original property name, not the mapped name. +> [Validation](./validation) is applied to the original property name, not the mapped name. Output mapping is applied when calling `$Bag->toArray()`, `$Bag->toCollection()`, or `$Bag->toJson()` (or when using `json_encode()`). diff --git a/docs/output.md b/docs/output.md index 8def116..7b539c8 100644 --- a/docs/output.md +++ b/docs/output.md @@ -11,10 +11,10 @@ $value->name; // 'Davey Shafik' ## Casting to Other Types -Bag supports casting to arrays and [`\Bag\Collections`](/collections##extending-laravel-collections) using the +Bag supports casting to arrays and [`\Bag\Collections`](./collections.md##extending-laravel-collections) using the `Bag->toArray()` and `Bag->toCollection()` methods. -Both methods will apply casting and mapping, as well as respect [hidden properties](/hidden). +Both methods will apply casting and mapping, as well as respect [hidden properties](./hidden). ## JSON Serialization @@ -27,4 +27,4 @@ $value->toJson(); // {"name": "Davey Shafik", "age": 40} json_encode($value, JSON_THROW_ON_ERROR); // {"name": "Davey Shafik", "age": 40} ``` -Both `Bag->toJson()` and `json_encode()` will respect [hidden properties](/hidden). +Both `Bag->toJson()` and `json_encode()` will respect [hidden properties](./hidden). diff --git a/docs/testing.md b/docs/testing.md index c644ac1..c2fb49e 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -32,7 +32,7 @@ return [ ``` > [!TIP] -> You can also generate factory classes automatically using the [`artisan make:bag`](/laravel-artisan-make-bag-command) command. +> You can also generate factory classes automatically using the [`artisan make:bag`](./laravel-artisan-make-bag-command) command. ## Using a Factory diff --git a/docs/upgrading.md b/docs/upgrading.md new file mode 100644 index 0000000..ca93752 --- /dev/null +++ b/docs/upgrading.md @@ -0,0 +1,50 @@ +# Upgrading to Bag 2 + +This guide will help you upgrade your application from Bag 1.x to Bag 2.x. + +## Casting with Union Types + +To support union types fully, the `\Bag\Casts\CastsPropertySet::set()` method signature has changed the first argument from: + +```php +public function set(string $propertyType, string $propertyName, \Illuminate\Support\Collection $properties): mixed +``` + +to: + +```php +public function set(\Bag\Collection $propertyTypes, string $propertyName, \Illuminate\Support\Collection $properties): mixed +``` + +This change allows for union types to be passed in as a collection of types (as string type names). Your return value must match one of the types in the collection. + +## Legacy Behavior + +In Bag 1.x the first type in the union was used to cast the property. To update your code and retain the previous behavior, you will want to do the following: + +```diff +- public function set(string $propertyType, string $propertyName, \Illuminate\Support\Collection $properties): mixed +- { ++ public function set(\Bag\Collection $propertyTypes, string $propertyName, \Illuminate\Support\Collection $properties): mixed ++ { ++ $propertyType = $propertyTypes->first(); +``` + +## Fill Nullables + +The behavior when instantiating a Bag has changed such that arguments that are nullable _without_ a default value are filled with nulls. +Previously, this would have caused exception to be thrown. This solves for a common scenario when you are filling a Bag from user input. + +```php +readonly class MyBag extends Bag { + public function __construct( + public ?string $name + } +} + +// Bag 1.4.0 (and older) +MyBag::from([]); // throws MissingPropertiesException + +// Bag 2.0.0+ +MyBag::from([]); // MyBag { $name = null } +``` diff --git a/docs/versions/1.x/basic-usage.md b/docs/versions/1.x/basic-usage.md new file mode 100644 index 0000000..beffd3b --- /dev/null +++ b/docs/versions/1.x/basic-usage.md @@ -0,0 +1,98 @@ +# Basic Usage + +## Creating a Value Object + +To create a new Value Object, extend the `Bag\Bag` class and define your properties in the constructor: + +```php +use Bag\Bag; + +readonly class MyValue extends Bag { + public function __construct( + public string $name, + public int $age, + ) { + } +} +``` + +> [!TIP] +> You can add an `@method` annotation to your class to provide auto-complete for the `::from()` method, or use the [Artisan Command with the --doc option](laravel-artisan-make-bag-command#updating-documentation) to generate it for you. + + +## Instantiating a Value Object + +To create a new instance of your Value Object, call the `::from()` method. You can use an array, a Collection, named arguments, or positional arguments. + + +### Named Arguments + +```php +$value = MyValue::from( + name: 'Davey Shafik', + age: => 40, +); +``` + +### Array or Collection of Arguments + +```php +$value = MyValue::from([ + 'name' => 'Davey Shafik', + 'age' => 40, +]); +``` + +or: + +```php +$value = MyValue::from(collect([ + 'name' => 'Davey Shafik', + 'age' => 40, +])); +``` + +### Positional Arguments + +```php +$value = MyValue::from('Davey Shafik', 40); +``` + +> [!WARNING] +> If you use positional arguments, you must ensure they are in the same order as the constructor. + +> [!WARNING] +> If you have a single array argument, **and** an array [transformer](./transformers), the transformer will be applied to the array, potentially causing unwanted side-effects. + +## Type Casting + +Bag will cast all values to their defined type _automatically_ for all scalar types, as well as the following: + +- `Bag` objects +- `\Bag\Collection` and `\Illuminate\Support\Collection` objects +- `\DateTimeInterface` objects will be cast using standard [PHP Date/Time formats](https://www.php.net/manual/en/datetime.formats.php) + - This includes `\DateTime`, `\DateTimeImmutable`, `\Carbon\Carbon` and `\Carbon\CarbonImmutable` +- Enums + +> [!TIP] +> We recommend using `\Carbon\CarbonImmutable` for all date times. + +### Modifying a Value Object + +Value Objects are immutable, so you cannot change their properties directly. Instead, you can create a new instance with the updated values using the `Bag->with()` or `Bag->append()` methods: + +```php +$value = MyValue::from([ + 'name' => 'Davey Shafik', + 'age' => 40, +]); + +$newValue = $value->with(age: 41); + +dump($newValue->toArray()); // ['name' => 'Davey Shafik', 'age' => 41] +``` + +You can pass either named arguments, or an array or Collection of key-value pairs to the `Bag->with()` method. + +> [!TIP] +> The `Bag->append()` method works the same way as `Bag->with()`, but it will not validate the new value object. You can manually validate the object using `Bag->valid()`. diff --git a/docs/versions/1.x/casters.md b/docs/versions/1.x/casters.md new file mode 100644 index 0000000..8eb93bf --- /dev/null +++ b/docs/versions/1.x/casters.md @@ -0,0 +1,134 @@ +# Built-in Casters + +The following built-in explicit casters are available: + +## Bag Collections + +`\Bag\Casts\CollectionOf` casts an array to a [Collection][./collections] of Bag objects. + +```php +use Bag\Bag; +use Bag\Attributes\Cast; +use Bag\Casts\CollectionOf; +use Bag\Collection; + +class MyValue extends Bag { + public function __construct( + #[Cast(CollectionOf::class, MyOtherValue::class)] + public Collection $values, + ) { + } +} + +$value = MyValue::from([ + 'values' => [ + ['name' => 'Davey Shafik', 'age' => 40], + … + ], +]); + +dump($value->values); // Collection +``` + +> [!TIP] +> The `CollectionOf` caster will use the appropriate Collection class based on the Bag class provided. + +## Dates & Time + +`\Bag\Casts\DateTime` casts a date/time string to an object that implements the `\DateTimeInterface` interface. + +```php +use Bag\Bag; +use Bag\Attributes\Cast; +use Bag\Casts\DateTime; +use Carbon\CarbonImmutable; + +class MyValue extends Bag { + public function __construct( + #[Cast( + DateTime::class, + format: 'u', + outputFormat: + 'Y-m-d', strictMode: true + )] + public CarbonImmutable $dateOfBirth, + ) { + } +} + +$value = MyValue::from([ + // Requires a UNIX timestamp input due to strictMode: true + 'dateOfBirth' => '454809600', +]); + +$value->dateOfBirth; // CarbonImmutable{date: 1984-05-31 00:00:00 UTC} +$value->toArray(); // ['dateOfBirth' => '1984-05-31'] due to outputFormat +``` + +The `DateTime` cast accepts the following parameters: + +- `format` - The format of the input and output date/time string (see [PHP date/time format](https://www.php.net/manual/en/datetimeimmutable.createfromformat.php#datetimeimmutable.createfromformat.parameters)) +- `outputFormat` - The format of the output date/time string, this will override the `format` parameter for output only +- `strictMode` - If `true`, the input date/time string must match the `format` parameter exactly, otherwise it is parsed as best as possible +- `dateTimeClass` - The class to use for the date/time object, must implement `\DateTimeInterface`, this must be compatible with the property type hint, and defaults to the type hint value + +> [!TIP] +> We recommend using `CarbonImmutable` as type for all date/time casting. + +## Money + +`\Bag\Casts\MoneyFromMinor` Casts a number to a `\Brick\Money\Money` object assuming minor units (e.g. Cents) + +```php +use Bag\Bag; +use Bag\Attributes\Cast; +use Bag\Casts\MoneyFromMinor; +use Brick\Money\Money; + +class MyValue extends Bag { + public function __construct( + #[Cast(MoneyFromMinor::class, currency: 'USD')] + public Money $amount, + ) { + } +} + +$value = MyValue::from([ + 'amount' => 1000, +]); + +dump($value->amount); // Money object with a value of 10.00 USD +``` + +`\Bag\Casts\MoneyFromMajor` Casts a number to a `\Brick\Money\Money` object assuming major units (e.g. Dollars) + +```php +use Bag\Bag; +use Bag\Attributes\Cast; +use Bag\Casts\MoneyFromMajor; +use Brick\Money\Money; + +class MyValue extends Bag { + public function __construct( + #[Cast(MoneyFromMajor::class, currency: 'USD')] + public Money $amount, + ) { + } +} + +$value = MyValue::from([ + 'amount' => 1000, +]); + +dump($value->amount); // Money object with a value of 1,000.00 USD +``` + +Both `MoneyFromMajor` and `MoneyFromMinor` accept the following parameters: + +- `currency` - The currency code to use for the `\Brick\Money\Money` object, either a 3-letter ISO 4217 code or a [`\PrinsFrank\Standards\Currency\CurrencyAlpha3`](https://github.com/PrinsFrank/standards/blob/main/src/Currency/CurrencyAlpha3.php#L22) enum case. +- `currencyProperty` - The input parameter to use for the currency code +- `locale` - The locale to use for formatting the money object, defaults to `en_US` + +> [!TIP] +> Best practices recommend that you always handle money as strings of minor units, _however_ if you are working with user input it's most likely in major units. +> We recommend using `MoneyFromMajor` as the input caster and `MoneyFromMinor` as the output caster for handling user input. diff --git a/docs/versions/1.x/casting.md b/docs/versions/1.x/casting.md new file mode 100644 index 0000000..bda5afe --- /dev/null +++ b/docs/versions/1.x/casting.md @@ -0,0 +1,107 @@ +# Casting Values + +Casting allows you to format input and output values appropriately. + +## Explicit Casting + +You can explicitly cast input and/or output values to a specific type using annotations. This allows you to cast simple values to complex values, +or simply transform it in some way. For example, you can cast a number to a `\Brick\Money\Money` object or a timestamp to a `\Carbon\CarbonImmutable` object. + +Casts can act on both input and output, depending on if they implement `CastsPropertySet` or `CastsPropertyGet`. + +Output casts are applied when calling `toArray()`, `toCollection()`, or when serializing to JSON. + +The following annotations are available: + +- `Bag\Attributes\Cast` - Cast both input and output depending on whether it implements `CastsPropertySet` and/or `CastsPropertyGet` +- `Bag\Attributes\CastInput` β€” Only cast input (requires the caster implements `CastsPropertySet`) +- `Bag\Attributes\CastOutput` β€” Only cast output (requires the caster implements `CastsPropertyGet`) + +```php +use Bag\Bag; +use Bag\Attributes\Cast; +use Bag\Casts\DateTime; +use Carbon\CarbonImmutable; + +readonly class MyValue extends Bag { + public function __construct( + public string $name, + public int $age, + #[Cast(DateTime::class, format: 'Y-m-d')] + public CarbonImmutable $dateOfBirth, + ) { + } +} +``` + +This will cast the `dateOfBirth` property to a `\Carbon\CarbonImmutable` object using the `Y-m-d` format: + +```php +$value = MyValue::from([ + 'name' => 'Davey Shafik', + 'age' => 40, + 'dateOfBirth' => '1984-05-31', +]); +``` + +When you access the `dateOfBirth` property directly, it will be a `\Carbon\CarbonImmutable` object: + +```php +dump($value->dateOfBirth); // CarbonImmutable +``` + +However, if you call `toArray()` or `toCollection()` then it will be formatted using the format you specified: + +```php +dump($value->toArray()); // ['name' => 'Davey Shafik', 'age' => 40, 'dateOfBirth' => '1984-05-31'] +``` + +### Available Casts + +Bag supports several [built-in casters][./casters], and you can create your own by implementing `CastsPropertySet` and/or `CastsPropertyGet`. + + +## Automatic Casting + +If no cast attribute is provided, input values are _automatically_ cast to the correct type based on the type hint of the property when possible. + +The following types are automatically cast: + +- Scalar types (i.e. `int`, `float`, `bool`, `string`) +- `\CarbonImmutable`, `\Carbon`, `\DateTimeImmutable`, and `\DateTime` objects (parsing the value using [PHP date/time formats](https://www.php.net/manual/en/datetime.formats.php)) +- `Bag` objects from arrays +- Laravel Collections (i.e. `collect($value)`) +- BackedEnums from value (i.e. `public FooEnum $foo` will cast the value `BAR` using `FooEnum::from('BAR')`) +- UnitEnums by name (i.e. `public FooEnum $foo` will cast the value `BAR` to `FooEnum::BAR`) +- Laravel Models from IDs (using `Model::findOrFail($value)`) + +All other types will be set to the input value (which may be invalid). Input objects that match the type hint are not re-cast. + +For example, given the following Bag class: + +```php +use Bag\Bag; +use Carbon\CarbonImmutable; + +class MyValue extends Bag { + public function __construct( + public CarbonImmutable $dateOfBirth, + ) { + } +} +``` + +When you create a new instance of `MyValue`, the `dateOfBirth` property will be a `\Carbon\CarbonImmutable` object: + +```php +MyValue::from([ + 'dateOfBirth' => '1984-05-31', +]); +``` + +However, if you pass in a value like `12:34:56`, it will result in a `\Carbon\CarbonImmutable` object with the current date and the time set to `12:34:56`. + +> [!WARNING] +> To avoid this issue, we recommend always using a `DateTime` cast as shown above, setting the `strictMode` parameter to `true`. This allows you to specify a required input format. + +When you access the `dateOfBirth` property directly, it will be the `\Carbon\CarbonImmutable` object. diff --git a/docs/versions/1.x/collections.md b/docs/versions/1.x/collections.md new file mode 100644 index 0000000..a862cdb --- /dev/null +++ b/docs/versions/1.x/collections.md @@ -0,0 +1,73 @@ +# Collections + +## Using Collections + +You can create a collection of Value objects using the `Bag::collect()` method: + +```php +$values = MyValue::collect([ + [ + 'name' => 'Davey Shafik', + 'age' => 40, + ], + [ + 'name' => 'Taylor Otwell', + 'age' => 40, + ], +]); +``` + +This will create a `\Bag\Collection` of `MyValue` objects. + +### Extending Laravel Collections + +`Bag\Collection` extends `\Illuminate\Support\Collection` and has [most of the same methods](https://laravel.com/docs/11.x/collections#available-methods). + +The following methods will throw an exception if used: + +- `->pull()` +- `->shift()` +- `->splice()` +- `->transform()` +- `->getOrPut()` +- `offsetSet()` (used with array access) +- `offsetUnset()` (used with array access) +- `__set()` (used when setting arbitrary properties) + +In addition, the following will create a new `Bag\Collection` instance instead of modifying the original: + +- `->forget()` +- `->pop()` +- `->prepend()` +- `->push()` +- `->put()` + +## Custom Collections + +If you want to use a custom collection class, you must first create a new class that extends `\Bag\Collection`: + +```php +use Bag\Collection; + +class MyValueCollection extends Collection { +} +``` + +Then you can use this collection class in your Value object by adding the `Collection` attribute to your Bag class: + +```php +use App\Values\Collections\MyValueCollection; +use Bag\Bag; +use Bag\Attributes\Collection; + +#[Collection(MyValueCollection::class) +readonly class MyValue extends Bag { + public function __construct( + public string $name, + public int $age, + ) { + } +} +``` + +When using `MyValue::collect()` a `MyValueCollection` object will be returned. diff --git a/docs/versions/1.x/computed-properties.md b/docs/versions/1.x/computed-properties.md new file mode 100644 index 0000000..6a15641 --- /dev/null +++ b/docs/versions/1.x/computed-properties.md @@ -0,0 +1,28 @@ +# Computed Properties + +Bag supports computed properties, these are properties that are derived at creation time rather than passed in. + +Failing to set a computed property will result in a `Bag\Exceptions\ComputedPropertyMissing` exception. + +## Using Computed Properties + +To use computed properties, define the property in your class, and add the `Bag\Attributes\Computed` attributed: + +```php{7,11} +use Bag\Bag; +use Bag\Attributes\Computed; +use Carbon\CarbonImmutable; + +class MyValue extends Bag +{ + #[Computed] + public string $computedProperty; + + public function __construct() { + $this->computedProperty = CarbonImmutable::now(); + } +} +``` + +> [!WARNING] +> You **must** set the property within the constructor, otherwise an exception will be thrown. diff --git a/docs/versions/1.x/hidden.md b/docs/versions/1.x/hidden.md new file mode 100644 index 0000000..4ccb8b3 --- /dev/null +++ b/docs/versions/1.x/hidden.md @@ -0,0 +1,79 @@ +# Hidden Properties + +Bag supports hiding properties when transforming to an array and/or JSON. +This is useful when you want to keep certain properties private, or when you want to hide sensitive information. + +Hidden properties are still accessible as object properties (e.g. `$bag->hiddenProperty`), but they will not be included +in the output when transforming to an array and/or JSON. + +## Hiding Properties + +Properties can be hidden by applying the `Hidden` attribute to the property: + +```php +use Bag\Attributes\Hidden; +use Bag\Bag; + +class MyValue extends Bag { + public function __construct( + #[Hidden] + public string $hiddenProperty + public string $visibleProperty + ) {} +} +``` + +In the above example, the `hiddenProperty` will not be included when calling `toArray()`, `toCollection()` or `json_encode()`: + +```php +$value = MyValue::from([ + 'hiddenProperty' => 'hidden', + 'visibleProperty' => 'visible' +]); + +$value->toArray(); // ['visibleProperty' => 'visible'] +json_encode($value, JSON_THROW_ON_ERROR); // {"visibleProperty": "visible"} +``` + +Additionally, you can use PHP's built-in `SensitiveParameter` attribute to hide sensitive information: + +```php +use Bag\Bag; +use SensitiveParameter; + +class MyValue extends Bag { + public function __construct( + #[SensitiveParameter] + public string $password + ) {} +} +``` + +## Hiding Properties from JSON + +In addition to always hiding properties, you can choose to _only_ hide them when serializing to JSON by using the `HiddenFromJson` attribute: + +```php +use Bag\Attributes\HiddenFromJson; +use Bag\Bag; + +class MyValue extends Bag { + public function __construct( + #[HiddenFromJson] + public string $hiddenProperty + public string $visibleProperty + ) {} +} +``` + +In the above example, the `hiddenProperty` will be included when calling `json_encode()`, but not when calling `toArray()`: + +```php +$value = MyValue::from([ + 'hiddenProperty' => 'hidden', + 'visibleProperty' => 'visible' +]); + +$value->toArray(); // ['hiddenProperty' => 'hidden', visibleProperty' => 'visible'] +json_encode($value, JSON_THROW_ON_ERROR); // {"visibleProperty": "visible"} +``` diff --git a/docs/versions/1.x/how-bag-works.md b/docs/versions/1.x/how-bag-works.md new file mode 100644 index 0000000..9546dff --- /dev/null +++ b/docs/versions/1.x/how-bag-works.md @@ -0,0 +1,213 @@ +# How Bag Works + +Bag works by utilizing pipelines to process the input and output data. + +There are four pipelines: + +- `InputPipeline` for handling input and constructing the Bag object +- `ValidationPipeline` for validating without constructing the Bag object +- `OutputPipeline` for handling output +- `OutputCollectionPipeline` for handling collection output + +Each pipeline is detailed below. + +> [!TIP] +> Each stage in the diagrams below is linked to the relevant source code for that stage. + +## The Input Pipeline + +The [`InputPipeline`](https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/InputPipeline.php) is responsible for processing the input data so that the `Bag` object can be created. The pipeline +consists of the following steps: + +```mermaid +graph TD; +start("Bag::from($data)") +--> transform(Transform Input) +--> process(Process Parameters) +--> variadic(Is Variadic?) +--> mapInput(Map Input) +--> laravelParams(Laravel Route Parameter Binding) +-- Finalized Input Values --> missing(Missing Parameters) --> missingError{Error?} +missingError -- Yes --> errorMissingParameters(MissingPropertiesException) +missingError -- No --> extra(Extra Parameters) --> extraError{Error?} +extraError -- Yes --> errorExtraParameters(ExtraPropertiesException) +extraError -- No --> validate(Validate) +--> valid{Valid?} +valid -- No --> errorValidation(ValidationException) +valid -- Yes --> cast(Cast Input) +--> construct("new Bag(...)") +--> computed(Verify Computed Values) +--> initialized{Initialized?} +initialized -- No --> errorInitialization(ComputedPropertyUninitializedException) +initialized -- Yes +--> bag(Bag Value Returned) + +class start mermaid-start +class missingError,extraError,valid,initialized mermaid-decision +class bag mermaid-end +class errorMissingParameters,errorExtraParameters,errorValidation,errorInitialization mermaid-error + +click start "https://github.com/dshafik/bag/blob/main/src/Bag/Bag.php" _blank +click transform "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/Transform.php" _blank +click process "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ProcessParameters.php" _blank +click variadic "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/IsVariadic.php" _blank +click mapInput "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/MapInput.php" _blank +click laravelParams "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/LaravelRouteParameters.php" _blank +click missing "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/MissingParameters.php" _blank +click extra "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ExtraParameters.php" _blank +click validate "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/Validate.php" _blank +click cast "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/CastInputValues.php" _blank +click construct "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/FillBag.php" _blank +click computed "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ComputedValues.php" _blank +``` + +## The Output Pipeline + +The `OutputPipeline` is responsible transforming the Bag data to the desired output array or JSON. The pipeline consists of the following steps: + +```mermaid +graph TD; +toArray("Bag->toArray()") --> processParameters +toCollection("Bag->toCollection()") --> processParameters +toJson("Bag->toJson()") --> processParameters +jsonEncode("json_encode($bag)") --> processParameters +get("Bag->get()") --> processParameters +unwrapped("Bag->unwrapped()") --> processParameters +processParameters(Process Parameters) +--> processProperties(Process Properties) +--> getValues(Get Values) +--> hide("Remove Hidden Attributes*") +--> hideJson("Remove Hidden JSON Attributes*") +--> cast(Cast Output) +--> mapOutput(Map Output) +--> wrap(Wrap Output*) +--> output(array or JSON string) + +class toArray,toCollection,toJson,jsonEncode,get,unwrapped mermaid-start +class output mermaid-end +class hide,hideJson,wrap mermaid-conditional + +click toArray "https://github.com/dshafik/bag/blob/main/src/Bag/Concerns/WithArrayable.php" _blank +click toCollection "https://github.com/dshafik/bag/blob/main/src/Bag/Concerns/WithCollections.php" _blank +click toJson "https://github.com/dshafik/bag/blob/main/src/Bag/Concerns/WithJson.php" _blank +click get "https://github.com/dshafik/bag/blob/main/src/Bag/Concerns/WithOutput.php" _blank +click unwrapped "https://github.com/dshafik/bag/blob/main/src/Bag/Concerns/WithOutput.php" _blank +click processParameters "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ProcessParameters.php" _blank +click processProperties "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ProcessProperties.php" _blank +click getValues "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/GetValues.php" _blank +click hide "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/HideValues.php" _blank +click hideJson "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/HideJsonValues.php" _blank +click cast "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/CastOutputValues.php" _blank +click mapOutput "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/MapOutput.php" _blank +click wrap "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/Wrap.php" _blank +``` + +> [!NOTE] +> \* These steps are only performed if the Bag is being converted to an array and/or JSON. + +## The Validation Pipeline + +The `ValidationPipeline` is responsible for validating the input data without constructing the Bag object. The pipeline consists of the following steps: + +```mermaid +graph TD; +start("Bag::validate($data)") +--> transform(Transform Input) +--> process(Process Parameters) +--> variadic(Is Variadic?) +--> mapInput(Map Input) +-- Finalized Input Values --> missing(Missing Parameters) --> missingError{Error?} +missingError -- Yes --> errorMissingParameters(MissingPropertiesException) +missingError -- No --> extra(Extra Parameters) --> extraError{Error?} +extraError -- Yes --> errorExtraParameters(ExtraPropertiesException) +extraError -- No --> validate(Validate) +--> valid{Valid?} +valid -- No --> errorValidation(ValidationException) +valid -- Yes --> success(return true) + +class start mermaid-start +class missingError,extraError,valid mermaid-decision +class success mermaid-end +class errorMissingParameters,errorExtraParameters,errorValidation mermaid-error + +click start "https://github.com/dshafik/bag/blob/main/src/Bag/Concerns/WithValidation.php" _blank +click transform "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/Transform.php" _blank +click process "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ProcessParameters.php" _blank +click variadic "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/IsVariadic.php" _blank +click mapInput "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/MapInput.php" _blank +click missing "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/MissingParameters.php" _blank +click extra "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ExtraParameters.php" _blank +click validate "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/Validate.php" _blank +``` + +## The Without Validate Pipeline + +The [`WithoutValidationPipeline`](https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/WithoutValidationPipeline.php) is identical to the `InputPipeline` but does not perform validation. The pipeline +consists of the following steps: + +```mermaid +graph TD; +start("Bag::from($data)") +--> transform(Transform Input) +--> process(Process Parameters) +--> variadic(Is Variadic?) +--> mapInput(Map Input) +--> laravelParams(Laravel Route Parameter Binding) +-- Finalized Input Values --> missing(Missing Parameters) --> missingError{Error?} +missingError -- Yes --> errorMissingParameters(MissingPropertiesException) +missingError -- No --> extra(Extra Parameters) --> extraError{Error?} +extraError -- Yes --> errorExtraParameters(ExtraPropertiesException) +extraError -- No +--> construct("new Bag(...)") +--> computed(Verify Computed Values) +--> initialized{Initialized?} +initialized -- No --> errorInitialization(ComputedPropertyUninitializedException) +initialized -- Yes +--> bag(Bag Value Returned) + +class start mermaid-start +class missingError,extraError,valid,initialized mermaid-decision +class bag mermaid-end +class errorMissingParameters,errorExtraParameters,errorValidation,errorInitialization mermaid-error + +click start "https://github.com/dshafik/bag/blob/main/src/Bag/Bag.php" _blank +click transform "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/Transform.php" _blank +click process "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ProcessParameters.php" _blank +click variadic "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/IsVariadic.php" _blank +click mapInput "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/MapInput.php" _blank +click laravelParams "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/LaravelRouteParameters.php" _blank +click missing "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/MissingParameters.php" _blank +click extra "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ExtraParameters.php" _blank +click validate "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/Validate.php" _blank +click cast "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/CastInputValues.php" _blank +click construct "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/FillBag.php" _blank +click computed "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ComputedValues.php" _blank +``` + +## The Output Collection Pipeline + +The `OutputCollectionPipeline` is responsible for transforming the Bag collection data to the desired output array or JSON. The pipeline consists of the following steps: + +```mermaid +graph TD; +toArray("Collection->toArray()") --> wrap +toJson("Collection->toJson()") --> wrap +jsonEncode("json_encode($collection)") --> wrap +unwrapped("Collection->unwrapped()") --> wrap +wrap(Wrap Collection*) +--> output(array or JSON string) + +class toArray,toJson,jsonEncode,unwrapped mermaid-start +class output mermaid-end + +click toArray "https://github.com/dshafik/bag/blob/main/src/Bag/Collection.php" _blank +click toJson "https://github.com/dshafik/bag/blob/main/src/Bag/Collection.php" _blank +click jsonEncode "https://github.com/dshafik/bag/blob/main/src/Bag/Collection.php" _blank +click unwrapped "https://github.com/dshafik/bag/blob/main/src/Bag/Collection.php" _blank +click wrap "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/WrapCollection.php" _blank +``` + +> [!NOTE] +> \* This step is only performed if the Bag is being converted to an array and/or JSON. + + diff --git a/docs/versions/1.x/index.md b/docs/versions/1.x/index.md new file mode 100644 index 0000000..7de2165 --- /dev/null +++ b/docs/versions/1.x/index.md @@ -0,0 +1,41 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: "Bag" + text: "Immutable Value Objects for PHP" + tagline: "composer require dshafik/bag" + image: /assets/images/bag.png + actions: + - theme: brand + text: Get Started + link: ./install + +features: + - title: Immutable & Strongly typed + icon: 🦾 + details: Bag value objects are immutable and strongly typed, a safer and more predictable way to work with data. + - title: Composable + icon: πŸ›  + details: Nest Bag value objects and collections to create complex data structures. + - title: Built on Laravel + icon: πŸ“¦ + details: Built-in validation, controller dependency injection, and more. Bag is designed to work seamlessly with Laravel. +--- + +## What is Bag? + +Bag helps you create immutable value objects. It's a great way to encapsulate data within your application. + +Bag prioritizes immutability and type safety with built-in validation and data casting. + +### When should I use Value Objects? + +Value objects should be used in place of regular arrays, allowing you enforce type safety and immutability. + +### Does it work with Laravel/Symfony/Other Framework? + +Bag is framework-agnostic, but it works great with Laravel. Bag uses standard Laravel [Collections](https://laravel.com/docs/11.x/collections) and [Validation](https://laravel.com/docs/11.x/validation). +In addition, it will automatically inject `Bag\Bag` value objects into your controllers with validation. + diff --git a/docs/versions/1.x/install.md b/docs/versions/1.x/install.md new file mode 100644 index 0000000..0398583 --- /dev/null +++ b/docs/versions/1.x/install.md @@ -0,0 +1,15 @@ +# Get Started with Bag Value Objects + +Bag is a library for creating immutable value objects in PHP. It's a great way to encapsulate data within your application. + +## Requirements + +Bag requires PHP 8.2+, and supports Laravel 10+. + +## Installation + +To install Bag, use [Composer](https://getcomposer.org): + +```bash +composer require dshafik/bag +``` diff --git a/docs/versions/1.x/laravel-artisan-make-bag-command.md b/docs/versions/1.x/laravel-artisan-make-bag-command.md new file mode 100644 index 0000000..be821a3 --- /dev/null +++ b/docs/versions/1.x/laravel-artisan-make-bag-command.md @@ -0,0 +1,289 @@ +# Generate Bag Value Objects, Collections, and Factories + +Bag includes the `make:bag` artisan command to make it easy to generate new Bag classes, Factories, and Collections. + +> [!NOTE] Namespaces +> The `make:bag` command will use the provided namespace to determine the location of the generated classes. +> You can set the namespace using the `--namespace` option or you will be prompted for it. The default namespace is `\App\Values`. + +## Generating Bags + +To create a new Bag class, use the `make:bag` command: + +```bash +php artisan make:bag MyBag +``` + +This will create a new `MyBag` class in `app/Values/MyBag.php`: + +```php + [!NOTE] +> When creating a new Bag _or_ when specifying the `--update` flag, it will automatically add the `Collection` attribute to the Bag class if necessary. + +## Generating Bag Factories + +The `make:bag` command can also generate Bag Factory classes, **and** will _automatically_ generate the `definition()` function based on the Bag class properties. + +To create a new Bag Factory class, use the `make:bag` command with the `--factory` option: + +```bash +php artisan make:bag MyBag --factory +``` + +This will create a new `MyBagFactory` class in `app/Values/Factories/MyBagFactory.php`: + +```php + [!TIP] +> When creating a new Bag _or_ when specifying the `--update` flag, it will automatically add the `Factory` attribute and `HasFactory` trait to the Bag class. + +## Updating Factories + +Once you had added properties to your Bag class, you can update the Factory class using the `--update` option: + +```bash +php artisan make:bag MyBag --factory --docs --update +``` + +> [!TIP] +> If you update the Bag class properties, you can re-run the `make:bag` command with the `--update` and `--force-excluding-bag` options to update the Factory class. + +For example, if we update our `MyBag` constructor to look like the following: + +```php +public function __construct( + public string $name, + public int $age, + #[Cast(DateTime::class, 'y-m-d')] + public CarbonImmutable $birthday, + public Money $money, + public AnotherBag $test, + #[Cast(CollectionOf::class, AnotherBag::class)] + public Collection $collection, +) { +} +``` + +The generated factory will look like this: + +```php + $this->faker->word(), + 'age' => $this->faker->randomNumber(), + 'birthday' => new CarbonImmutable(), + 'money' => Money::ofMinor($this->faker->numberBetween(100, 10000), 'USD'), + 'test' => AnotherBag::factory()->make(), + 'collection' => AnotherBag::collect([AnotherBag::factory()->make()]), + ]; + } +} +``` + +## Documentation/Auto-Complete Helpers + +To enable easier use of named/positional arguments when creating a new Bag, you can use the `--docs` option to automatically generate it for the Bag, +this will add an `@method` annotation to the Bag class to provide auto-complete for the `::from()` method: + +```bash +php artisan make:bag MyBag --docs +``` + +This will add the following to the `MyBag` class: + +```php +/** + * @method static static from(array $data) + */ +``` + +## Updating Documentation + +Similar to Factories, you can update the documentation using the `--update` option: + +```bash +php artisan make:bag MyBag --docs --update +``` + +This will update the `@method` annotation to include documentation for all defined Value attributes. For example, given the following bag: + +```php +use App\Values\Collections\ReportCollection; +use App\Values\Report; +use App\Values\Team; +use Carbon\CarbonImmutable; + +readonly class User extends Bag { + public function __construct( + public string $name, + public int $age, + public Team $team, + #[Cast(CollectionOf::class, Report::class)] + public ReportCollection $reports, + public CarbonImmutable $lastLogin, + ) { + } +} +``` + +The `@method` annotation will added to the `User` class: + +```php +use App\Values\Collections\ReportCollection; +use App\Values\Report; +use App\Values\Team; +use Carbon\CarbonImmutable; + +/** + * @method static static from(string $name, int $age, Team $team, ReportCollection $reports, CarbonImmutable $lastLogin) + */ +readonly class User extends Bag { + public function __construct( + public string $name, + public int $age, + public Team $team, + #[Cast(CollectionOf::class, Report::class)] + public ReportCollection $reports, + public CarbonImmutable $lastLogin, + ) { + } +} +``` + +## Expected Usage + +Because the factory is based on the Bag class properties, you will typically create and customize the Bag value object, and **then** create +the factory and add docs. To do this, you would follow these steps: + +1. Create the Bag class (optionally, with a collection): + +```bash +php artisan make:bag MyBag --collection +``` + +2. Customize the Bag class + +3. Create the Bag Factory and docs: + +```bash +php artisan make:bag MyBag --factory --docs --update +``` + +If you have already created the factory, you must add the `--force-except-bag` option to overwrite it: + +```bash +php artisan make:bag MyBag --factory --docs --update --force-except-bag +``` + +> [!WARNING] +> If you use the `--force` option instead of `--force-except-bag` it will overwrite your customized Bag Value class, losing any customizations. + +## The `make:bag` Command + +The `make:bag` command has the following options: + +``` +Description: + Create a new Bag value class, with optional factory and collection. + +Usage: + make:bag [options] [--] + +Arguments: + name + +Options: + -F, --force Force overwriting all files + -E, --force-except-bag Force overwriting Factory/Collection files + -u, --update Update Bag class to add factory/collection + -f, --factory[=FACTORY] Create a Factory for the Bag [default: "interactive"] + -c, --collection[=COLLECTION] Create a Collection for the Bag [default: "interactive"] + -d, --docs Add Bag::from() docs to the Bag for IDE auto-complete + -N, --namespace[=NAMESPACE] Specify the namespace for the Bag + --pretend Dump the file contents instead of writing to disk + -h, --help Display help for the given command. When no command is given display help for the list command + -q, --quiet Do not output any message + -V, --version Display this application version + --ansi|--no-ansi Force (or disable --no-ansi) ANSI output + -n, --no-interaction Do not ask any interactive question + --env[=ENV] The environment the command should run under + -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug +``` diff --git a/docs/versions/1.x/laravel-controller-injection.md b/docs/versions/1.x/laravel-controller-injection.md new file mode 100644 index 0000000..bc4df56 --- /dev/null +++ b/docs/versions/1.x/laravel-controller-injection.md @@ -0,0 +1,42 @@ +# Laravel Controller Injection + +Bag can automatically inject Bag objects into your controllers using Laravel's automatic dependency injection. This can take +the place of using Laravel's form request validation _and_ accessing the input data. + +```php +use App\Values\MyValue; + +class MyController extends Controller { + public function store(MyValue $value) { + // $value is a validated MyValue object + } +} +``` + +## Automatic Validation + +When you type hint a `Bag` object in your controller method, Bag will automatically validate the request data and inject the `Bag` object into your controller method. + +## Manual Validation + +If you want to inject the `Bag` object without validation, you can add the `WithoutValidation` attribute to the property: + +```php +use App\Values\MyValue; +use Bag\Attributes\WithoutValidation; + +class MyController extends Controller { + public function store( + #[WithoutValidation] MyValue $value + ) { + $value = $value->append(extra: 'data')->valid(); + // $value is now a validated MyValue object + } +} +``` + +> [!TIP] +> The `Bag->valid()` method will throw a `ValidationException` if the Bag object is not valid by default, you can pass in `false` to return null instead. + +> [!CAUTION] +> The input values must still fulfill the requirements of the Bag constructor, all required properties must be present otherwise an exception will be thrown. diff --git a/docs/versions/1.x/laravel-eloquent-casting.md b/docs/versions/1.x/laravel-eloquent-casting.md new file mode 100644 index 0000000..3847005 --- /dev/null +++ b/docs/versions/1.x/laravel-eloquent-casting.md @@ -0,0 +1,78 @@ +# Eloquent Casting + +Bag supports Eloquent casting for both Bag objects and Collections. + +## Casting Bag Objects + +To cast a Bag object, use the Bag class name as the cast type in the `$casts` property of your Eloquent model: + +```php{7} +use Illuminate\Database\Eloquent\Model; +use App\Values\MyValue; + +class MyModel extends Model +{ + protected $casts = [ + 'my_value' => MyValue::class, + ]; +} +``` + +This will cast the `MyValue` object to a JSON string when saving it to the database and will cast it back to a `MyValue` object when retrieving it from the database. + +> [!WARNING] +> Bag will store _all_ properties, including hidden properties. + +## Casting Collections + +For Laravel 11+, you can cast to a Collection using the `casts()` method along with the `::castAsCollection()` method on your Bag class: + +```php{6,8} +use Illuminate\Database\Eloquent\Model; +use App\Values\MyValue; + +class MyModel extends Model +{ + public function casts(): array { + return [ + 'my_values' => MyValue::castAsCollection(), + ]; + } +} +``` + +or the `AsBagCollection::of()` method: + +```php{7,9} +use Illuminate\Database\Eloquent\Model; +use App\Values\MyValue; +use Bag\AsBagCollection; + +class MyModel extends Model +{ + public function casts(): array { + return [ + 'my_values' => AsBagCollection::of(MyValue::class), + ]; + } +} +``` + +Alternatively, for Laravel 10, you can use the `AsBagCollection` class as the cast type in the `$casts` property of your Eloquent model, passing in the Bag class name as the first argument: + +```php{8} +use Illuminate\Database\Eloquent\Model; +use App\Values\MyValue; +use Bag\AsBagCollection; + +class MyModel extends Model +{ + protected $casts = [ + 'my_values' => AsBagCollection::class . ':' . MyValue::class, + ]; +} +``` + +Bag will cast the Collection to a JSON string when saving to the database and will cast them back to a Collection of `MyValue` objects when retrieving them from the database. + +Bag will automatically use the custom collection class if one is defined on the Bag class when retrieving the value. diff --git a/docs/versions/1.x/laravel-route-parameter-binding.md b/docs/versions/1.x/laravel-route-parameter-binding.md new file mode 100644 index 0000000..46e7689 --- /dev/null +++ b/docs/versions/1.x/laravel-route-parameter-binding.md @@ -0,0 +1,111 @@ +# Laravel Route Parameter Binding + +Bag can automatically populate properties from Laravel Route Parameters. This can be useful when you have both a +Bag representing your request body, and a route parameter that you want to associate with the Bag. + +## Binding Route Parameters + +To bind a route parameter, use the `Bag\Attributes\Laravel\FromRouteParameter` attribute on the property you want to bind. + +For example given the following controller: + +```php +use App\Values\MyValue; + +class MyController { + public function show(MyValue $bag, string $id) { + // $bag is automatically injected and validated + // $id is a route parameter + } +} +``` + +You can create a Bag like this that will automatically populate the `id` property based on the route parameter as injected above: + +```php{6} +use Bag\Attributes\Laravel\FromRouteParameter; +use Bag\Bag; + +class MyValue extends Bag +{ + #[FromRouteParameter()] + public string $id; +} +``` + +This will automatically populate the `id` property from the route parameter with the same name as the property. + +You can also use a different property name just by passing it to the attribute: + +```php{6} +use Bag\Attributes\Laravel\FromRouteParameter; +use Bag\Bag; + +class MyValue extends Bag +{ + #[FromRouteParameter('id')] + public string $valueId; +} +``` + +This will automatically popualte the `valueId` property from the route parameter with the name `id`. + +## Binding Route Parameter Properties + +In addition to binding the entire route parameter value to a single property, you can also bind a property or array key +from that value to your Bag using the `Bag\Attributes\Laravel\FromRouteParameterProperty` attribute. + +For example, given the following controller: + +```php +use App\Models\User; +use App\Values\MyValue; + +class MyController { + public function show(MyValue $bag, User $user) { + // $bag is automatically injected and validated + // $user is a route parameter + } +} +``` + +We can map the `id` property from the `User` model to a property on the Bag like this: + +```php{6} +use Bag\Attributes\Laravel\FromRouteParameter; +use Bag\Bag; + +class MyValue extends Bag +{ + #[FromRouteParameterProperty('user')] + public string $id; +} +``` + +This will automatically populate the `id` property with the `id` property from the `user` route parameter. +Alternatively, you can specify a source property name: + +```php{6} +use Bag\Attributes\Laravel\FromRouteParameter; +use Bag\Bag; + +class MyValue extends Bag +{ + #[FromRouteParameterProperty('user', 'id')] + public string $userId; +} +``` + +This will automatically populate the property `userId` with the `id` property from the `user` route parameter. + + +## Types & Casting + +The value from the route parameter will be cast by Laravel to the type specified in the controller method, +and will then be cast to the type of the property in the Bag if necessary. + +For example, if the route parameter method argument is an integer, and the property is a string, the integer will be cast to a string. +However, if the route parameter is an eloquent model and the property is also the same eloquent model, the value will not be cast. + +> [!TIP] +> This works great for route model binding! diff --git a/docs/versions/1.x/mapping.md b/docs/versions/1.x/mapping.md new file mode 100644 index 0000000..edcfa1a --- /dev/null +++ b/docs/versions/1.x/mapping.md @@ -0,0 +1,191 @@ +# Mapping + +Bag allows you to map both input names to properties, and properties to output names. This is useful when +transforming JSON `snake_case` to your codes `camelCase` and vice-versa. + +## How Mapping Works + +Mapping should be thought of as aliasing property names. The input mapper determines what the _incoming_ aliases _could_ be by transforming the original property name, while the output mapper transform the property name and uses it for the _outgoing_ property name. This means +that if you want to map a `propertyName` from the input `property_name` you would use a `SnakeCase` mapper, rather than a `camelCase` mapper. + +## Mapping Names + +To map names use the `Bag\Attributes\MapName`, `Bag\Attributes\MapInputName`, or `Bag\Attributes\MapOutputName` attributes. These attributes can either be applied to the entire class, **or** to an individual property. + +### Class-level Mapping + +Class-level mapping applies to all properties on the class. This is useful when all properties on a class should be mapped in the same way. + +This is done by applying the mapper attribute to the class: + +```php{5,6} +use Bag\Bag; +use Bag\Attributes\MapName; +use Bag\Mappers\SnakeCase; + +#[MapInputName(SnakeCase::class) +#[MapOutputName(SnakeCase::class)] +class MyValue extends Bag { + public function __construct( + public string $myValue, + public string $myOtherValue + ) {} +} +``` + +> [!NOTE] +> The above is functionally equivelent to using: +> ```php +> #[MapName(input: SnakeCase::class, output: SnakeCase::class)] +> ``` +> +> We recommend using the `MapInputName` and `MapOutputName` attributes as mapper arguments can be passed +> directly, rather than as an array of values to either the `inputParams` and/or `outputParams` arguments: +> +> ```php +> #[MapInputName(MapperName::class, 'param1', 'param2')] +> #[MapOutputName(MapperName::class, 'param1', 'param2')] +> +> // vs +> +> #[MapName(input: MapperName::class, inputParams: ['param1', 'param2'], output: MapperName::class, outputParams: ['param1', 'param2'])] +> ``` + +In this above example, the `MyValue` class will map _all_ property names from `snake_case` to `camelCase` on both input and output, in this case, `my_value` to `myValue` and `my_other_value` to `myOtherValue`: + +```php +$value = MyValue::from([ + 'my_value' => 'value', + 'my_other_value' => 'other value' +]); +``` + +In addition to the mapped names, you can still use the original property names: + +```php +$value = MyValue::from([ + 'myValue' => 'value', + 'myOtherValue' => 'other value' +]); +``` + +> [!TIP] +> You can specify a mapper on either the class or property level, or both. If you specify a mapper on both the class and property level, the property-level mapper will take precedence. + +## Built-in Mappers + +Bag comes with a few built-in mappers: + +- `SnakeCase` - Converts property names to/from `snake_case` +- `CamelCase` - Converts property names to/from `camelCase` +- `Alias` - Allows you to specify a custom alias for a specific property name +- `Stringable` - Converts property names using a sequence of [fluent string helper methods](https://laravel.com/docs/11.x/strings#fluent-strings-method-list). + +### Using the Alias Mapper + +The `Alias` mapper allows you to specify a custom alias for a specific property name. + +> [!WARNING] +> Unlike other mappers, the `Alias` mapper **must** only be applied to individual properties. + +In the following example we alias the input name `uuid` to the property `id`: + +```php{7} +use Bag\Bag; +use Bag\Attributes\MapInputName; +use Bag\Mappers\Alias; + +class MyValue extends Bag { + public function __construct( + #[MapInputName(Alias::class, 'uuid')])] + public string $id, + ) {} +} +```` + +### Using the Stringable Mapper + +The `Stringable` mapper allows you to chain any of the [fluent string helper methods](https://laravel.com/docs/11.x/strings#fluent-strings-method-list) to convert property names. + +The `Stringable` mapper accepts any number of transformations. To pass in arguments to a given transformation, use a colon `:` followed by a comma-separated list of arguments. + +```php{4,5} +use Bag\Bag; +use Bag\Mappers\Stringable; + +#[MapInputName(Stringable::class, 'camel', 'kebab')] +#[MapOutputName(Stringable::class, 'camel', 'kebab')] +class MyValue extends Bag { + public function __construct( + public string $myValue, + public string $myOtherValue + ) {} +} +``` + +## When Mapping Applies + +Input mapping is applied when calling `Bag::from()` or `Bag::withoutValidation()`. You can use either the original property name _or_ the mapped name when creating a Bag. + +> [!TIP] +> [Validation](validation) is applied to the original property name, not the mapped name. + +Output mapping is applied when calling `$Bag->toArray()`, `$Bag->toCollection()`, or `$Bag->toJson()` (or when using `json_encode()`). + +## Mapping Hierarchy + +For input mapping, _all_ mappers are used, allowing _multiple_ mapped names to match to the same property. **The last _incoming_ +property name that matches will be the value used for the Bag.** + +The mapping hierarchy is as follows: + +- Class Level: `MapName(input)` + - Class Level: `MapInputName` + - Property Level: `MapName(input)` + - Property Level: `MapInputName` + +For output mapping, _only_ the last mapper is used. The mapping hierarchy is as follows: + +- Class Level: `MapName(output)` + - Class Level: `MapOutputName` + - PropertyLevel Level: `MapName(Output)` + - PropertyLevel Level: `MapOutputName` + +> [!NOTE] +> The `MapName` and `MapOutputName` attributes can only be added once at each level. You can add as many `MapInputName` attributes as you like! + +## Custom Mappers + +You can also create your own mappers by implementing the `\Bag\Mappers\MapperInterface` interface. + +```php +use Bag\Mappers\MapperInterface; +use Illuminate\Support\Str; + +class Kebab implements MapperInterface { + public function input(string $name): string { + return Str::of($name)->camel()->kebab(); + } + + public function output(string $name): string { + return Str::of($name)->camel()->kebab(); + } +} +``` + +Then specify it in the Mapping attribute: + +```php{5,6} +use \App\Values\Mappers\Kebab; +use Bag\Bag; +use Bag\Mappers\Stringable; + +#[MapInputName(Kebab::class)] +#[MapOutputName(Kebab::class)] +class MyValue extends Bag { + public function __construct( + public string $myValue, + public string $myOtherValue + ) {} +} +``` diff --git a/docs/versions/1.x/object-to-bag.md b/docs/versions/1.x/object-to-bag.md new file mode 100644 index 0000000..a789a3c --- /dev/null +++ b/docs/versions/1.x/object-to-bag.md @@ -0,0 +1,55 @@ +# Creating Bags From Objects + +Bag provides an easy way to create Bags from objects with the `Bag\Traits\HasBag` trait. + +This trait provides a `->toBag()` method that converts the object into a Bag object using the Bag class +defined in the `Bag\Attributes\Bag` class attribute. + +## Adding `HasBag` To Your Class + +```php{5,7} +use App\Values\MyValue; +use Bag\Attributes\Bag; +use Bag\Traits\HasBag; + +#[Bag(MyValue::class)] +class MyClass { + use HasBag; + + // ... +} +``` + +Once you have made these changes to your class, you can easily create a Bag object from an instance of your class by calling the `->toBag()` method: + +```php +$myClass = new MyClass(); +$bag = $myClass->toBag(); +``` + +## Property Visibility + +By default, the `Bag\Traits\HasBag` trait will only include public properties in the Bag object. If you would like to include protected and private properties, you can pass the `visibility` argument to the `Bag\Attributes\Bag` attribute. + +The visibility property is a bitmask of the following values: + +- `Bag\Attributes\Bag::PUBLIC` - Include public properties +- `Bag\Attributes\Bag::PROTECTED` - Include protected properties +- `Bag\Attributes\Bag::PRIVATE` - Include private properties +- `Bag\Attributes\Bag::ALL` - Include all properties + +```php{5} +use App\Values\MyValue; +use Bag\Attributes\Bag; +use Bag\Traits\HasBag; + +#[Bag(MyValue::class, Bag::PUBLIC | Bag::PROTECTED)] +class MyClass { + use HasBag; + + // ... +} +``` + +> [!TIP] +> You can also specify the visibility when calling the `->toBag()` method e.g. `$myClass->toBag(Bag::ALL)` diff --git a/docs/versions/1.x/output.md b/docs/versions/1.x/output.md new file mode 100644 index 0000000..034da54 --- /dev/null +++ b/docs/versions/1.x/output.md @@ -0,0 +1,30 @@ +# Output + +Once you have created a Bag value object, you can access the properties using object notation: + +```php +use App\Values\MyValue; + +$value = new MyValue(name: 'Davey Shafik', age: 40); +$value->name; // 'Davey Shafik' +``` + +## Casting to Other Types + +Bag supports casting to arrays and [`\Bag\Collections`][./collections##extending-laravel-collections] using the +`Bag->toArray()` and `Bag->toCollection()` methods. + +Both methods will apply casting and mapping, as well as respect [hidden properties][./hidden]. + +## JSON Serialization + +In addition, you can serialize a Bag object to JSON using `json_encode()` or `Bag->toJson(): + +```php +$value = MyValue::from(name: 'Davey Shafik', age: 40); + +$value->toJson(); // {"name": "Davey Shafik", "age": 40} +json_encode($value, JSON_THROW_ON_ERROR); // {"name": "Davey Shafik", "age": 40} +``` + +Both `Bag->toJson()` and `json_encode()` will respect [hidden properties][./hidden]. diff --git a/docs/roadmap.md b/docs/versions/1.x/roadmap.md similarity index 100% rename from docs/roadmap.md rename to docs/versions/1.x/roadmap.md diff --git a/docs/versions/1.x/testing.md b/docs/versions/1.x/testing.md new file mode 100644 index 0000000..42323bc --- /dev/null +++ b/docs/versions/1.x/testing.md @@ -0,0 +1,157 @@ +# Testing + +Bag supports Factories to make creating test values easier. Bag factories are similar to [Eloquent factories](https://laravel.com/docs/11.x/eloquent-factories), but they are used to create Bag objects. + +## Creating a Factory + +Factories extend the `Bag\Factory` class, and define a `definition()` method that returns an array of default values for the value object. + +```php +use Bag\Factory; + +class MyValueFactory extends Factory { + #[Override] + public function definition(): array { + return [ + 'name' => 'Davey Shafik', + 'age' => 40, + ]; + } +} +``` + +### Faker Integration + +Factories include [Faker](https://fakerphp.org) support out of the box. You can use the `$faker` property to generate random values: + +```php +return [ + 'name' => $this->faker->name(), + 'age' => $this->faker->numberBetween(18, 65), +]; +``` + +> [!TIP] +> You can also generate factory classes automatically using the [`artisan make:bag`][./laravel-artisan-make-bag-command] command. + +## Using a Factory + +Before you can use a Factory, you must first add both the `Factory` attribute and the `HasFactory` trait to your Bag object: + +```php{5,7} +use Bag\Attributes\Factory; +use Bag\Bag; +use Bag\Traits\HasFactory; + +#[Factory(MyValueFactory::class)] +class MyValue extends Bag { + use HasFactory; + + public function __construct( + public string $name, + public int $age, + ) {} +} +``` + +You can now use the factory to create a new instance of the value object: + +```php +$bag = MyValue::factory()->make(); +``` + +This will create a new `MyValue` object using the factory definition. + +## Customizing Factory State + +You can also specify custom values when creating a factory, which will override the factory definition. You can pass the values to the `::factory()` call itself, +using the `->state()` method on the factory, or by passing it to the `->make()` method. + +```php +// All three are identical: + +$value = MyValue::factory([ + 'name' => 'Taylor Otwell', +])->make(); + +$value = MyValue::factory()->make([ + 'name' => 'Taylor Otwell', +]); + +$value = MyValue::factory()->state([ + 'name' => 'Taylor Otwell', +])->make(); +``` + +## Named States + +Bag supports named states, which allow you to modify the state of the value object when creating it: + +```php +use Bag\Factory; + +class MyValueFactory extends Factory { + public function definition(): array { + return [ + 'name' => 'Davey Shafik', + 'age' => 40, + ]; + } + + public function withName(string $name): static { + return $this->state([ + 'name' => $name, + ]); + } +} +``` + +You can now use the state when creating the value object: + +```php +$bag = MyValue::factory()->withName($faker->name())->make(); +``` + +## Creating Collections of Bag Values + +You can use the `->count()` method to create a collection of Bag objects: + +```php +$values = MyValue::factory()->count(10)->make(); +``` + +This will create a `Bag\Collection` of 10 identical `MyValue` objects. + +> [!TIP] +> If your Bag object has a `Collection` attribute, `->make()` will return an instance of that collection class. + +## Sequences + +Bag factories support Eloquent factory Sequences to generate unique values for each instance in a collection. + +```php +use Illuminate\Database\Eloquent\Factories\Sequence; + +$bag = MyValue::factory()->count(10)->sequence(fn(Sequence $sequence) => [ + 'name' => 'Person #' . $sequence->index, + 'age' => 18 + $sequence->index, +])->make(); +``` + +In this example, the `name` property will be set to `Person #1`, `Person #2`, etc., and the `age` property will be set to `19`, `20`, etc. + +The `->sequence()` method accepts any of the following: + +- A `Illuminate\Database\Eloquent\Factories\Sequence` instance created with a `closure` that returns an array of values +- A `Illuminate\Database\Eloquent\Factories\Sequence` instance created with a variadic number of arrays of values +- A `closure` value that returns an array of values +- A variadic number of arrays of values + +You may also pass a `Sequence` object to the `->state()` method. + +> [!TIP] +> If you create more values than number of value arrays passed in, the sequence will start over from the beginning. + +> [!WARNING] +> If you use both states (named or via the `::factory()`, `->state()`, or `->make()` methods) and sequences, sequences will be applied _after_ the state, so the sequences will override any values set by the state. + diff --git a/docs/versions/1.x/transformers.md b/docs/versions/1.x/transformers.md new file mode 100644 index 0000000..8f1ca6c --- /dev/null +++ b/docs/versions/1.x/transformers.md @@ -0,0 +1,102 @@ +# Transformers + +Transformers are helpers that transform Bag input data from custom types, e.g. Models or JSON strings. + +## Using Transformers + +Transformers are methods defined on your Bag class that transform input data into the correct type. Transformers are +identified by adding the `Transforms` attribute to the method, passing the type you want to transform from. + +For example, to transform from a JSON string: + +```php +use Bag\Bag; + +class MyValue extends Bag +{ + #[Transforms('string')] + protected static function fromJsonString(string $json): array + { + return json_decode($json, true, 512, JSON_THROW_ON_ERROR); + } +} +``` + +or to transform from a specific class: + +```php +use App\Models\User; +use Bag\Bag; + +class MyValue extends Bag +{ + #[Transforms(User::class)] + protected static function fromUser(User $user): array + { + return $user->toArray(); + } +} +``` + +You can pass multiple types to the `Transforms` attribute, or use multiple `Transforms` attributes to handle multiple types. + +The following two examples are functionally the same: + +```php +use App\Models\Book; +use App\Models\Magazine; +use Bag\Bag; + +class MyValue extends Bag +{ + #[Transforms(Book::class)] + #[Transforms(Magazine::class)] + protected static function fromMedia(Book|Magazine $media): array + { + return $media->toArray(); + } +} +``` + +and: + +```php +use App\Models\Book; +use App\Models\Magazine; +use Bag\Bag; + +class MyValue extends Bag +{ + #[Transforms(Book::class, Magazine::class)] + protected static function fromMedia(Book|Magazine $media): array + { + return $media->toArray(); + } +} +``` + +Bag will match child classes to their parents, so `Transforms(Model::class)` will match any child `Model` objects, however, +if there is a more specific transformer available, Bag will use that instead. + +> [!TIP] +> Bag will use the most specific transformer available, if two or more transformers are equally specific then Bag will use the first one it finds. + +### Handling JSON + +By default, Bag will transform JSON strings, but you can override this behavior by defining a overriding the `fromJsonString` method: + +```php +use Bag\Bag; + +class MyValue extends Bag +{ + #[Transforms(Bag::FROM_JSON)] + protected static function fromJsonString(string $json): array + { + return json_decode($json, true, 512, JSON_THROW_ON_ERROR); + } +} +``` + +To differentiate between other strings and JSON, you should use the special type `Bag::FROM_JSON` as the `Transformer` type. + diff --git a/docs/versions/1.x/validation.md b/docs/versions/1.x/validation.md new file mode 100644 index 0000000..87f8270 --- /dev/null +++ b/docs/versions/1.x/validation.md @@ -0,0 +1,106 @@ +# Validation + +Bag uses Laravel's validation system to validate input data. You can define validation rules using annotations, and/or using the `rules()` method. + +`Bag` will automatically validate input data when creating a new instance using the `Bag::from()` method. + +```php +use Bag\Attributes\Validation\Required; +use Bag\Bag; + +readonly class MyValue extends Bag +{ + public function __construct( + #[Required] + public string $name, + public int $age, + ) { + } + + public static function rules(): array + { + return [ + 'name' => ['string'], + 'age' => ['integer'], + ]; + } +} +``` + +In this example we added a `#[Required]` attribute to the `name` property, and defined validation rules in the `rules()` method. + +You can validate a Bag object before creating it using the `Bag::validate()` method: + +```php +$value = MyValue::validate([ + 'name' => 'Davey Shafik', + 'age' => 40, +]); +``` + +## Creating a Bag Without Validation + +If you want to create a Bag without automatically validating it, you can use the `Bag::withoutValidation()` method: + +```php +$value = MyValue::withoutValidation([ + 'name' => 'Davey Shafik', + 'age' => 40, +]); +``` + +To futher append properties to a Bag without validation, you can use the `Bag::append()` method: + +```php +$value = MyValue::withoutValidation([ + 'name' => 'Davey Shafik', +])->append([ + 'age' => 40, +]); +``` + +## Built-in Validation Attributes + +Bag provides a number of built-in validation attributes, based on various Laravel validation rules: + +| Rule | | Usage | +|-------------------------------------------------------------------------------|---------------------------------------------------------------|--------------------------------------------| +| [Between](https://laravel.com/docs/validation#rule-between) | The value should be between two values (inclusive) | `#[Between(1, 10)]` | +| [Boolean](https://laravel.com/docs/validation#rule-boolean) | The value should be a boolean | `#[Boolean]` | +| [Decimal](https://laravel.com/docs/validation#rule-decimal) | The value should be a decimal number | `#[Decimal]` | +| [Email](https://laravel.com/docs/validation#rule-email) | The value should be an email address | `#[Email]` | +| [Enum](https://laravel.com/docs/validation#rule-enum) | The value should be an enum case | `#[Enum(MyEnum::class)]` | +| [Exists](https://laravel.com/docs/validation#rule-exists) | The value must exist in a field in a table | `#[Exists('table', 'optionalColumn')]` | +| [In](https://laravel.com/docs/validation#rule-in) | The value should be in the given list | `#[In('foo', 'bar')]` | +| [Integer](https://laravel.com/docs/validation#rule-integer) | The value should be an integer | `#[Integer]` | +| [Max](https://laravel.com/docs/validation#rule-max) | The value should be at most a given size | `#[Max(100)]` | +| [Min](https://laravel.com/docs/validation#rule-min) | The value should be at minimum a given size | `#[Min(1)]` | +| [NotRegex](https://laravel.com/docs/validation#rule-not-regex) | The value should not match a given regex | `#[NotRegex('/regex/')]` | +| [Numeric](https://laravel.com/docs/validation#rule-numeric) | The value should be numeric | `#[Numeric]` | +| [Regex](https://laravel.com/docs/validation#rule-regex) | The value should match a given regex | `#[Regex('/regex/')]` | +| [Required](https://laravel.com/docs/validation#rule-required) | The value is required | `#[Required]` | +| [RequiredIf](https://laravel.com/docs/validation#rule-required-if) | The value is required if another field matches a value | `#[RequiredIf('otherField', 'value')]` | +| [RequiredUnless](https://laravel.com/docs/validation#rule-required-unless) | The value is required unless another field matches a value | `#[RequiredUnless('otherField', 'value')]` | +| [RequiredWith](https://laravel.com/docs/validation#rule-required-with) | The value is required if another field is present | `#[RequiredWith('otherField')]` | +| [RequiredWithAll](https://laravel.com/docs/validation#rule-required-with-all) | The value is required if more than one other field is present | `#[RequiredWithAll('field1', 'field2')]` | +| [Size](https://laravel.com/docs/validation#rule-size) | The value should have a specific size | `#[Size(10)]` | +| [Str](https://laravel.com/docs/validation#rule-string) | The value should be a string | `#[Str]` | +| [Unique](https://laravel.com/docs/validation#rule-unique) | The value must be unique for a field in a table | `#[Unique('table', 'optionalColumn')]` | + +In addition, a generic `\Bag\Attributes\Validation\Rule` attribute is available to apply any Laravel validation rule: + +```php +use Bag\Attributes\Validation\Rule; + +readonly class MyValue extends Bag +{ + public function __construct( + #[Required] + public string $username, + public string $password, + #[Rule('same:password')] + public string $passwordConfirmation, + ) { + } +} +``` diff --git a/docs/versions/1.x/variadics.md b/docs/versions/1.x/variadics.md new file mode 100644 index 0000000..b9f6d4f --- /dev/null +++ b/docs/versions/1.x/variadics.md @@ -0,0 +1,63 @@ +# Variadic Properties + +Bag supports the use of [Variadic](https://www.php.net/manual/en/functions.arguments.php#functions.variable-arg-list) arguments. + +Variadic arguments **must** be manually assigned to a property: + +```php +use Bag\Bag; + +class MyValue extends Bag { + public $values; + + public function __construct(mixed ...$values) { + $this->values = $values; + } +} +``` + +## Casting + +Bag will automatically cast variadic values to their defined typed: + +```php +use Bag\Bag; + +class MyValue extends Bag { + public $values; + + public function __construct(bool ...$values) { + $this->values = $values; + } +} +``` + +The above example will cast all values to `bool`: + +```php +$bag = new MyValue(true, false, 0, 1); + +// $bag->values = [true, false, false, true] +``` + +You can also use `Cast` attributes to cast the values: + +```php +use Bag\Bag; +use Bag\Casts\DateTime; +use Carbon\CarbonImmutable; + +class MyValue extends Bag { + public $values; + + public function __construct( + #[Cast(DateTime::class, format: 'Y-m-d')] + CarbonImmutable ...$values + ) { + $this->values = $values; + } +} +``` + +This will cast all input values to `CarbonImmutable` instances, using the `Y-m-d` format. + diff --git a/docs/versions/1.x/why.md b/docs/versions/1.x/why.md new file mode 100644 index 0000000..34833f1 --- /dev/null +++ b/docs/versions/1.x/why.md @@ -0,0 +1,65 @@ +# Why Choose Bag? + +Bag is a simple, lightweight, modern, and flexible library for working with value objects in PHP. + +It is designed to be easy to use, with a minimal API that is easy to understand and use. + +Bag focuses on immutability and type safety. + +## Compared to spatie/laravel-data + +Bag was heavily inspired by the excellent [spatie/laravel-data](https://spatie.be/docs/laravel-data/) package, and as such +should feel very familiar to anyone who has used it β€” _however_, it has several key differences. + +## Common Features + +- [Value Casting](casting) (both in and out, although spatie/laravel-data calls outbound casting Transforming) + - Including nested Bag objects +- [Name Mapping](mapping) (both in and out) at the class and property level +- [Validation](validation) (although spatie/laravel-data does not support all Laravel validation options easily) +- [Collections](collections) of Value Objects[*](#collections) +- [Object to Bag](object-to-bag) conversion +- [Wrapping](wrapping) of output arrays/JSON +- [Eloquent Casting](laravel-eloquent-casting) +- [Laravel Controller Injection](laravel-controller-injection) + +## Immutability + +Bag is immutable by default. + +spatie/laravel-data does not support immutable value objects, and as of PHP 8.3, there is no reasonable way to make them immutable. + +## Factory Support + +Bag [factories](testing) support most of the rich features and simple UX of Laravel Model Factories except for the `create()` method (as value objects do not feature persistence). +This includes support for [factory states](https://laravel.com/docs/11.x/eloquent-factories#factory-states) and [sequences](https://laravel.com/docs/11.x/eloquent-factories#sequences). + +spatie/laravel-data v3 does not support factories, while v4 has [rudimentary support](https://spatie.be/docs/laravel-data/v4/as-a-data-transfer-object/factories). + +## Variadic Support + +Bag supports the use of [Variadic](variadics) during value object creation. + +## Collections + +Bag uses Laravel Collections as the basis for its [Collection](collections) classes, and supports them wherever Collections are used, however `Bag\Collection` is an immutable-safe variant that we recommend +using whenever possible. + +spatie/laravel-data v3 uses a custom `DataCollection` class that is not based on Laravel collections and lacks many Collection features. v4 uses Laravel Collections, although it still +has [custom collection classes](https://spatie.be/docs/laravel-data/v4/as-a-data-transfer-object/collections) with varying levels of compatibility with Laravel Collections. + +## Hidden Properties + +Bag supports [hiding properties](hidden) when transforming to an array and/or JSON. + +## Artisan `make:bag` Command + +Bag supports an [artisan command](laravel-artisan-make-bag-command) for creating new Bag classes, including support for creating factories and collections. + +## Other Differences + +In addition to the above, Bag has a few other minor differences: + +- [Casters](casting) apply to both incoming values and outgoing values, while spatie/laravel-data splits these into two difference concepts. +- Input from complex values uses [Transformers](transformers), which are more explicit in Bag, while spatie/laravel-data uses a more implicit approach with magic methods e.g. `::fromModel()` +- Simpler attribute names: `Cast` vs `WithCast` and `WithTransformer` diff --git a/docs/versions/1.x/wrapping.md b/docs/versions/1.x/wrapping.md new file mode 100644 index 0000000..3eca5f3 --- /dev/null +++ b/docs/versions/1.x/wrapping.md @@ -0,0 +1,76 @@ +# Wrapping Outputs + +Bag supports wrapping both Bag values and Collections when transforming to an array or JSON. + +## Wrapping Bags + +To wrap a Bag, add the `Bag\Attributes\Wrap` or `Bag\Attributes\WrapJson` attribute to the class: + +```php{4} +use Bag\Attributes\Wrap; +use Bag\Bag; + +#[Wrap('data')] +class MyValue extends Bag { + public function __construct( + public string $name, + public int $age, + ) {} +} +``` + +Now whenever you call `->toArray()` or serialize to JSON, the output will be wrapped in a key of `data`: + +```php +$myValue = MyValue::from(['name' => 'Davey Shafik', 'age' => 40]); +$myValue->toArray(); // ['data' => ['name' => 'Davey Shafik', 'age' => 40]] +$myValue->toJson(); // {"data":{"name":"Davey Shafik","age":40}} +``` + +If you only want to wrap when serializing to JSON, you can use the `WrapJson` attribute instead: + +```php{4} +use Bag\Attributes\WrapJson; +use Bag\Bag; + +#[WrapJson('data')] +class MyValue extends Bag { + public function __construct( + public string $name, + public int $age, + ) {} +} +``` + +Now when you serialize to JSON, the values will be wrapped, but when calling `->toArray()` the output will not be wrapped + +```php +$myValue = MyValue::from(['name' => 'Davey Shafik', 'age' => 40]); +$myValue->toArray(); // ['name' => 'Davey Shafik', 'age' => 40] +$myValue->toJson(); // {"data":{"name":"Davey Shafik","age":40}} +``` + +> [!TIP] +> You can add both `Wrap` and `WrapJson` attributes to the same class to apply different wrapping to `->toArray()` and JSON serialization respectively. + +## Wrapping Collections + +Wrapping Collections works exactly the same way as with Bags: add the `Bag\Attributes\Wrap` or `Bag\Attributes\WrapJson` attribute to the Collection class: + +```php{4} +use Bag\Attributes\Wrap; +use Bag\Collection; + +#[Wrap('data')] +class MyCollection extends Collection { +} +``` + +Now when you call `->toArray()` or serialize to JSON, the output will be wrapped in a key of `data`: + +```php +$collection = MyValue::factory()->count(2)->make(); + +$collection->toArray(); // ['data' => [["name" => "Davey Shafik", "age" => 40], ...]] +$collection->toJson(); // {"data":[{"name":"Davey Shafik","age":40}, ...]} +``` diff --git a/docs/versions/2.0/basic-usage.md b/docs/versions/2.0/basic-usage.md new file mode 100644 index 0000000..1405e19 --- /dev/null +++ b/docs/versions/2.0/basic-usage.md @@ -0,0 +1,138 @@ +# Basic Usage + +## Creating a Value Object + +To create a new Value Object, extend the `Bag\Bag` class and define your properties in the constructor: + +```php +use Bag\Bag; + +readonly class MyValue extends Bag { + public function __construct( + public string $name, + public int $age, + ) { + } +} +``` + +> [!TIP] +> You can add an `@method` annotation to your class to provide auto-complete for the `::from()` method, or use the [Artisan Command with the --doc option](laravel-artisan-make-bag-command#updating-documentation) to generate it for you. + + +## Instantiating a Value Object + +To create a new instance of your Value Object, call the `::from()` method. You can use an array, a Collection, named arguments, or positional arguments. + + +### Named Arguments + +```php +$value = MyValue::from( + name: 'Davey Shafik', + age: => 40, +); +``` + +### Array or Collection of Arguments + +```php +$value = MyValue::from([ + 'name' => 'Davey Shafik', + 'age' => 40, +]); +``` + +or: + +```php +$value = MyValue::from(collect([ + 'name' => 'Davey Shafik', + 'age' => 40, +])); +``` + +### Positional Arguments + +```php +$value = MyValue::from('Davey Shafik', 40); +``` + +> [!WARNING] +> If you use positional arguments, you must ensure they are in the same order as the constructor. + +> [!WARNING] +> If you have a single array argument, **and** an array [transformer](transformers), the transformer will be applied to the array, potentially causing unwanted side-effects. + +## Type Casting + +If the input value matches the type of the property (including [union types](https://www.php.net/manual/en/language.types.type-system.php#language.types.type-system.composite.union)), it will be used as-is. Otherwise, Bag will cast all values to their defined type _automatically_ for all scalar types, as well as the following: + +- `Bag` objects +- `\Bag\Collection` and `\Illuminate\Support\Collection` objects +- `\DateTimeInterface` objects will be cast using standard [PHP Date/Time formats](https://www.php.net/manual/en/datetime.formats.php) + - This includes `\DateTime`, `\DateTimeImmutable`, `\Carbon\Carbon` and `\Carbon\CarbonImmutable` +- Enums + +> [!TIP] +> We recommend using `\Carbon\CarbonImmutable` for all date times. + +## Default Values + +You can define default values for your properties by setting them in the constructor: + +```php +use Bag\Bag; + +readonly class MyValue extends Bag { + public function __construct( + public string $name, + public int $age = 40, + ) { + } +} + +$value = MyValue::from([ + 'name' => 'Davey Shafik', +])->toArray(); // ['name' => 'Davey Shafik', 'age' => 40] +``` + +## Nullable Values + +Bag will fill missing nullable values without a default value with `null`: + +```php +use Bag\Bag; + +readonly class MyValue extends Bag { + public function __construct( + public string $name, + public ?int $age, + ) { + } +} + +$value = MyValue::from([ + 'name' => 'Davey Shafik', +])->toArray(); // ['name' => 'Davey Shafik', 'age' => null] +``` + +### Modifying a Value Object + +Value Objects are immutable, so you cannot change their properties directly. Instead, you can create a new instance with the updated values using the `Bag->with()` or `Bag->append()` methods: + +```php +$value = MyValue::from([ + 'name' => 'Davey Shafik', + 'age' => 40, +]); + +$newValue = $value->with(age: 41); + +dump($newValue->toArray()); // ['name' => 'Davey Shafik', 'age' => 41] +``` + +You can pass either named arguments, or an array or Collection of key-value pairs to the `Bag->with()` method. + +> [!TIP] +> The `Bag->append()` method works the same way as `Bag->with()`, but it will not validate the new value object. You can manually validate the object using `Bag->valid()`. diff --git a/docs/versions/2.0/casters.md b/docs/versions/2.0/casters.md new file mode 100644 index 0000000..8eb93bf --- /dev/null +++ b/docs/versions/2.0/casters.md @@ -0,0 +1,134 @@ +# Built-in Casters + +The following built-in explicit casters are available: + +## Bag Collections + +`\Bag\Casts\CollectionOf` casts an array to a [Collection][./collections] of Bag objects. + +```php +use Bag\Bag; +use Bag\Attributes\Cast; +use Bag\Casts\CollectionOf; +use Bag\Collection; + +class MyValue extends Bag { + public function __construct( + #[Cast(CollectionOf::class, MyOtherValue::class)] + public Collection $values, + ) { + } +} + +$value = MyValue::from([ + 'values' => [ + ['name' => 'Davey Shafik', 'age' => 40], + … + ], +]); + +dump($value->values); // Collection +``` + +> [!TIP] +> The `CollectionOf` caster will use the appropriate Collection class based on the Bag class provided. + +## Dates & Time + +`\Bag\Casts\DateTime` casts a date/time string to an object that implements the `\DateTimeInterface` interface. + +```php +use Bag\Bag; +use Bag\Attributes\Cast; +use Bag\Casts\DateTime; +use Carbon\CarbonImmutable; + +class MyValue extends Bag { + public function __construct( + #[Cast( + DateTime::class, + format: 'u', + outputFormat: + 'Y-m-d', strictMode: true + )] + public CarbonImmutable $dateOfBirth, + ) { + } +} + +$value = MyValue::from([ + // Requires a UNIX timestamp input due to strictMode: true + 'dateOfBirth' => '454809600', +]); + +$value->dateOfBirth; // CarbonImmutable{date: 1984-05-31 00:00:00 UTC} +$value->toArray(); // ['dateOfBirth' => '1984-05-31'] due to outputFormat +``` + +The `DateTime` cast accepts the following parameters: + +- `format` - The format of the input and output date/time string (see [PHP date/time format](https://www.php.net/manual/en/datetimeimmutable.createfromformat.php#datetimeimmutable.createfromformat.parameters)) +- `outputFormat` - The format of the output date/time string, this will override the `format` parameter for output only +- `strictMode` - If `true`, the input date/time string must match the `format` parameter exactly, otherwise it is parsed as best as possible +- `dateTimeClass` - The class to use for the date/time object, must implement `\DateTimeInterface`, this must be compatible with the property type hint, and defaults to the type hint value + +> [!TIP] +> We recommend using `CarbonImmutable` as type for all date/time casting. + +## Money + +`\Bag\Casts\MoneyFromMinor` Casts a number to a `\Brick\Money\Money` object assuming minor units (e.g. Cents) + +```php +use Bag\Bag; +use Bag\Attributes\Cast; +use Bag\Casts\MoneyFromMinor; +use Brick\Money\Money; + +class MyValue extends Bag { + public function __construct( + #[Cast(MoneyFromMinor::class, currency: 'USD')] + public Money $amount, + ) { + } +} + +$value = MyValue::from([ + 'amount' => 1000, +]); + +dump($value->amount); // Money object with a value of 10.00 USD +``` + +`\Bag\Casts\MoneyFromMajor` Casts a number to a `\Brick\Money\Money` object assuming major units (e.g. Dollars) + +```php +use Bag\Bag; +use Bag\Attributes\Cast; +use Bag\Casts\MoneyFromMajor; +use Brick\Money\Money; + +class MyValue extends Bag { + public function __construct( + #[Cast(MoneyFromMajor::class, currency: 'USD')] + public Money $amount, + ) { + } +} + +$value = MyValue::from([ + 'amount' => 1000, +]); + +dump($value->amount); // Money object with a value of 1,000.00 USD +``` + +Both `MoneyFromMajor` and `MoneyFromMinor` accept the following parameters: + +- `currency` - The currency code to use for the `\Brick\Money\Money` object, either a 3-letter ISO 4217 code or a [`\PrinsFrank\Standards\Currency\CurrencyAlpha3`](https://github.com/PrinsFrank/standards/blob/main/src/Currency/CurrencyAlpha3.php#L22) enum case. +- `currencyProperty` - The input parameter to use for the currency code +- `locale` - The locale to use for formatting the money object, defaults to `en_US` + +> [!TIP] +> Best practices recommend that you always handle money as strings of minor units, _however_ if you are working with user input it's most likely in major units. +> We recommend using `MoneyFromMajor` as the input caster and `MoneyFromMinor` as the output caster for handling user input. diff --git a/docs/versions/2.0/casting.md b/docs/versions/2.0/casting.md new file mode 100644 index 0000000..bda5afe --- /dev/null +++ b/docs/versions/2.0/casting.md @@ -0,0 +1,107 @@ +# Casting Values + +Casting allows you to format input and output values appropriately. + +## Explicit Casting + +You can explicitly cast input and/or output values to a specific type using annotations. This allows you to cast simple values to complex values, +or simply transform it in some way. For example, you can cast a number to a `\Brick\Money\Money` object or a timestamp to a `\Carbon\CarbonImmutable` object. + +Casts can act on both input and output, depending on if they implement `CastsPropertySet` or `CastsPropertyGet`. + +Output casts are applied when calling `toArray()`, `toCollection()`, or when serializing to JSON. + +The following annotations are available: + +- `Bag\Attributes\Cast` - Cast both input and output depending on whether it implements `CastsPropertySet` and/or `CastsPropertyGet` +- `Bag\Attributes\CastInput` β€” Only cast input (requires the caster implements `CastsPropertySet`) +- `Bag\Attributes\CastOutput` β€” Only cast output (requires the caster implements `CastsPropertyGet`) + +```php +use Bag\Bag; +use Bag\Attributes\Cast; +use Bag\Casts\DateTime; +use Carbon\CarbonImmutable; + +readonly class MyValue extends Bag { + public function __construct( + public string $name, + public int $age, + #[Cast(DateTime::class, format: 'Y-m-d')] + public CarbonImmutable $dateOfBirth, + ) { + } +} +``` + +This will cast the `dateOfBirth` property to a `\Carbon\CarbonImmutable` object using the `Y-m-d` format: + +```php +$value = MyValue::from([ + 'name' => 'Davey Shafik', + 'age' => 40, + 'dateOfBirth' => '1984-05-31', +]); +``` + +When you access the `dateOfBirth` property directly, it will be a `\Carbon\CarbonImmutable` object: + +```php +dump($value->dateOfBirth); // CarbonImmutable +``` + +However, if you call `toArray()` or `toCollection()` then it will be formatted using the format you specified: + +```php +dump($value->toArray()); // ['name' => 'Davey Shafik', 'age' => 40, 'dateOfBirth' => '1984-05-31'] +``` + +### Available Casts + +Bag supports several [built-in casters][./casters], and you can create your own by implementing `CastsPropertySet` and/or `CastsPropertyGet`. + + +## Automatic Casting + +If no cast attribute is provided, input values are _automatically_ cast to the correct type based on the type hint of the property when possible. + +The following types are automatically cast: + +- Scalar types (i.e. `int`, `float`, `bool`, `string`) +- `\CarbonImmutable`, `\Carbon`, `\DateTimeImmutable`, and `\DateTime` objects (parsing the value using [PHP date/time formats](https://www.php.net/manual/en/datetime.formats.php)) +- `Bag` objects from arrays +- Laravel Collections (i.e. `collect($value)`) +- BackedEnums from value (i.e. `public FooEnum $foo` will cast the value `BAR` using `FooEnum::from('BAR')`) +- UnitEnums by name (i.e. `public FooEnum $foo` will cast the value `BAR` to `FooEnum::BAR`) +- Laravel Models from IDs (using `Model::findOrFail($value)`) + +All other types will be set to the input value (which may be invalid). Input objects that match the type hint are not re-cast. + +For example, given the following Bag class: + +```php +use Bag\Bag; +use Carbon\CarbonImmutable; + +class MyValue extends Bag { + public function __construct( + public CarbonImmutable $dateOfBirth, + ) { + } +} +``` + +When you create a new instance of `MyValue`, the `dateOfBirth` property will be a `\Carbon\CarbonImmutable` object: + +```php +MyValue::from([ + 'dateOfBirth' => '1984-05-31', +]); +``` + +However, if you pass in a value like `12:34:56`, it will result in a `\Carbon\CarbonImmutable` object with the current date and the time set to `12:34:56`. + +> [!WARNING] +> To avoid this issue, we recommend always using a `DateTime` cast as shown above, setting the `strictMode` parameter to `true`. This allows you to specify a required input format. + +When you access the `dateOfBirth` property directly, it will be the `\Carbon\CarbonImmutable` object. diff --git a/docs/versions/2.0/collections.md b/docs/versions/2.0/collections.md new file mode 100644 index 0000000..a862cdb --- /dev/null +++ b/docs/versions/2.0/collections.md @@ -0,0 +1,73 @@ +# Collections + +## Using Collections + +You can create a collection of Value objects using the `Bag::collect()` method: + +```php +$values = MyValue::collect([ + [ + 'name' => 'Davey Shafik', + 'age' => 40, + ], + [ + 'name' => 'Taylor Otwell', + 'age' => 40, + ], +]); +``` + +This will create a `\Bag\Collection` of `MyValue` objects. + +### Extending Laravel Collections + +`Bag\Collection` extends `\Illuminate\Support\Collection` and has [most of the same methods](https://laravel.com/docs/11.x/collections#available-methods). + +The following methods will throw an exception if used: + +- `->pull()` +- `->shift()` +- `->splice()` +- `->transform()` +- `->getOrPut()` +- `offsetSet()` (used with array access) +- `offsetUnset()` (used with array access) +- `__set()` (used when setting arbitrary properties) + +In addition, the following will create a new `Bag\Collection` instance instead of modifying the original: + +- `->forget()` +- `->pop()` +- `->prepend()` +- `->push()` +- `->put()` + +## Custom Collections + +If you want to use a custom collection class, you must first create a new class that extends `\Bag\Collection`: + +```php +use Bag\Collection; + +class MyValueCollection extends Collection { +} +``` + +Then you can use this collection class in your Value object by adding the `Collection` attribute to your Bag class: + +```php +use App\Values\Collections\MyValueCollection; +use Bag\Bag; +use Bag\Attributes\Collection; + +#[Collection(MyValueCollection::class) +readonly class MyValue extends Bag { + public function __construct( + public string $name, + public int $age, + ) { + } +} +``` + +When using `MyValue::collect()` a `MyValueCollection` object will be returned. diff --git a/docs/versions/2.0/computed-properties.md b/docs/versions/2.0/computed-properties.md new file mode 100644 index 0000000..6a15641 --- /dev/null +++ b/docs/versions/2.0/computed-properties.md @@ -0,0 +1,28 @@ +# Computed Properties + +Bag supports computed properties, these are properties that are derived at creation time rather than passed in. + +Failing to set a computed property will result in a `Bag\Exceptions\ComputedPropertyMissing` exception. + +## Using Computed Properties + +To use computed properties, define the property in your class, and add the `Bag\Attributes\Computed` attributed: + +```php{7,11} +use Bag\Bag; +use Bag\Attributes\Computed; +use Carbon\CarbonImmutable; + +class MyValue extends Bag +{ + #[Computed] + public string $computedProperty; + + public function __construct() { + $this->computedProperty = CarbonImmutable::now(); + } +} +``` + +> [!WARNING] +> You **must** set the property within the constructor, otherwise an exception will be thrown. diff --git a/docs/versions/2.0/hidden.md b/docs/versions/2.0/hidden.md new file mode 100644 index 0000000..4ccb8b3 --- /dev/null +++ b/docs/versions/2.0/hidden.md @@ -0,0 +1,79 @@ +# Hidden Properties + +Bag supports hiding properties when transforming to an array and/or JSON. +This is useful when you want to keep certain properties private, or when you want to hide sensitive information. + +Hidden properties are still accessible as object properties (e.g. `$bag->hiddenProperty`), but they will not be included +in the output when transforming to an array and/or JSON. + +## Hiding Properties + +Properties can be hidden by applying the `Hidden` attribute to the property: + +```php +use Bag\Attributes\Hidden; +use Bag\Bag; + +class MyValue extends Bag { + public function __construct( + #[Hidden] + public string $hiddenProperty + public string $visibleProperty + ) {} +} +``` + +In the above example, the `hiddenProperty` will not be included when calling `toArray()`, `toCollection()` or `json_encode()`: + +```php +$value = MyValue::from([ + 'hiddenProperty' => 'hidden', + 'visibleProperty' => 'visible' +]); + +$value->toArray(); // ['visibleProperty' => 'visible'] +json_encode($value, JSON_THROW_ON_ERROR); // {"visibleProperty": "visible"} +``` + +Additionally, you can use PHP's built-in `SensitiveParameter` attribute to hide sensitive information: + +```php +use Bag\Bag; +use SensitiveParameter; + +class MyValue extends Bag { + public function __construct( + #[SensitiveParameter] + public string $password + ) {} +} +``` + +## Hiding Properties from JSON + +In addition to always hiding properties, you can choose to _only_ hide them when serializing to JSON by using the `HiddenFromJson` attribute: + +```php +use Bag\Attributes\HiddenFromJson; +use Bag\Bag; + +class MyValue extends Bag { + public function __construct( + #[HiddenFromJson] + public string $hiddenProperty + public string $visibleProperty + ) {} +} +``` + +In the above example, the `hiddenProperty` will be included when calling `json_encode()`, but not when calling `toArray()`: + +```php +$value = MyValue::from([ + 'hiddenProperty' => 'hidden', + 'visibleProperty' => 'visible' +]); + +$value->toArray(); // ['hiddenProperty' => 'hidden', visibleProperty' => 'visible'] +json_encode($value, JSON_THROW_ON_ERROR); // {"visibleProperty": "visible"} +``` diff --git a/docs/versions/2.0/how-bag-works.md b/docs/versions/2.0/how-bag-works.md new file mode 100644 index 0000000..4fab4da --- /dev/null +++ b/docs/versions/2.0/how-bag-works.md @@ -0,0 +1,219 @@ +# How Bag Works + +Bag works by utilizing pipelines to process the input and output data. + +There are four pipelines: + +- `InputPipeline` for handling input and constructing the Bag object +- `ValidationPipeline` for validating without constructing the Bag object +- `OutputPipeline` for handling output +- `OutputCollectionPipeline` for handling collection output + +Each pipeline is detailed below. + +> [!TIP] +> Each stage in the diagrams below is linked to the relevant source code for that stage. + +## The Input Pipeline + +The [`InputPipeline`](https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/InputPipeline.php) is responsible for processing the input data so that the `Bag` object can be created. The pipeline +consists of the following steps: + +```mermaid +graph TD; +start("Bag::from($data)") +--> transform(Transform Input) +--> process(Process Parameters) +--> variadic(Is Variadic?) +--> fillNulls(Fill Nulls) +--> mapInput(Map Input) +--> laravelParams(Laravel Route Parameter Binding) +-- Finalized Input Values --> missing(Missing Parameters) --> missingError{Error?} +missingError -- Yes --> errorMissingParameters(MissingPropertiesException) +missingError -- No --> extra(Extra Parameters) --> extraError{Error?} +extraError -- Yes --> errorExtraParameters(ExtraPropertiesException) +extraError -- No --> validate(Validate) +--> valid{Valid?} +valid -- No --> errorValidation(ValidationException) +valid -- Yes --> cast(Cast Input) +--> construct("new Bag(...)") +--> computed(Verify Computed Values) +--> initialized{Initialized?} +initialized -- No --> errorInitialization(ComputedPropertyUninitializedException) +initialized -- Yes +--> bag(Bag Value Returned) + +class start mermaid-start +class missingError,extraError,valid,initialized mermaid-decision +class bag mermaid-end +class errorMissingParameters,errorExtraParameters,errorValidation,errorInitialization mermaid-error + +click start "https://github.com/dshafik/bag/blob/main/src/Bag/Bag.php" _blank +click transform "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/Transform.php" _blank +click process "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ProcessParameters.php" _blank +click variadic "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/IsVariadic.php" _blank +click fillNull "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/FillNulls.php" _blank +click mapInput "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/MapInput.php" _blank +click laravelParams "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/LaravelRouteParameters.php" _blank +click missing "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/MissingParameters.php" _blank +click extra "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ExtraParameters.php" _blank +click validate "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/Validate.php" _blank +click cast "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/CastInputValues.php" _blank +click construct "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/FillBag.php" _blank +click computed "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ComputedValues.php" _blank +``` + +## The Output Pipeline + +The `OutputPipeline` is responsible transforming the Bag data to the desired output array or JSON. The pipeline consists of the following steps: + +```mermaid +graph TD; +toArray("Bag->toArray()") --> processParameters +toCollection("Bag->toCollection()") --> processParameters +toJson("Bag->toJson()") --> processParameters +jsonEncode("json_encode($bag)") --> processParameters +get("Bag->get()") --> processParameters +unwrapped("Bag->unwrapped()") --> processParameters +processParameters(Process Parameters) +--> processProperties(Process Properties) +--> getValues(Get Values) +--> hide("Remove Hidden Attributes*") +--> hideJson("Remove Hidden JSON Attributes*") +--> cast(Cast Output) +--> mapOutput(Map Output) +--> wrap(Wrap Output*) +--> output(array or JSON string) + +class toArray,toCollection,toJson,jsonEncode,get,unwrapped mermaid-start +class output mermaid-end +class hide,hideJson,wrap mermaid-conditional + +click toArray "https://github.com/dshafik/bag/blob/main/src/Bag/Concerns/WithArrayable.php" _blank +click toCollection "https://github.com/dshafik/bag/blob/main/src/Bag/Concerns/WithCollections.php" _blank +click toJson "https://github.com/dshafik/bag/blob/main/src/Bag/Concerns/WithJson.php" _blank +click get "https://github.com/dshafik/bag/blob/main/src/Bag/Concerns/WithOutput.php" _blank +click unwrapped "https://github.com/dshafik/bag/blob/main/src/Bag/Concerns/WithOutput.php" _blank +click processParameters "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ProcessParameters.php" _blank +click processProperties "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ProcessProperties.php" _blank +click getValues "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/GetValues.php" _blank +click hide "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/HideValues.php" _blank +click hideJson "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/HideJsonValues.php" _blank +click cast "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/CastOutputValues.php" _blank +click mapOutput "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/MapOutput.php" _blank +click wrap "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/Wrap.php" _blank +``` + +> [!NOTE] +> \* These steps are only performed if the Bag is being converted to an array and/or JSON. + +## The Validation Pipeline + +The `ValidationPipeline` is responsible for validating the input data without constructing the Bag object. The pipeline consists of the following steps: + +```mermaid +graph TD; +start("Bag::validate($data)") +--> transform(Transform Input) +--> process(Process Parameters) +--> variadic(Is Variadic?) +--> fillNulls(Fill Nulls) +--> mapInput(Map Input) +-- Finalized Input Values --> missing(Missing Parameters) --> missingError{Error?} +missingError -- Yes --> errorMissingParameters(MissingPropertiesException) +missingError -- No --> extra(Extra Parameters) --> extraError{Error?} +extraError -- Yes --> errorExtraParameters(ExtraPropertiesException) +extraError -- No --> validate(Validate) +--> valid{Valid?} +valid -- No --> errorValidation(ValidationException) +valid -- Yes --> success(return true) + +class start mermaid-start +class missingError,extraError,valid mermaid-decision +class success mermaid-end +class errorMissingParameters,errorExtraParameters,errorValidation mermaid-error + +click start "https://github.com/dshafik/bag/blob/main/src/Bag/Concerns/WithValidation.php" _blank +click transform "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/Transform.php" _blank +click process "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ProcessParameters.php" _blank +click variadic "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/IsVariadic.php" _blank +click fillNulls "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/FillNulls.php" _blank +click mapInput "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/MapInput.php" _blank +click missing "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/MissingParameters.php" _blank +click extra "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ExtraParameters.php" _blank +click validate "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/Validate.php" _blank +``` + +## The Without Validate Pipeline + +The [`WithoutValidationPipeline`](https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/WithoutValidationPipeline.php) is identical to the `InputPipeline` but does not perform validation. The pipeline +consists of the following steps: + +```mermaid +graph TD; +start("Bag::from($data)") +--> transform(Transform Input) +--> process(Process Parameters) +--> variadic(Is Variadic?) +--> fillNulls(Fill Nulls) +--> mapInput(Map Input) +--> laravelParams(Laravel Route Parameter Binding) +-- Finalized Input Values --> missing(Missing Parameters) --> missingError{Error?} +missingError -- Yes --> errorMissingParameters(MissingPropertiesException) +missingError -- No --> extra(Extra Parameters) --> extraError{Error?} +extraError -- Yes --> errorExtraParameters(ExtraPropertiesException) +extraError -- No +--> construct("new Bag(...)") +--> computed(Verify Computed Values) +--> initialized{Initialized?} +initialized -- No --> errorInitialization(ComputedPropertyUninitializedException) +initialized -- Yes +--> bag(Bag Value Returned) + +class start mermaid-start +class missingError,extraError,valid,initialized mermaid-decision +class bag mermaid-end +class errorMissingParameters,errorExtraParameters,errorValidation,errorInitialization mermaid-error + +click start "https://github.com/dshafik/bag/blob/main/src/Bag/Bag.php" _blank +click transform "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/Transform.php" _blank +click process "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ProcessParameters.php" _blank +click variadic "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/IsVariadic.php" _blank +click fillNulls "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/FillNulls.php" _blank +click mapInput "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/MapInput.php" _blank +click laravelParams "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/LaravelRouteParameters.php" _blank +click missing "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/MissingParameters.php" _blank +click extra "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ExtraParameters.php" _blank +click validate "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/Validate.php" _blank +click cast "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/CastInputValues.php" _blank +click construct "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/FillBag.php" _blank +click computed "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ComputedValues.php" _blank +``` + +## The Output Collection Pipeline + +The `OutputCollectionPipeline` is responsible for transforming the Bag collection data to the desired output array or JSON. The pipeline consists of the following steps: + +```mermaid +graph TD; +toArray("Collection->toArray()") --> wrap +toJson("Collection->toJson()") --> wrap +jsonEncode("json_encode($collection)") --> wrap +unwrapped("Collection->unwrapped()") --> wrap +wrap(Wrap Collection*) +--> output(array or JSON string) + +class toArray,toJson,jsonEncode,unwrapped mermaid-start +class output mermaid-end + +click toArray "https://github.com/dshafik/bag/blob/main/src/Bag/Collection.php" _blank +click toJson "https://github.com/dshafik/bag/blob/main/src/Bag/Collection.php" _blank +click jsonEncode "https://github.com/dshafik/bag/blob/main/src/Bag/Collection.php" _blank +click unwrapped "https://github.com/dshafik/bag/blob/main/src/Bag/Collection.php" _blank +click wrap "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/WrapCollection.php" _blank +``` + +> [!NOTE] +> \* This step is only performed if the Bag is being converted to an array and/or JSON. + + diff --git a/docs/versions/2.0/index.md b/docs/versions/2.0/index.md new file mode 100644 index 0000000..7de2165 --- /dev/null +++ b/docs/versions/2.0/index.md @@ -0,0 +1,41 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: "Bag" + text: "Immutable Value Objects for PHP" + tagline: "composer require dshafik/bag" + image: /assets/images/bag.png + actions: + - theme: brand + text: Get Started + link: ./install + +features: + - title: Immutable & Strongly typed + icon: 🦾 + details: Bag value objects are immutable and strongly typed, a safer and more predictable way to work with data. + - title: Composable + icon: πŸ›  + details: Nest Bag value objects and collections to create complex data structures. + - title: Built on Laravel + icon: πŸ“¦ + details: Built-in validation, controller dependency injection, and more. Bag is designed to work seamlessly with Laravel. +--- + +## What is Bag? + +Bag helps you create immutable value objects. It's a great way to encapsulate data within your application. + +Bag prioritizes immutability and type safety with built-in validation and data casting. + +### When should I use Value Objects? + +Value objects should be used in place of regular arrays, allowing you enforce type safety and immutability. + +### Does it work with Laravel/Symfony/Other Framework? + +Bag is framework-agnostic, but it works great with Laravel. Bag uses standard Laravel [Collections](https://laravel.com/docs/11.x/collections) and [Validation](https://laravel.com/docs/11.x/validation). +In addition, it will automatically inject `Bag\Bag` value objects into your controllers with validation. + diff --git a/docs/versions/2.0/install.md b/docs/versions/2.0/install.md new file mode 100644 index 0000000..0398583 --- /dev/null +++ b/docs/versions/2.0/install.md @@ -0,0 +1,15 @@ +# Get Started with Bag Value Objects + +Bag is a library for creating immutable value objects in PHP. It's a great way to encapsulate data within your application. + +## Requirements + +Bag requires PHP 8.2+, and supports Laravel 10+. + +## Installation + +To install Bag, use [Composer](https://getcomposer.org): + +```bash +composer require dshafik/bag +``` diff --git a/docs/versions/2.0/laravel-artisan-make-bag-command.md b/docs/versions/2.0/laravel-artisan-make-bag-command.md new file mode 100644 index 0000000..be821a3 --- /dev/null +++ b/docs/versions/2.0/laravel-artisan-make-bag-command.md @@ -0,0 +1,289 @@ +# Generate Bag Value Objects, Collections, and Factories + +Bag includes the `make:bag` artisan command to make it easy to generate new Bag classes, Factories, and Collections. + +> [!NOTE] Namespaces +> The `make:bag` command will use the provided namespace to determine the location of the generated classes. +> You can set the namespace using the `--namespace` option or you will be prompted for it. The default namespace is `\App\Values`. + +## Generating Bags + +To create a new Bag class, use the `make:bag` command: + +```bash +php artisan make:bag MyBag +``` + +This will create a new `MyBag` class in `app/Values/MyBag.php`: + +```php + [!NOTE] +> When creating a new Bag _or_ when specifying the `--update` flag, it will automatically add the `Collection` attribute to the Bag class if necessary. + +## Generating Bag Factories + +The `make:bag` command can also generate Bag Factory classes, **and** will _automatically_ generate the `definition()` function based on the Bag class properties. + +To create a new Bag Factory class, use the `make:bag` command with the `--factory` option: + +```bash +php artisan make:bag MyBag --factory +``` + +This will create a new `MyBagFactory` class in `app/Values/Factories/MyBagFactory.php`: + +```php + [!TIP] +> When creating a new Bag _or_ when specifying the `--update` flag, it will automatically add the `Factory` attribute and `HasFactory` trait to the Bag class. + +## Updating Factories + +Once you had added properties to your Bag class, you can update the Factory class using the `--update` option: + +```bash +php artisan make:bag MyBag --factory --docs --update +``` + +> [!TIP] +> If you update the Bag class properties, you can re-run the `make:bag` command with the `--update` and `--force-excluding-bag` options to update the Factory class. + +For example, if we update our `MyBag` constructor to look like the following: + +```php +public function __construct( + public string $name, + public int $age, + #[Cast(DateTime::class, 'y-m-d')] + public CarbonImmutable $birthday, + public Money $money, + public AnotherBag $test, + #[Cast(CollectionOf::class, AnotherBag::class)] + public Collection $collection, +) { +} +``` + +The generated factory will look like this: + +```php + $this->faker->word(), + 'age' => $this->faker->randomNumber(), + 'birthday' => new CarbonImmutable(), + 'money' => Money::ofMinor($this->faker->numberBetween(100, 10000), 'USD'), + 'test' => AnotherBag::factory()->make(), + 'collection' => AnotherBag::collect([AnotherBag::factory()->make()]), + ]; + } +} +``` + +## Documentation/Auto-Complete Helpers + +To enable easier use of named/positional arguments when creating a new Bag, you can use the `--docs` option to automatically generate it for the Bag, +this will add an `@method` annotation to the Bag class to provide auto-complete for the `::from()` method: + +```bash +php artisan make:bag MyBag --docs +``` + +This will add the following to the `MyBag` class: + +```php +/** + * @method static static from(array $data) + */ +``` + +## Updating Documentation + +Similar to Factories, you can update the documentation using the `--update` option: + +```bash +php artisan make:bag MyBag --docs --update +``` + +This will update the `@method` annotation to include documentation for all defined Value attributes. For example, given the following bag: + +```php +use App\Values\Collections\ReportCollection; +use App\Values\Report; +use App\Values\Team; +use Carbon\CarbonImmutable; + +readonly class User extends Bag { + public function __construct( + public string $name, + public int $age, + public Team $team, + #[Cast(CollectionOf::class, Report::class)] + public ReportCollection $reports, + public CarbonImmutable $lastLogin, + ) { + } +} +``` + +The `@method` annotation will added to the `User` class: + +```php +use App\Values\Collections\ReportCollection; +use App\Values\Report; +use App\Values\Team; +use Carbon\CarbonImmutable; + +/** + * @method static static from(string $name, int $age, Team $team, ReportCollection $reports, CarbonImmutable $lastLogin) + */ +readonly class User extends Bag { + public function __construct( + public string $name, + public int $age, + public Team $team, + #[Cast(CollectionOf::class, Report::class)] + public ReportCollection $reports, + public CarbonImmutable $lastLogin, + ) { + } +} +``` + +## Expected Usage + +Because the factory is based on the Bag class properties, you will typically create and customize the Bag value object, and **then** create +the factory and add docs. To do this, you would follow these steps: + +1. Create the Bag class (optionally, with a collection): + +```bash +php artisan make:bag MyBag --collection +``` + +2. Customize the Bag class + +3. Create the Bag Factory and docs: + +```bash +php artisan make:bag MyBag --factory --docs --update +``` + +If you have already created the factory, you must add the `--force-except-bag` option to overwrite it: + +```bash +php artisan make:bag MyBag --factory --docs --update --force-except-bag +``` + +> [!WARNING] +> If you use the `--force` option instead of `--force-except-bag` it will overwrite your customized Bag Value class, losing any customizations. + +## The `make:bag` Command + +The `make:bag` command has the following options: + +``` +Description: + Create a new Bag value class, with optional factory and collection. + +Usage: + make:bag [options] [--] + +Arguments: + name + +Options: + -F, --force Force overwriting all files + -E, --force-except-bag Force overwriting Factory/Collection files + -u, --update Update Bag class to add factory/collection + -f, --factory[=FACTORY] Create a Factory for the Bag [default: "interactive"] + -c, --collection[=COLLECTION] Create a Collection for the Bag [default: "interactive"] + -d, --docs Add Bag::from() docs to the Bag for IDE auto-complete + -N, --namespace[=NAMESPACE] Specify the namespace for the Bag + --pretend Dump the file contents instead of writing to disk + -h, --help Display help for the given command. When no command is given display help for the list command + -q, --quiet Do not output any message + -V, --version Display this application version + --ansi|--no-ansi Force (or disable --no-ansi) ANSI output + -n, --no-interaction Do not ask any interactive question + --env[=ENV] The environment the command should run under + -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug +``` diff --git a/docs/versions/2.0/laravel-controller-injection.md b/docs/versions/2.0/laravel-controller-injection.md new file mode 100644 index 0000000..bc4df56 --- /dev/null +++ b/docs/versions/2.0/laravel-controller-injection.md @@ -0,0 +1,42 @@ +# Laravel Controller Injection + +Bag can automatically inject Bag objects into your controllers using Laravel's automatic dependency injection. This can take +the place of using Laravel's form request validation _and_ accessing the input data. + +```php +use App\Values\MyValue; + +class MyController extends Controller { + public function store(MyValue $value) { + // $value is a validated MyValue object + } +} +``` + +## Automatic Validation + +When you type hint a `Bag` object in your controller method, Bag will automatically validate the request data and inject the `Bag` object into your controller method. + +## Manual Validation + +If you want to inject the `Bag` object without validation, you can add the `WithoutValidation` attribute to the property: + +```php +use App\Values\MyValue; +use Bag\Attributes\WithoutValidation; + +class MyController extends Controller { + public function store( + #[WithoutValidation] MyValue $value + ) { + $value = $value->append(extra: 'data')->valid(); + // $value is now a validated MyValue object + } +} +``` + +> [!TIP] +> The `Bag->valid()` method will throw a `ValidationException` if the Bag object is not valid by default, you can pass in `false` to return null instead. + +> [!CAUTION] +> The input values must still fulfill the requirements of the Bag constructor, all required properties must be present otherwise an exception will be thrown. diff --git a/docs/versions/2.0/laravel-eloquent-casting.md b/docs/versions/2.0/laravel-eloquent-casting.md new file mode 100644 index 0000000..3847005 --- /dev/null +++ b/docs/versions/2.0/laravel-eloquent-casting.md @@ -0,0 +1,78 @@ +# Eloquent Casting + +Bag supports Eloquent casting for both Bag objects and Collections. + +## Casting Bag Objects + +To cast a Bag object, use the Bag class name as the cast type in the `$casts` property of your Eloquent model: + +```php{7} +use Illuminate\Database\Eloquent\Model; +use App\Values\MyValue; + +class MyModel extends Model +{ + protected $casts = [ + 'my_value' => MyValue::class, + ]; +} +``` + +This will cast the `MyValue` object to a JSON string when saving it to the database and will cast it back to a `MyValue` object when retrieving it from the database. + +> [!WARNING] +> Bag will store _all_ properties, including hidden properties. + +## Casting Collections + +For Laravel 11+, you can cast to a Collection using the `casts()` method along with the `::castAsCollection()` method on your Bag class: + +```php{6,8} +use Illuminate\Database\Eloquent\Model; +use App\Values\MyValue; + +class MyModel extends Model +{ + public function casts(): array { + return [ + 'my_values' => MyValue::castAsCollection(), + ]; + } +} +``` + +or the `AsBagCollection::of()` method: + +```php{7,9} +use Illuminate\Database\Eloquent\Model; +use App\Values\MyValue; +use Bag\AsBagCollection; + +class MyModel extends Model +{ + public function casts(): array { + return [ + 'my_values' => AsBagCollection::of(MyValue::class), + ]; + } +} +``` + +Alternatively, for Laravel 10, you can use the `AsBagCollection` class as the cast type in the `$casts` property of your Eloquent model, passing in the Bag class name as the first argument: + +```php{8} +use Illuminate\Database\Eloquent\Model; +use App\Values\MyValue; +use Bag\AsBagCollection; + +class MyModel extends Model +{ + protected $casts = [ + 'my_values' => AsBagCollection::class . ':' . MyValue::class, + ]; +} +``` + +Bag will cast the Collection to a JSON string when saving to the database and will cast them back to a Collection of `MyValue` objects when retrieving them from the database. + +Bag will automatically use the custom collection class if one is defined on the Bag class when retrieving the value. diff --git a/docs/versions/2.0/laravel-route-parameter-binding.md b/docs/versions/2.0/laravel-route-parameter-binding.md new file mode 100644 index 0000000..46e7689 --- /dev/null +++ b/docs/versions/2.0/laravel-route-parameter-binding.md @@ -0,0 +1,111 @@ +# Laravel Route Parameter Binding + +Bag can automatically populate properties from Laravel Route Parameters. This can be useful when you have both a +Bag representing your request body, and a route parameter that you want to associate with the Bag. + +## Binding Route Parameters + +To bind a route parameter, use the `Bag\Attributes\Laravel\FromRouteParameter` attribute on the property you want to bind. + +For example given the following controller: + +```php +use App\Values\MyValue; + +class MyController { + public function show(MyValue $bag, string $id) { + // $bag is automatically injected and validated + // $id is a route parameter + } +} +``` + +You can create a Bag like this that will automatically populate the `id` property based on the route parameter as injected above: + +```php{6} +use Bag\Attributes\Laravel\FromRouteParameter; +use Bag\Bag; + +class MyValue extends Bag +{ + #[FromRouteParameter()] + public string $id; +} +``` + +This will automatically populate the `id` property from the route parameter with the same name as the property. + +You can also use a different property name just by passing it to the attribute: + +```php{6} +use Bag\Attributes\Laravel\FromRouteParameter; +use Bag\Bag; + +class MyValue extends Bag +{ + #[FromRouteParameter('id')] + public string $valueId; +} +``` + +This will automatically popualte the `valueId` property from the route parameter with the name `id`. + +## Binding Route Parameter Properties + +In addition to binding the entire route parameter value to a single property, you can also bind a property or array key +from that value to your Bag using the `Bag\Attributes\Laravel\FromRouteParameterProperty` attribute. + +For example, given the following controller: + +```php +use App\Models\User; +use App\Values\MyValue; + +class MyController { + public function show(MyValue $bag, User $user) { + // $bag is automatically injected and validated + // $user is a route parameter + } +} +``` + +We can map the `id` property from the `User` model to a property on the Bag like this: + +```php{6} +use Bag\Attributes\Laravel\FromRouteParameter; +use Bag\Bag; + +class MyValue extends Bag +{ + #[FromRouteParameterProperty('user')] + public string $id; +} +``` + +This will automatically populate the `id` property with the `id` property from the `user` route parameter. +Alternatively, you can specify a source property name: + +```php{6} +use Bag\Attributes\Laravel\FromRouteParameter; +use Bag\Bag; + +class MyValue extends Bag +{ + #[FromRouteParameterProperty('user', 'id')] + public string $userId; +} +``` + +This will automatically populate the property `userId` with the `id` property from the `user` route parameter. + + +## Types & Casting + +The value from the route parameter will be cast by Laravel to the type specified in the controller method, +and will then be cast to the type of the property in the Bag if necessary. + +For example, if the route parameter method argument is an integer, and the property is a string, the integer will be cast to a string. +However, if the route parameter is an eloquent model and the property is also the same eloquent model, the value will not be cast. + +> [!TIP] +> This works great for route model binding! diff --git a/docs/versions/2.0/mapping.md b/docs/versions/2.0/mapping.md new file mode 100644 index 0000000..edcfa1a --- /dev/null +++ b/docs/versions/2.0/mapping.md @@ -0,0 +1,191 @@ +# Mapping + +Bag allows you to map both input names to properties, and properties to output names. This is useful when +transforming JSON `snake_case` to your codes `camelCase` and vice-versa. + +## How Mapping Works + +Mapping should be thought of as aliasing property names. The input mapper determines what the _incoming_ aliases _could_ be by transforming the original property name, while the output mapper transform the property name and uses it for the _outgoing_ property name. This means +that if you want to map a `propertyName` from the input `property_name` you would use a `SnakeCase` mapper, rather than a `camelCase` mapper. + +## Mapping Names + +To map names use the `Bag\Attributes\MapName`, `Bag\Attributes\MapInputName`, or `Bag\Attributes\MapOutputName` attributes. These attributes can either be applied to the entire class, **or** to an individual property. + +### Class-level Mapping + +Class-level mapping applies to all properties on the class. This is useful when all properties on a class should be mapped in the same way. + +This is done by applying the mapper attribute to the class: + +```php{5,6} +use Bag\Bag; +use Bag\Attributes\MapName; +use Bag\Mappers\SnakeCase; + +#[MapInputName(SnakeCase::class) +#[MapOutputName(SnakeCase::class)] +class MyValue extends Bag { + public function __construct( + public string $myValue, + public string $myOtherValue + ) {} +} +``` + +> [!NOTE] +> The above is functionally equivelent to using: +> ```php +> #[MapName(input: SnakeCase::class, output: SnakeCase::class)] +> ``` +> +> We recommend using the `MapInputName` and `MapOutputName` attributes as mapper arguments can be passed +> directly, rather than as an array of values to either the `inputParams` and/or `outputParams` arguments: +> +> ```php +> #[MapInputName(MapperName::class, 'param1', 'param2')] +> #[MapOutputName(MapperName::class, 'param1', 'param2')] +> +> // vs +> +> #[MapName(input: MapperName::class, inputParams: ['param1', 'param2'], output: MapperName::class, outputParams: ['param1', 'param2'])] +> ``` + +In this above example, the `MyValue` class will map _all_ property names from `snake_case` to `camelCase` on both input and output, in this case, `my_value` to `myValue` and `my_other_value` to `myOtherValue`: + +```php +$value = MyValue::from([ + 'my_value' => 'value', + 'my_other_value' => 'other value' +]); +``` + +In addition to the mapped names, you can still use the original property names: + +```php +$value = MyValue::from([ + 'myValue' => 'value', + 'myOtherValue' => 'other value' +]); +``` + +> [!TIP] +> You can specify a mapper on either the class or property level, or both. If you specify a mapper on both the class and property level, the property-level mapper will take precedence. + +## Built-in Mappers + +Bag comes with a few built-in mappers: + +- `SnakeCase` - Converts property names to/from `snake_case` +- `CamelCase` - Converts property names to/from `camelCase` +- `Alias` - Allows you to specify a custom alias for a specific property name +- `Stringable` - Converts property names using a sequence of [fluent string helper methods](https://laravel.com/docs/11.x/strings#fluent-strings-method-list). + +### Using the Alias Mapper + +The `Alias` mapper allows you to specify a custom alias for a specific property name. + +> [!WARNING] +> Unlike other mappers, the `Alias` mapper **must** only be applied to individual properties. + +In the following example we alias the input name `uuid` to the property `id`: + +```php{7} +use Bag\Bag; +use Bag\Attributes\MapInputName; +use Bag\Mappers\Alias; + +class MyValue extends Bag { + public function __construct( + #[MapInputName(Alias::class, 'uuid')])] + public string $id, + ) {} +} +```` + +### Using the Stringable Mapper + +The `Stringable` mapper allows you to chain any of the [fluent string helper methods](https://laravel.com/docs/11.x/strings#fluent-strings-method-list) to convert property names. + +The `Stringable` mapper accepts any number of transformations. To pass in arguments to a given transformation, use a colon `:` followed by a comma-separated list of arguments. + +```php{4,5} +use Bag\Bag; +use Bag\Mappers\Stringable; + +#[MapInputName(Stringable::class, 'camel', 'kebab')] +#[MapOutputName(Stringable::class, 'camel', 'kebab')] +class MyValue extends Bag { + public function __construct( + public string $myValue, + public string $myOtherValue + ) {} +} +``` + +## When Mapping Applies + +Input mapping is applied when calling `Bag::from()` or `Bag::withoutValidation()`. You can use either the original property name _or_ the mapped name when creating a Bag. + +> [!TIP] +> [Validation](validation) is applied to the original property name, not the mapped name. + +Output mapping is applied when calling `$Bag->toArray()`, `$Bag->toCollection()`, or `$Bag->toJson()` (or when using `json_encode()`). + +## Mapping Hierarchy + +For input mapping, _all_ mappers are used, allowing _multiple_ mapped names to match to the same property. **The last _incoming_ +property name that matches will be the value used for the Bag.** + +The mapping hierarchy is as follows: + +- Class Level: `MapName(input)` + - Class Level: `MapInputName` + - Property Level: `MapName(input)` + - Property Level: `MapInputName` + +For output mapping, _only_ the last mapper is used. The mapping hierarchy is as follows: + +- Class Level: `MapName(output)` + - Class Level: `MapOutputName` + - PropertyLevel Level: `MapName(Output)` + - PropertyLevel Level: `MapOutputName` + +> [!NOTE] +> The `MapName` and `MapOutputName` attributes can only be added once at each level. You can add as many `MapInputName` attributes as you like! + +## Custom Mappers + +You can also create your own mappers by implementing the `\Bag\Mappers\MapperInterface` interface. + +```php +use Bag\Mappers\MapperInterface; +use Illuminate\Support\Str; + +class Kebab implements MapperInterface { + public function input(string $name): string { + return Str::of($name)->camel()->kebab(); + } + + public function output(string $name): string { + return Str::of($name)->camel()->kebab(); + } +} +``` + +Then specify it in the Mapping attribute: + +```php{5,6} +use \App\Values\Mappers\Kebab; +use Bag\Bag; +use Bag\Mappers\Stringable; + +#[MapInputName(Kebab::class)] +#[MapOutputName(Kebab::class)] +class MyValue extends Bag { + public function __construct( + public string $myValue, + public string $myOtherValue + ) {} +} +``` diff --git a/docs/versions/2.0/object-to-bag.md b/docs/versions/2.0/object-to-bag.md new file mode 100644 index 0000000..a789a3c --- /dev/null +++ b/docs/versions/2.0/object-to-bag.md @@ -0,0 +1,55 @@ +# Creating Bags From Objects + +Bag provides an easy way to create Bags from objects with the `Bag\Traits\HasBag` trait. + +This trait provides a `->toBag()` method that converts the object into a Bag object using the Bag class +defined in the `Bag\Attributes\Bag` class attribute. + +## Adding `HasBag` To Your Class + +```php{5,7} +use App\Values\MyValue; +use Bag\Attributes\Bag; +use Bag\Traits\HasBag; + +#[Bag(MyValue::class)] +class MyClass { + use HasBag; + + // ... +} +``` + +Once you have made these changes to your class, you can easily create a Bag object from an instance of your class by calling the `->toBag()` method: + +```php +$myClass = new MyClass(); +$bag = $myClass->toBag(); +``` + +## Property Visibility + +By default, the `Bag\Traits\HasBag` trait will only include public properties in the Bag object. If you would like to include protected and private properties, you can pass the `visibility` argument to the `Bag\Attributes\Bag` attribute. + +The visibility property is a bitmask of the following values: + +- `Bag\Attributes\Bag::PUBLIC` - Include public properties +- `Bag\Attributes\Bag::PROTECTED` - Include protected properties +- `Bag\Attributes\Bag::PRIVATE` - Include private properties +- `Bag\Attributes\Bag::ALL` - Include all properties + +```php{5} +use App\Values\MyValue; +use Bag\Attributes\Bag; +use Bag\Traits\HasBag; + +#[Bag(MyValue::class, Bag::PUBLIC | Bag::PROTECTED)] +class MyClass { + use HasBag; + + // ... +} +``` + +> [!TIP] +> You can also specify the visibility when calling the `->toBag()` method e.g. `$myClass->toBag(Bag::ALL)` diff --git a/docs/versions/2.0/output.md b/docs/versions/2.0/output.md new file mode 100644 index 0000000..034da54 --- /dev/null +++ b/docs/versions/2.0/output.md @@ -0,0 +1,30 @@ +# Output + +Once you have created a Bag value object, you can access the properties using object notation: + +```php +use App\Values\MyValue; + +$value = new MyValue(name: 'Davey Shafik', age: 40); +$value->name; // 'Davey Shafik' +``` + +## Casting to Other Types + +Bag supports casting to arrays and [`\Bag\Collections`][./collections##extending-laravel-collections] using the +`Bag->toArray()` and `Bag->toCollection()` methods. + +Both methods will apply casting and mapping, as well as respect [hidden properties][./hidden]. + +## JSON Serialization + +In addition, you can serialize a Bag object to JSON using `json_encode()` or `Bag->toJson(): + +```php +$value = MyValue::from(name: 'Davey Shafik', age: 40); + +$value->toJson(); // {"name": "Davey Shafik", "age": 40} +json_encode($value, JSON_THROW_ON_ERROR); // {"name": "Davey Shafik", "age": 40} +``` + +Both `Bag->toJson()` and `json_encode()` will respect [hidden properties][./hidden]. diff --git a/docs/versions/2.0/roadmap.md b/docs/versions/2.0/roadmap.md new file mode 100644 index 0000000..a44d6fa --- /dev/null +++ b/docs/versions/2.0/roadmap.md @@ -0,0 +1,11 @@ +# Roadmap + +The following is a list of features that we want to implement before we consider the library to be stable. This list is subject to change and is not exhaustive. + +- [x] Add support for multiple mappers on a single class or property +- [x] Add support for a simple Object -> Bag trait ([Docs](object-to-bag)) +- [x] Add support for wrapping a Bag when transforming to an array/JSON ([Docs](wrapping)) +- [x] Add support for filling data from Laravel route parameters ([Docs](laravel-route-parameter-binding)) +- [x] Add support for computed values ([Docs](computed-properties)) + +Do you see something missing? File a [Feature Request](https://github.com/dshafik/bag/issues/new?assignees=&labels=enhancement&projects=&template=feature_request.md&title=)! diff --git a/docs/versions/2.0/testing.md b/docs/versions/2.0/testing.md new file mode 100644 index 0000000..42323bc --- /dev/null +++ b/docs/versions/2.0/testing.md @@ -0,0 +1,157 @@ +# Testing + +Bag supports Factories to make creating test values easier. Bag factories are similar to [Eloquent factories](https://laravel.com/docs/11.x/eloquent-factories), but they are used to create Bag objects. + +## Creating a Factory + +Factories extend the `Bag\Factory` class, and define a `definition()` method that returns an array of default values for the value object. + +```php +use Bag\Factory; + +class MyValueFactory extends Factory { + #[Override] + public function definition(): array { + return [ + 'name' => 'Davey Shafik', + 'age' => 40, + ]; + } +} +``` + +### Faker Integration + +Factories include [Faker](https://fakerphp.org) support out of the box. You can use the `$faker` property to generate random values: + +```php +return [ + 'name' => $this->faker->name(), + 'age' => $this->faker->numberBetween(18, 65), +]; +``` + +> [!TIP] +> You can also generate factory classes automatically using the [`artisan make:bag`][./laravel-artisan-make-bag-command] command. + +## Using a Factory + +Before you can use a Factory, you must first add both the `Factory` attribute and the `HasFactory` trait to your Bag object: + +```php{5,7} +use Bag\Attributes\Factory; +use Bag\Bag; +use Bag\Traits\HasFactory; + +#[Factory(MyValueFactory::class)] +class MyValue extends Bag { + use HasFactory; + + public function __construct( + public string $name, + public int $age, + ) {} +} +``` + +You can now use the factory to create a new instance of the value object: + +```php +$bag = MyValue::factory()->make(); +``` + +This will create a new `MyValue` object using the factory definition. + +## Customizing Factory State + +You can also specify custom values when creating a factory, which will override the factory definition. You can pass the values to the `::factory()` call itself, +using the `->state()` method on the factory, or by passing it to the `->make()` method. + +```php +// All three are identical: + +$value = MyValue::factory([ + 'name' => 'Taylor Otwell', +])->make(); + +$value = MyValue::factory()->make([ + 'name' => 'Taylor Otwell', +]); + +$value = MyValue::factory()->state([ + 'name' => 'Taylor Otwell', +])->make(); +``` + +## Named States + +Bag supports named states, which allow you to modify the state of the value object when creating it: + +```php +use Bag\Factory; + +class MyValueFactory extends Factory { + public function definition(): array { + return [ + 'name' => 'Davey Shafik', + 'age' => 40, + ]; + } + + public function withName(string $name): static { + return $this->state([ + 'name' => $name, + ]); + } +} +``` + +You can now use the state when creating the value object: + +```php +$bag = MyValue::factory()->withName($faker->name())->make(); +``` + +## Creating Collections of Bag Values + +You can use the `->count()` method to create a collection of Bag objects: + +```php +$values = MyValue::factory()->count(10)->make(); +``` + +This will create a `Bag\Collection` of 10 identical `MyValue` objects. + +> [!TIP] +> If your Bag object has a `Collection` attribute, `->make()` will return an instance of that collection class. + +## Sequences + +Bag factories support Eloquent factory Sequences to generate unique values for each instance in a collection. + +```php +use Illuminate\Database\Eloquent\Factories\Sequence; + +$bag = MyValue::factory()->count(10)->sequence(fn(Sequence $sequence) => [ + 'name' => 'Person #' . $sequence->index, + 'age' => 18 + $sequence->index, +])->make(); +``` + +In this example, the `name` property will be set to `Person #1`, `Person #2`, etc., and the `age` property will be set to `19`, `20`, etc. + +The `->sequence()` method accepts any of the following: + +- A `Illuminate\Database\Eloquent\Factories\Sequence` instance created with a `closure` that returns an array of values +- A `Illuminate\Database\Eloquent\Factories\Sequence` instance created with a variadic number of arrays of values +- A `closure` value that returns an array of values +- A variadic number of arrays of values + +You may also pass a `Sequence` object to the `->state()` method. + +> [!TIP] +> If you create more values than number of value arrays passed in, the sequence will start over from the beginning. + +> [!WARNING] +> If you use both states (named or via the `::factory()`, `->state()`, or `->make()` methods) and sequences, sequences will be applied _after_ the state, so the sequences will override any values set by the state. + diff --git a/docs/versions/2.0/transformers.md b/docs/versions/2.0/transformers.md new file mode 100644 index 0000000..8f1ca6c --- /dev/null +++ b/docs/versions/2.0/transformers.md @@ -0,0 +1,102 @@ +# Transformers + +Transformers are helpers that transform Bag input data from custom types, e.g. Models or JSON strings. + +## Using Transformers + +Transformers are methods defined on your Bag class that transform input data into the correct type. Transformers are +identified by adding the `Transforms` attribute to the method, passing the type you want to transform from. + +For example, to transform from a JSON string: + +```php +use Bag\Bag; + +class MyValue extends Bag +{ + #[Transforms('string')] + protected static function fromJsonString(string $json): array + { + return json_decode($json, true, 512, JSON_THROW_ON_ERROR); + } +} +``` + +or to transform from a specific class: + +```php +use App\Models\User; +use Bag\Bag; + +class MyValue extends Bag +{ + #[Transforms(User::class)] + protected static function fromUser(User $user): array + { + return $user->toArray(); + } +} +``` + +You can pass multiple types to the `Transforms` attribute, or use multiple `Transforms` attributes to handle multiple types. + +The following two examples are functionally the same: + +```php +use App\Models\Book; +use App\Models\Magazine; +use Bag\Bag; + +class MyValue extends Bag +{ + #[Transforms(Book::class)] + #[Transforms(Magazine::class)] + protected static function fromMedia(Book|Magazine $media): array + { + return $media->toArray(); + } +} +``` + +and: + +```php +use App\Models\Book; +use App\Models\Magazine; +use Bag\Bag; + +class MyValue extends Bag +{ + #[Transforms(Book::class, Magazine::class)] + protected static function fromMedia(Book|Magazine $media): array + { + return $media->toArray(); + } +} +``` + +Bag will match child classes to their parents, so `Transforms(Model::class)` will match any child `Model` objects, however, +if there is a more specific transformer available, Bag will use that instead. + +> [!TIP] +> Bag will use the most specific transformer available, if two or more transformers are equally specific then Bag will use the first one it finds. + +### Handling JSON + +By default, Bag will transform JSON strings, but you can override this behavior by defining a overriding the `fromJsonString` method: + +```php +use Bag\Bag; + +class MyValue extends Bag +{ + #[Transforms(Bag::FROM_JSON)] + protected static function fromJsonString(string $json): array + { + return json_decode($json, true, 512, JSON_THROW_ON_ERROR); + } +} +``` + +To differentiate between other strings and JSON, you should use the special type `Bag::FROM_JSON` as the `Transformer` type. + diff --git a/docs/versions/2.0/upgrading.md b/docs/versions/2.0/upgrading.md new file mode 100644 index 0000000..ca93752 --- /dev/null +++ b/docs/versions/2.0/upgrading.md @@ -0,0 +1,50 @@ +# Upgrading to Bag 2 + +This guide will help you upgrade your application from Bag 1.x to Bag 2.x. + +## Casting with Union Types + +To support union types fully, the `\Bag\Casts\CastsPropertySet::set()` method signature has changed the first argument from: + +```php +public function set(string $propertyType, string $propertyName, \Illuminate\Support\Collection $properties): mixed +``` + +to: + +```php +public function set(\Bag\Collection $propertyTypes, string $propertyName, \Illuminate\Support\Collection $properties): mixed +``` + +This change allows for union types to be passed in as a collection of types (as string type names). Your return value must match one of the types in the collection. + +## Legacy Behavior + +In Bag 1.x the first type in the union was used to cast the property. To update your code and retain the previous behavior, you will want to do the following: + +```diff +- public function set(string $propertyType, string $propertyName, \Illuminate\Support\Collection $properties): mixed +- { ++ public function set(\Bag\Collection $propertyTypes, string $propertyName, \Illuminate\Support\Collection $properties): mixed ++ { ++ $propertyType = $propertyTypes->first(); +``` + +## Fill Nullables + +The behavior when instantiating a Bag has changed such that arguments that are nullable _without_ a default value are filled with nulls. +Previously, this would have caused exception to be thrown. This solves for a common scenario when you are filling a Bag from user input. + +```php +readonly class MyBag extends Bag { + public function __construct( + public ?string $name + } +} + +// Bag 1.4.0 (and older) +MyBag::from([]); // throws MissingPropertiesException + +// Bag 2.0.0+ +MyBag::from([]); // MyBag { $name = null } +``` diff --git a/docs/versions/2.0/validation.md b/docs/versions/2.0/validation.md new file mode 100644 index 0000000..87f8270 --- /dev/null +++ b/docs/versions/2.0/validation.md @@ -0,0 +1,106 @@ +# Validation + +Bag uses Laravel's validation system to validate input data. You can define validation rules using annotations, and/or using the `rules()` method. + +`Bag` will automatically validate input data when creating a new instance using the `Bag::from()` method. + +```php +use Bag\Attributes\Validation\Required; +use Bag\Bag; + +readonly class MyValue extends Bag +{ + public function __construct( + #[Required] + public string $name, + public int $age, + ) { + } + + public static function rules(): array + { + return [ + 'name' => ['string'], + 'age' => ['integer'], + ]; + } +} +``` + +In this example we added a `#[Required]` attribute to the `name` property, and defined validation rules in the `rules()` method. + +You can validate a Bag object before creating it using the `Bag::validate()` method: + +```php +$value = MyValue::validate([ + 'name' => 'Davey Shafik', + 'age' => 40, +]); +``` + +## Creating a Bag Without Validation + +If you want to create a Bag without automatically validating it, you can use the `Bag::withoutValidation()` method: + +```php +$value = MyValue::withoutValidation([ + 'name' => 'Davey Shafik', + 'age' => 40, +]); +``` + +To futher append properties to a Bag without validation, you can use the `Bag::append()` method: + +```php +$value = MyValue::withoutValidation([ + 'name' => 'Davey Shafik', +])->append([ + 'age' => 40, +]); +``` + +## Built-in Validation Attributes + +Bag provides a number of built-in validation attributes, based on various Laravel validation rules: + +| Rule | | Usage | +|-------------------------------------------------------------------------------|---------------------------------------------------------------|--------------------------------------------| +| [Between](https://laravel.com/docs/validation#rule-between) | The value should be between two values (inclusive) | `#[Between(1, 10)]` | +| [Boolean](https://laravel.com/docs/validation#rule-boolean) | The value should be a boolean | `#[Boolean]` | +| [Decimal](https://laravel.com/docs/validation#rule-decimal) | The value should be a decimal number | `#[Decimal]` | +| [Email](https://laravel.com/docs/validation#rule-email) | The value should be an email address | `#[Email]` | +| [Enum](https://laravel.com/docs/validation#rule-enum) | The value should be an enum case | `#[Enum(MyEnum::class)]` | +| [Exists](https://laravel.com/docs/validation#rule-exists) | The value must exist in a field in a table | `#[Exists('table', 'optionalColumn')]` | +| [In](https://laravel.com/docs/validation#rule-in) | The value should be in the given list | `#[In('foo', 'bar')]` | +| [Integer](https://laravel.com/docs/validation#rule-integer) | The value should be an integer | `#[Integer]` | +| [Max](https://laravel.com/docs/validation#rule-max) | The value should be at most a given size | `#[Max(100)]` | +| [Min](https://laravel.com/docs/validation#rule-min) | The value should be at minimum a given size | `#[Min(1)]` | +| [NotRegex](https://laravel.com/docs/validation#rule-not-regex) | The value should not match a given regex | `#[NotRegex('/regex/')]` | +| [Numeric](https://laravel.com/docs/validation#rule-numeric) | The value should be numeric | `#[Numeric]` | +| [Regex](https://laravel.com/docs/validation#rule-regex) | The value should match a given regex | `#[Regex('/regex/')]` | +| [Required](https://laravel.com/docs/validation#rule-required) | The value is required | `#[Required]` | +| [RequiredIf](https://laravel.com/docs/validation#rule-required-if) | The value is required if another field matches a value | `#[RequiredIf('otherField', 'value')]` | +| [RequiredUnless](https://laravel.com/docs/validation#rule-required-unless) | The value is required unless another field matches a value | `#[RequiredUnless('otherField', 'value')]` | +| [RequiredWith](https://laravel.com/docs/validation#rule-required-with) | The value is required if another field is present | `#[RequiredWith('otherField')]` | +| [RequiredWithAll](https://laravel.com/docs/validation#rule-required-with-all) | The value is required if more than one other field is present | `#[RequiredWithAll('field1', 'field2')]` | +| [Size](https://laravel.com/docs/validation#rule-size) | The value should have a specific size | `#[Size(10)]` | +| [Str](https://laravel.com/docs/validation#rule-string) | The value should be a string | `#[Str]` | +| [Unique](https://laravel.com/docs/validation#rule-unique) | The value must be unique for a field in a table | `#[Unique('table', 'optionalColumn')]` | + +In addition, a generic `\Bag\Attributes\Validation\Rule` attribute is available to apply any Laravel validation rule: + +```php +use Bag\Attributes\Validation\Rule; + +readonly class MyValue extends Bag +{ + public function __construct( + #[Required] + public string $username, + public string $password, + #[Rule('same:password')] + public string $passwordConfirmation, + ) { + } +} +``` diff --git a/docs/versions/2.0/variadics.md b/docs/versions/2.0/variadics.md new file mode 100644 index 0000000..b9f6d4f --- /dev/null +++ b/docs/versions/2.0/variadics.md @@ -0,0 +1,63 @@ +# Variadic Properties + +Bag supports the use of [Variadic](https://www.php.net/manual/en/functions.arguments.php#functions.variable-arg-list) arguments. + +Variadic arguments **must** be manually assigned to a property: + +```php +use Bag\Bag; + +class MyValue extends Bag { + public $values; + + public function __construct(mixed ...$values) { + $this->values = $values; + } +} +``` + +## Casting + +Bag will automatically cast variadic values to their defined typed: + +```php +use Bag\Bag; + +class MyValue extends Bag { + public $values; + + public function __construct(bool ...$values) { + $this->values = $values; + } +} +``` + +The above example will cast all values to `bool`: + +```php +$bag = new MyValue(true, false, 0, 1); + +// $bag->values = [true, false, false, true] +``` + +You can also use `Cast` attributes to cast the values: + +```php +use Bag\Bag; +use Bag\Casts\DateTime; +use Carbon\CarbonImmutable; + +class MyValue extends Bag { + public $values; + + public function __construct( + #[Cast(DateTime::class, format: 'Y-m-d')] + CarbonImmutable ...$values + ) { + $this->values = $values; + } +} +``` + +This will cast all input values to `CarbonImmutable` instances, using the `Y-m-d` format. + diff --git a/docs/versions/2.0/why.md b/docs/versions/2.0/why.md new file mode 100644 index 0000000..34833f1 --- /dev/null +++ b/docs/versions/2.0/why.md @@ -0,0 +1,65 @@ +# Why Choose Bag? + +Bag is a simple, lightweight, modern, and flexible library for working with value objects in PHP. + +It is designed to be easy to use, with a minimal API that is easy to understand and use. + +Bag focuses on immutability and type safety. + +## Compared to spatie/laravel-data + +Bag was heavily inspired by the excellent [spatie/laravel-data](https://spatie.be/docs/laravel-data/) package, and as such +should feel very familiar to anyone who has used it β€” _however_, it has several key differences. + +## Common Features + +- [Value Casting](casting) (both in and out, although spatie/laravel-data calls outbound casting Transforming) + - Including nested Bag objects +- [Name Mapping](mapping) (both in and out) at the class and property level +- [Validation](validation) (although spatie/laravel-data does not support all Laravel validation options easily) +- [Collections](collections) of Value Objects[*](#collections) +- [Object to Bag](object-to-bag) conversion +- [Wrapping](wrapping) of output arrays/JSON +- [Eloquent Casting](laravel-eloquent-casting) +- [Laravel Controller Injection](laravel-controller-injection) + +## Immutability + +Bag is immutable by default. + +spatie/laravel-data does not support immutable value objects, and as of PHP 8.3, there is no reasonable way to make them immutable. + +## Factory Support + +Bag [factories](testing) support most of the rich features and simple UX of Laravel Model Factories except for the `create()` method (as value objects do not feature persistence). +This includes support for [factory states](https://laravel.com/docs/11.x/eloquent-factories#factory-states) and [sequences](https://laravel.com/docs/11.x/eloquent-factories#sequences). + +spatie/laravel-data v3 does not support factories, while v4 has [rudimentary support](https://spatie.be/docs/laravel-data/v4/as-a-data-transfer-object/factories). + +## Variadic Support + +Bag supports the use of [Variadic](variadics) during value object creation. + +## Collections + +Bag uses Laravel Collections as the basis for its [Collection](collections) classes, and supports them wherever Collections are used, however `Bag\Collection` is an immutable-safe variant that we recommend +using whenever possible. + +spatie/laravel-data v3 uses a custom `DataCollection` class that is not based on Laravel collections and lacks many Collection features. v4 uses Laravel Collections, although it still +has [custom collection classes](https://spatie.be/docs/laravel-data/v4/as-a-data-transfer-object/collections) with varying levels of compatibility with Laravel Collections. + +## Hidden Properties + +Bag supports [hiding properties](hidden) when transforming to an array and/or JSON. + +## Artisan `make:bag` Command + +Bag supports an [artisan command](laravel-artisan-make-bag-command) for creating new Bag classes, including support for creating factories and collections. + +## Other Differences + +In addition to the above, Bag has a few other minor differences: + +- [Casters](casting) apply to both incoming values and outgoing values, while spatie/laravel-data splits these into two difference concepts. +- Input from complex values uses [Transformers](transformers), which are more explicit in Bag, while spatie/laravel-data uses a more implicit approach with magic methods e.g. `::fromModel()` +- Simpler attribute names: `Cast` vs `WithCast` and `WithTransformer` diff --git a/docs/versions/2.0/wrapping.md b/docs/versions/2.0/wrapping.md new file mode 100644 index 0000000..3eca5f3 --- /dev/null +++ b/docs/versions/2.0/wrapping.md @@ -0,0 +1,76 @@ +# Wrapping Outputs + +Bag supports wrapping both Bag values and Collections when transforming to an array or JSON. + +## Wrapping Bags + +To wrap a Bag, add the `Bag\Attributes\Wrap` or `Bag\Attributes\WrapJson` attribute to the class: + +```php{4} +use Bag\Attributes\Wrap; +use Bag\Bag; + +#[Wrap('data')] +class MyValue extends Bag { + public function __construct( + public string $name, + public int $age, + ) {} +} +``` + +Now whenever you call `->toArray()` or serialize to JSON, the output will be wrapped in a key of `data`: + +```php +$myValue = MyValue::from(['name' => 'Davey Shafik', 'age' => 40]); +$myValue->toArray(); // ['data' => ['name' => 'Davey Shafik', 'age' => 40]] +$myValue->toJson(); // {"data":{"name":"Davey Shafik","age":40}} +``` + +If you only want to wrap when serializing to JSON, you can use the `WrapJson` attribute instead: + +```php{4} +use Bag\Attributes\WrapJson; +use Bag\Bag; + +#[WrapJson('data')] +class MyValue extends Bag { + public function __construct( + public string $name, + public int $age, + ) {} +} +``` + +Now when you serialize to JSON, the values will be wrapped, but when calling `->toArray()` the output will not be wrapped + +```php +$myValue = MyValue::from(['name' => 'Davey Shafik', 'age' => 40]); +$myValue->toArray(); // ['name' => 'Davey Shafik', 'age' => 40] +$myValue->toJson(); // {"data":{"name":"Davey Shafik","age":40}} +``` + +> [!TIP] +> You can add both `Wrap` and `WrapJson` attributes to the same class to apply different wrapping to `->toArray()` and JSON serialization respectively. + +## Wrapping Collections + +Wrapping Collections works exactly the same way as with Bags: add the `Bag\Attributes\Wrap` or `Bag\Attributes\WrapJson` attribute to the Collection class: + +```php{4} +use Bag\Attributes\Wrap; +use Bag\Collection; + +#[Wrap('data')] +class MyCollection extends Collection { +} +``` + +Now when you call `->toArray()` or serialize to JSON, the output will be wrapped in a key of `data`: + +```php +$collection = MyValue::factory()->count(2)->make(); + +$collection->toArray(); // ['data' => [["name" => "Davey Shafik", "age" => 40], ...]] +$collection->toJson(); // {"data":[{"name":"Davey Shafik","age":40}, ...]} +``` diff --git a/docs/whats-new.md b/docs/whats-new.md new file mode 100644 index 0000000..829cfa3 --- /dev/null +++ b/docs/whats-new.md @@ -0,0 +1,13 @@ +# What's New in Bag 2.1 + +## Stripping Extra Parameters + +Version 2.1 adds support for stripping extra parameters when creating Bag instances to avoid errors. + +Prior to 2.1, Bag would throw an exception if you passed in unknown parameters. + +To solve this, the addition of the `\Bag\Attributes\StripExtraParameters` attribute can be used, either [on the +class](./basic-usage#stripping-extra-parameters) or when using the [Laravel controller injection feature](./laravel-controller-injection#avoiding-extra-parameters). + +In addition to explicitly opting into this feature, using the [`\Bag\Attributes\WithoutValidation` attribute](./laravel-controller-injection#manual-validation) +or [`Bag::withoutValidation()`](./validation#creating-a-bag-without-validation) method will also strip extra parameters. diff --git a/docs/why.md b/docs/why.md index 34833f1..110d426 100644 --- a/docs/why.md +++ b/docs/why.md @@ -13,15 +13,15 @@ should feel very familiar to anyone who has used it β€” _however_, it has severa ## Common Features -- [Value Casting](casting) (both in and out, although spatie/laravel-data calls outbound casting Transforming) +- [Value Casting](./casting) (both in and out, although spatie/laravel-data calls outbound casting Transforming) - Including nested Bag objects -- [Name Mapping](mapping) (both in and out) at the class and property level -- [Validation](validation) (although spatie/laravel-data does not support all Laravel validation options easily) -- [Collections](collections) of Value Objects[*](#collections) -- [Object to Bag](object-to-bag) conversion -- [Wrapping](wrapping) of output arrays/JSON -- [Eloquent Casting](laravel-eloquent-casting) -- [Laravel Controller Injection](laravel-controller-injection) +- [Name Mapping](./mapping) (both in and out) at the class and property level +- [Validation](./validation) (although spatie/laravel-data does not support all Laravel validation options easily) +- [Collections](./collections) of Value Objects[*](#collections) +- [Object to Bag](./object-to-bag) conversion +- [Wrapping](./wrapping) of output arrays/JSON +- [Eloquent Casting](./laravel-eloquent-casting) +- [Laravel Controller Injection](./laravel-controller-injection) ## Immutability @@ -31,18 +31,18 @@ spatie/laravel-data does not support immutable value objects, and as of PHP 8.3, ## Factory Support -Bag [factories](testing) support most of the rich features and simple UX of Laravel Model Factories except for the `create()` method (as value objects do not feature persistence). +Bag [factories](./testing) support most of the rich features and simple UX of Laravel Model Factories except for the `create()` method (as value objects do not feature persistence). This includes support for [factory states](https://laravel.com/docs/11.x/eloquent-factories#factory-states) and [sequences](https://laravel.com/docs/11.x/eloquent-factories#sequences). spatie/laravel-data v3 does not support factories, while v4 has [rudimentary support](https://spatie.be/docs/laravel-data/v4/as-a-data-transfer-object/factories). ## Variadic Support -Bag supports the use of [Variadic](variadics) during value object creation. +Bag supports the use of [Variadic](./variadics) during value object creation. ## Collections -Bag uses Laravel Collections as the basis for its [Collection](collections) classes, and supports them wherever Collections are used, however `Bag\Collection` is an immutable-safe variant that we recommend +Bag uses Laravel Collections as the basis for its [Collection](./collections) classes, and supports them wherever Collections are used, however `Bag\Collection` is an immutable-safe variant that we recommend using whenever possible. spatie/laravel-data v3 uses a custom `DataCollection` class that is not based on Laravel collections and lacks many Collection features. v4 uses Laravel Collections, although it still @@ -50,16 +50,16 @@ has [custom collection classes](https://spatie.be/docs/laravel-data/v4/as-a-data ## Hidden Properties -Bag supports [hiding properties](hidden) when transforming to an array and/or JSON. +Bag supports [hiding properties](./hidden) when transforming to an array and/or JSON. ## Artisan `make:bag` Command -Bag supports an [artisan command](laravel-artisan-make-bag-command) for creating new Bag classes, including support for creating factories and collections. +Bag supports an [artisan command](./laravel-artisan-make-bag-command) for creating new Bag classes, including support for creating factories and collections. ## Other Differences In addition to the above, Bag has a few other minor differences: -- [Casters](casting) apply to both incoming values and outgoing values, while spatie/laravel-data splits these into two difference concepts. -- Input from complex values uses [Transformers](transformers), which are more explicit in Bag, while spatie/laravel-data uses a more implicit approach with magic methods e.g. `::fromModel()` +- [Casters](./casting) apply to both incoming values and outgoing values, while spatie/laravel-data splits these into two difference concepts. +- Input from complex values uses [Transformers](./transformers), which are more explicit in Bag, while spatie/laravel-data uses a more implicit approach with magic methods e.g. `::fromModel()` - Simpler attribute names: `Cast` vs `WithCast` and `WithTransformer` diff --git a/package-lock.json b/package-lock.json index 96f2e3e..00d8488 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,8 @@ "packages": { "": { "dependencies": { - "markdown-it-task-lists": "^2.1.1" + "markdown-it-task-lists": "^2.1.1", + "vitepress-versioning-plugin": "^1.3.0" }, "devDependencies": { "mermaid": "^10.9.3", @@ -14,34 +15,34 @@ } }, "node_modules/@algolia/autocomplete-core": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.9.3.tgz", - "integrity": "sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==", - "dev": true, + "version": "1.17.9", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.9.tgz", + "integrity": "sha512-O7BxrpLDPJWWHv/DLA9DRFWs+iY1uOJZkqUwjS5HSZAGcl0hIVCQ97LTLewiZmZ402JYUrun+8NqFP+hCknlbQ==", + "license": "MIT", "dependencies": { - "@algolia/autocomplete-plugin-algolia-insights": "1.9.3", - "@algolia/autocomplete-shared": "1.9.3" + "@algolia/autocomplete-plugin-algolia-insights": "1.17.9", + "@algolia/autocomplete-shared": "1.17.9" } }, "node_modules/@algolia/autocomplete-plugin-algolia-insights": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.9.3.tgz", - "integrity": "sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg==", - "dev": true, + "version": "1.17.9", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.9.tgz", + "integrity": "sha512-u1fEHkCbWF92DBeB/KHeMacsjsoI0wFhjZtlCq2ddZbAehshbZST6Hs0Avkc0s+4UyBGbMDnSuXHLuvRWK5iDQ==", + "license": "MIT", "dependencies": { - "@algolia/autocomplete-shared": "1.9.3" + "@algolia/autocomplete-shared": "1.17.9" }, "peerDependencies": { "search-insights": ">= 1 < 3" } }, "node_modules/@algolia/autocomplete-preset-algolia": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.9.3.tgz", - "integrity": "sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA==", - "dev": true, + "version": "1.17.9", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.9.tgz", + "integrity": "sha512-Na1OuceSJeg8j7ZWn5ssMu/Ax3amtOwk76u4h5J4eK2Nx2KB5qt0Z4cOapCsxot9VcEN11ADV5aUSlQF4RhGjQ==", + "license": "MIT", "dependencies": { - "@algolia/autocomplete-shared": "1.9.3" + "@algolia/autocomplete-shared": "1.17.9" }, "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", @@ -49,168 +50,221 @@ } }, "node_modules/@algolia/autocomplete-shared": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.9.3.tgz", - "integrity": "sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ==", - "dev": true, + "version": "1.17.9", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.9.tgz", + "integrity": "sha512-iDf05JDQ7I0b7JEA/9IektxN/80a2MZ1ToohfmNS3rfeuQnIKI3IJlIafD0xu4StbtQTghx9T3Maa97ytkXenQ==", + "license": "MIT", "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", "algoliasearch": ">= 4.9.1 < 6" } }, - "node_modules/@algolia/cache-browser-local-storage": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.23.3.tgz", - "integrity": "sha512-vRHXYCpPlTDE7i6UOy2xE03zHF2C8MEFjPN2v7fRbqVpcOvAUQK81x3Kc21xyb5aSIpYCjWCZbYZuz8Glyzyyg==", - "dev": true, + "node_modules/@algolia/client-abtesting": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.20.0.tgz", + "integrity": "sha512-YaEoNc1Xf2Yk6oCfXXkZ4+dIPLulCx8Ivqj0OsdkHWnsI3aOJChY5qsfyHhDBNSOhqn2ilgHWxSfyZrjxBcAww==", + "license": "MIT", "dependencies": { - "@algolia/cache-common": "4.23.3" + "@algolia/client-common": "5.20.0", + "@algolia/requester-browser-xhr": "5.20.0", + "@algolia/requester-fetch": "5.20.0", + "@algolia/requester-node-http": "5.20.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@algolia/cache-common": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.23.3.tgz", - "integrity": "sha512-h9XcNI6lxYStaw32pHpB1TMm0RuxphF+Ik4o7tcQiodEdpKK+wKufY6QXtba7t3k8eseirEMVB83uFFF3Nu54A==", - "dev": true - }, - "node_modules/@algolia/cache-in-memory": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.23.3.tgz", - "integrity": "sha512-yvpbuUXg/+0rbcagxNT7un0eo3czx2Uf0y4eiR4z4SD7SiptwYTpbuS0IHxcLHG3lq22ukx1T6Kjtk/rT+mqNg==", - "dev": true, + "node_modules/@algolia/client-analytics": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.20.0.tgz", + "integrity": "sha512-CIT9ni0+5sYwqehw+t5cesjho3ugKQjPVy/iPiJvtJX4g8Cdb6je6SPt2uX72cf2ISiXCAX9U3cY0nN0efnRDw==", + "license": "MIT", "dependencies": { - "@algolia/cache-common": "4.23.3" + "@algolia/client-common": "5.20.0", + "@algolia/requester-browser-xhr": "5.20.0", + "@algolia/requester-fetch": "5.20.0", + "@algolia/requester-node-http": "5.20.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@algolia/client-account": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.23.3.tgz", - "integrity": "sha512-hpa6S5d7iQmretHHF40QGq6hz0anWEHGlULcTIT9tbUssWUriN9AUXIFQ8Ei4w9azD0hc1rUok9/DeQQobhQMA==", - "dev": true, - "dependencies": { - "@algolia/client-common": "4.23.3", - "@algolia/client-search": "4.23.3", - "@algolia/transporter": "4.23.3" + "node_modules/@algolia/client-common": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.20.0.tgz", + "integrity": "sha512-iSTFT3IU8KNpbAHcBUJw2HUrPnMXeXLyGajmCL7gIzWOsYM4GabZDHXOFx93WGiXMti1dymz8k8R+bfHv1YZmA==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@algolia/client-analytics": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.23.3.tgz", - "integrity": "sha512-LBsEARGS9cj8VkTAVEZphjxTjMVCci+zIIiRhpFun9jGDUlS1XmhCW7CTrnaWeIuCQS/2iPyRqSy1nXPjcBLRA==", - "dev": true, + "node_modules/@algolia/client-insights": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.20.0.tgz", + "integrity": "sha512-w9RIojD45z1csvW1vZmAko82fqE/Dm+Ovsy2ElTsjFDB0HMAiLh2FO86hMHbEXDPz6GhHKgGNmBRiRP8dDPgJg==", + "license": "MIT", "dependencies": { - "@algolia/client-common": "4.23.3", - "@algolia/client-search": "4.23.3", - "@algolia/requester-common": "4.23.3", - "@algolia/transporter": "4.23.3" + "@algolia/client-common": "5.20.0", + "@algolia/requester-browser-xhr": "5.20.0", + "@algolia/requester-fetch": "5.20.0", + "@algolia/requester-node-http": "5.20.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@algolia/client-common": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.23.3.tgz", - "integrity": "sha512-l6EiPxdAlg8CYhroqS5ybfIczsGUIAC47slLPOMDeKSVXYG1n0qGiz4RjAHLw2aD0xzh2EXZ7aRguPfz7UKDKw==", - "dev": true, + "node_modules/@algolia/client-personalization": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.20.0.tgz", + "integrity": "sha512-p/hftHhrbiHaEcxubYOzqVV4gUqYWLpTwK+nl2xN3eTrSW9SNuFlAvUBFqPXSVBqc6J5XL9dNKn3y8OA1KElSQ==", + "license": "MIT", "dependencies": { - "@algolia/requester-common": "4.23.3", - "@algolia/transporter": "4.23.3" + "@algolia/client-common": "5.20.0", + "@algolia/requester-browser-xhr": "5.20.0", + "@algolia/requester-fetch": "5.20.0", + "@algolia/requester-node-http": "5.20.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@algolia/client-personalization": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.23.3.tgz", - "integrity": "sha512-3E3yF3Ocr1tB/xOZiuC3doHQBQ2zu2MPTYZ0d4lpfWads2WTKG7ZzmGnsHmm63RflvDeLK/UVx7j2b3QuwKQ2g==", - "dev": true, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.20.0.tgz", + "integrity": "sha512-m4aAuis5vZi7P4gTfiEs6YPrk/9hNTESj3gEmGFgfJw3hO2ubdS4jSId1URd6dGdt0ax2QuapXufcrN58hPUcw==", + "license": "MIT", "dependencies": { - "@algolia/client-common": "4.23.3", - "@algolia/requester-common": "4.23.3", - "@algolia/transporter": "4.23.3" + "@algolia/client-common": "5.20.0", + "@algolia/requester-browser-xhr": "5.20.0", + "@algolia/requester-fetch": "5.20.0", + "@algolia/requester-node-http": "5.20.0" + }, + "engines": { + "node": ">= 14.0.0" } }, "node_modules/@algolia/client-search": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.23.3.tgz", - "integrity": "sha512-P4VAKFHqU0wx9O+q29Q8YVuaowaZ5EM77rxfmGnkHUJggh28useXQdopokgwMeYw2XUht49WX5RcTQ40rZIabw==", - "dev": true, + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.20.0.tgz", + "integrity": "sha512-KL1zWTzrlN4MSiaK1ea560iCA/UewMbS4ZsLQRPoDTWyrbDKVbztkPwwv764LAqgXk0fvkNZvJ3IelcK7DqhjQ==", + "license": "MIT", "dependencies": { - "@algolia/client-common": "4.23.3", - "@algolia/requester-common": "4.23.3", - "@algolia/transporter": "4.23.3" + "@algolia/client-common": "5.20.0", + "@algolia/requester-browser-xhr": "5.20.0", + "@algolia/requester-fetch": "5.20.0", + "@algolia/requester-node-http": "5.20.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@algolia/logger-common": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.23.3.tgz", - "integrity": "sha512-y9kBtmJwiZ9ZZ+1Ek66P0M68mHQzKRxkW5kAAXYN/rdzgDN0d2COsViEFufxJ0pb45K4FRcfC7+33YB4BLrZ+g==", - "dev": true + "node_modules/@algolia/ingestion": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.20.0.tgz", + "integrity": "sha512-shj2lTdzl9un4XJblrgqg54DoK6JeKFO8K8qInMu4XhE2JuB8De6PUuXAQwiRigZupbI0xq8aM0LKdc9+qiLQA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.20.0", + "@algolia/requester-browser-xhr": "5.20.0", + "@algolia/requester-fetch": "5.20.0", + "@algolia/requester-node-http": "5.20.0" + }, + "engines": { + "node": ">= 14.0.0" + } }, - "node_modules/@algolia/logger-console": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.23.3.tgz", - "integrity": "sha512-8xoiseoWDKuCVnWP8jHthgaeobDLolh00KJAdMe9XPrWPuf1by732jSpgy2BlsLTaT9m32pHI8CRfrOqQzHv3A==", - "dev": true, + "node_modules/@algolia/monitoring": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.20.0.tgz", + "integrity": "sha512-aF9blPwOhKtWvkjyyXh9P5peqmhCA1XxLBRgItT+K6pbT0q4hBDQrCid+pQZJYy4HFUKjB/NDDwyzFhj/rwKhw==", + "license": "MIT", "dependencies": { - "@algolia/logger-common": "4.23.3" + "@algolia/client-common": "5.20.0", + "@algolia/requester-browser-xhr": "5.20.0", + "@algolia/requester-fetch": "5.20.0", + "@algolia/requester-node-http": "5.20.0" + }, + "engines": { + "node": ">= 14.0.0" } }, "node_modules/@algolia/recommend": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-4.23.3.tgz", - "integrity": "sha512-9fK4nXZF0bFkdcLBRDexsnGzVmu4TSYZqxdpgBW2tEyfuSSY54D4qSRkLmNkrrz4YFvdh2GM1gA8vSsnZPR73w==", - "dev": true, + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.20.0.tgz", + "integrity": "sha512-T6B/WPdZR3b89/F9Vvk6QCbt/wrLAtrGoL8z4qPXDFApQ8MuTFWbleN/4rHn6APWO3ps+BUePIEbue2rY5MlRw==", + "license": "MIT", "dependencies": { - "@algolia/cache-browser-local-storage": "4.23.3", - "@algolia/cache-common": "4.23.3", - "@algolia/cache-in-memory": "4.23.3", - "@algolia/client-common": "4.23.3", - "@algolia/client-search": "4.23.3", - "@algolia/logger-common": "4.23.3", - "@algolia/logger-console": "4.23.3", - "@algolia/requester-browser-xhr": "4.23.3", - "@algolia/requester-common": "4.23.3", - "@algolia/requester-node-http": "4.23.3", - "@algolia/transporter": "4.23.3" + "@algolia/client-common": "5.20.0", + "@algolia/requester-browser-xhr": "5.20.0", + "@algolia/requester-fetch": "5.20.0", + "@algolia/requester-node-http": "5.20.0" + }, + "engines": { + "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-browser-xhr": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.23.3.tgz", - "integrity": "sha512-jDWGIQ96BhXbmONAQsasIpTYWslyjkiGu0Quydjlowe+ciqySpiDUrJHERIRfELE5+wFc7hc1Q5hqjGoV7yghw==", - "dev": true, + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.20.0.tgz", + "integrity": "sha512-t6//lXsq8E85JMenHrI6mhViipUT5riNhEfCcvtRsTV+KIBpC6Od18eK864dmBhoc5MubM0f+sGpKOqJIlBSCg==", + "license": "MIT", "dependencies": { - "@algolia/requester-common": "4.23.3" + "@algolia/client-common": "5.20.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@algolia/requester-common": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.23.3.tgz", - "integrity": "sha512-xloIdr/bedtYEGcXCiF2muajyvRhwop4cMZo+K2qzNht0CMzlRkm8YsDdj5IaBhshqfgmBb3rTg4sL4/PpvLYw==", - "dev": true + "node_modules/@algolia/requester-fetch": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.20.0.tgz", + "integrity": "sha512-FHxYGqRY+6bgjKsK4aUsTAg6xMs2S21elPe4Y50GB0Y041ihvw41Vlwy2QS6K9ldoftX4JvXodbKTcmuQxywdQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.20.0" + }, + "engines": { + "node": ">= 14.0.0" + } }, "node_modules/@algolia/requester-node-http": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.23.3.tgz", - "integrity": "sha512-zgu++8Uj03IWDEJM3fuNl34s746JnZOWn1Uz5taV1dFyJhVM/kTNw9Ik7YJWiUNHJQXcaD8IXD1eCb0nq/aByA==", - "dev": true, + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.20.0.tgz", + "integrity": "sha512-kmtQClq/w3vtPteDSPvaW9SPZL/xrIgMrxZyAgsFwrJk0vJxqyC5/hwHmrCraDnStnGSADnLpBf4SpZnwnkwWw==", + "license": "MIT", "dependencies": { - "@algolia/requester-common": "4.23.3" + "@algolia/client-common": "5.20.0" + }, + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@algolia/transporter": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.23.3.tgz", - "integrity": "sha512-Wjl5gttqnf/gQKJA+dafnD0Y6Yw97yvfY8R9h0dQltX1GXTgNs1zWgvtWW0tHl1EgMdhAyw189uWiZMnL3QebQ==", - "dev": true, - "dependencies": { - "@algolia/cache-common": "4.23.3", - "@algolia/logger-common": "4.23.3", - "@algolia/requester-common": "4.23.3" + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", - "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", - "dev": true, + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz", + "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.7" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -218,6 +272,19 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/types": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz", + "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@braintree/sanitize-url": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", @@ -225,31 +292,31 @@ "dev": true }, "node_modules/@docsearch/css": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.6.0.tgz", - "integrity": "sha512-+sbxb71sWre+PwDK7X2T8+bhS6clcVMLwBPznX45Qu6opJcgRjAp7gYSDzVFp187J+feSj5dNBN1mJoi6ckkUQ==", - "dev": true + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.3.tgz", + "integrity": "sha512-1nELpMV40JDLJ6rpVVFX48R1jsBFIQ6RnEQDsLFGmzOjPWTOMlZqUcXcvRx8VmYV/TqnS1l784Ofz+ZEb+wEOQ==", + "license": "MIT" }, "node_modules/@docsearch/js": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.6.0.tgz", - "integrity": "sha512-QujhqINEElrkIfKwyyyTfbsfMAYCkylInLYMRqHy7PHc8xTBQCow73tlo/Kc7oIwBrCLf0P3YhjlOeV4v8hevQ==", - "dev": true, + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.8.3.tgz", + "integrity": "sha512-CQsX1zeoPJIWxN3IGoDSWOqzRc0JsOE9Bclegf9llwjYN2rzzJF93zagGcT3uI3tF31oCqTuUOVGW/mVFb7arw==", + "license": "MIT", "dependencies": { - "@docsearch/react": "3.6.0", + "@docsearch/react": "3.8.3", "preact": "^10.0.0" } }, "node_modules/@docsearch/react": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.6.0.tgz", - "integrity": "sha512-HUFut4ztcVNmqy9gp/wxNbC7pTOHhgVVkHVGCACTuLhUKUhKAF9KYHJtMiLUJxEqiFLQiuri1fWF8zqwM/cu1w==", - "dev": true, + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.3.tgz", + "integrity": "sha512-6UNrg88K7lJWmuS6zFPL/xgL+n326qXqZ7Ybyy4E8P/6Rcblk3GE8RXxeol4Pd5pFpKMhOhBhzABKKwHtbJCIg==", + "license": "MIT", "dependencies": { - "@algolia/autocomplete-core": "1.9.3", - "@algolia/autocomplete-preset-algolia": "1.9.3", - "@docsearch/css": "3.6.0", - "algoliasearch": "^4.19.1" + "@algolia/autocomplete-core": "1.17.9", + "@algolia/autocomplete-preset-algolia": "1.17.9", + "@docsearch/css": "3.8.3", + "algoliasearch": "^5.14.2" }, "peerDependencies": { "@types/react": ">= 16.8.0 < 19.0.0", @@ -279,7 +346,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "aix" @@ -295,7 +361,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "android" @@ -311,7 +376,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "android" @@ -327,7 +391,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "android" @@ -343,7 +406,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -359,7 +421,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -375,7 +436,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -391,7 +451,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -407,7 +466,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -423,7 +481,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -439,7 +496,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "linux" @@ -455,7 +511,6 @@ "cpu": [ "loong64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -471,7 +526,6 @@ "cpu": [ "mips64el" ], - "dev": true, "optional": true, "os": [ "linux" @@ -487,7 +541,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -503,7 +556,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -519,7 +571,6 @@ "cpu": [ "s390x" ], - "dev": true, "optional": true, "os": [ "linux" @@ -535,7 +586,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -551,7 +601,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "netbsd" @@ -567,7 +616,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "openbsd" @@ -583,7 +631,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "sunos" @@ -599,7 +646,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -615,7 +661,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -631,7 +676,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -640,11 +684,26 @@ "node": ">=12" } }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.21", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.21.tgz", + "integrity": "sha512-aqbIuVshMZ2fNEhm25//9DoKudboXF3CpoEQJJlHl9gVSVNOTr4cgaCIZvgSEYmys2HHEfmhcpoZIhoEFZS8SQ==", + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" }, "node_modules/@mermaid-js/mermaid-mindmap": { "version": "9.3.0", @@ -669,7 +728,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -683,7 +741,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -697,7 +754,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -711,7 +767,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -725,7 +780,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -739,7 +793,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -753,7 +806,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -767,7 +819,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -781,7 +832,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -795,7 +845,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -809,7 +858,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -823,7 +871,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -837,7 +884,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -851,7 +897,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -865,7 +910,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -879,7 +923,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -887,20 +930,84 @@ ] }, "node_modules/@shikijs/core": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.3.0.tgz", - "integrity": "sha512-7fedsBfuILDTBmrYZNFI8B6ATTxhQAasUHllHmjvSZPnoq4bULWoTpHwmuQvZ8Aq03/tAa2IGo6RXqWtHdWaCA==", - "dev": true + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.29.1.tgz", + "integrity": "sha512-Mo1gGGkuOYjDu5H8YwzmOuly9vNr8KDVkqj9xiKhhhFS8jisAtDSEWB9hzqRHLVQgFdA310e8XRJcW4tYhRB2A==", + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "1.29.1", + "@shikijs/engine-oniguruma": "1.29.1", + "@shikijs/types": "1.29.1", + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.29.1.tgz", + "integrity": "sha512-Hpi8k9x77rCQ7F/7zxIOUruNkNidMyBnP5qAGbLFqg4kRrg1HZhkB8btib5EXbQWTtLb5gBHOdBwshk20njD7Q==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.1", + "@shikijs/vscode-textmate": "^10.0.1", + "oniguruma-to-es": "^2.2.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.1.tgz", + "integrity": "sha512-gSt2WhLNgEeLstcweQOSp+C+MhOpTsgdNXRqr3zP6M+BUBZ8Md9OU2BYwUYsALBxHza7hwaIWtFHjQ/aOOychw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.1", + "@shikijs/vscode-textmate": "^10.0.1" + } + }, + "node_modules/@shikijs/langs": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-1.29.1.tgz", + "integrity": "sha512-iERn4HlyuT044/FgrvLOaZgKVKf3PozjKjyV/RZ5GnlyYEAZFcgwHGkYboeBv2IybQG1KVS/e7VGgiAU4JY2Gw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.1" + } + }, + "node_modules/@shikijs/themes": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-1.29.1.tgz", + "integrity": "sha512-lb11zf72Vc9uxkl+aec2oW1HVTHJ2LtgZgumb4Rr6By3y/96VmlU44bkxEb8WBWH3RUtbqAJEN0jljD9cF7H7g==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.29.1" + } }, "node_modules/@shikijs/transformers": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-1.3.0.tgz", - "integrity": "sha512-3mlpg2I9CjhjE96dEWQOGeCWoPcyTov3s4aAsHmgvnTHa8MBknEnCQy8/xivJPSpD+olqOqIEoHnLfbNJK29AA==", - "dev": true, + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-1.29.1.tgz", + "integrity": "sha512-jVzJhriZ0t9y8TvsV4AzBm74BCLUoK6Bf41aIjJkZc1hKeL0PQtsNL096b1AxgZRwJwTfQalWZ+jBkRAuqVMPw==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "1.29.1", + "@shikijs/types": "1.29.1" + } + }, + "node_modules/@shikijs/types": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.29.1.tgz", + "integrity": "sha512-aBqAuhYRp5vSir3Pc9+QPu9WESBOjUo03ao0IHLC4TyTioSsp/SkbAZSrIH4ghYYC1T1KTEpRSBa83bas4RnPA==", + "license": "MIT", "dependencies": { - "shiki": "1.3.0" + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4" } }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.1.tgz", + "integrity": "sha512-fTIQwLF+Qhuws31iw7Ncl1R3HUDtGwIipiJ9iU+UsDUwMhegFcQKQHd51nZjb7CArq0MvON8rbgCGQYWHUKAdg==", + "license": "MIT" + }, "node_modules/@types/d3-scale": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", @@ -934,23 +1041,37 @@ "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } }, "node_modules/@types/linkify-it": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", - "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==", - "dev": true + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.14.tgz", + "integrity": "sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==", + "license": "MIT" }, "node_modules/@types/markdown-it": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.0.1.tgz", - "integrity": "sha512-6WfOG3jXR78DW8L5cTYCVVGAsIFZskRHCDo5tbqa+qtKVt4oDRVH7hyIWu1SpDQJlmIoEivNQZ5h+AGAOrgOtQ==", - "dev": true, + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", "dependencies": { - "@types/linkify-it": "*", - "@types/mdurl": "*" + "@types/linkify-it": "^5", + "@types/mdurl": "^2" } }, "node_modules/@types/mdast": { @@ -963,10 +1084,10 @@ } }, "node_modules/@types/mdurl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz", - "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT" }, "node_modules/@types/ms": { "version": "0.7.34", @@ -977,182 +1098,187 @@ "node_modules/@types/unist": { "version": "2.0.10", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", - "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", - "dev": true + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" }, "node_modules/@types/web-bluetooth": { "version": "0.0.20", "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", - "dev": true + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" }, "node_modules/@vitejs/plugin-vue": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz", - "integrity": "sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==", - "dev": true, + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz", + "integrity": "sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==", + "license": "MIT", "engines": { "node": "^18.0.0 || >=20.0.0" }, "peerDependencies": { - "vite": "^5.0.0", + "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, "node_modules/@vue/compiler-core": { - "version": "3.4.25", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.25.tgz", - "integrity": "sha512-Y2pLLopaElgWnMNolgG8w3C5nNUVev80L7hdQ5iIKPtMJvhVpG0zhnBG/g3UajJmZdvW0fktyZTotEHD1Srhbg==", - "dev": true, + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", + "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", + "license": "MIT", "dependencies": { - "@babel/parser": "^7.24.4", - "@vue/shared": "3.4.25", + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.13", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-dom": { - "version": "3.4.25", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.25.tgz", - "integrity": "sha512-Ugz5DusW57+HjllAugLci19NsDK+VyjGvmbB2TXaTcSlQxwL++2PETHx/+Qv6qFwNLzSt7HKepPe4DcTE3pBWg==", - "dev": true, + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", + "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", + "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.4.25", - "@vue/shared": "3.4.25" + "@vue/compiler-core": "3.5.13", + "@vue/shared": "3.5.13" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.4.25", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.25.tgz", - "integrity": "sha512-m7rryuqzIoQpOBZ18wKyq05IwL6qEpZxFZfRxlNYuIPDqywrXQxgUwLXIvoU72gs6cRdY6wHD0WVZIFE4OEaAQ==", - "dev": true, + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", + "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", + "license": "MIT", "dependencies": { - "@babel/parser": "^7.24.4", - "@vue/compiler-core": "3.4.25", - "@vue/compiler-dom": "3.4.25", - "@vue/compiler-ssr": "3.4.25", - "@vue/shared": "3.4.25", + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.13", + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13", "estree-walker": "^2.0.2", - "magic-string": "^0.30.10", - "postcss": "^8.4.38", + "magic-string": "^0.30.11", + "postcss": "^8.4.48", "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.4.25", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.25.tgz", - "integrity": "sha512-H2ohvM/Pf6LelGxDBnfbbXFPyM4NE3hrw0e/EpwuSiYu8c819wx+SVGdJ65p/sFrYDd6OnSDxN1MB2mN07hRSQ==", - "dev": true, + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", + "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", + "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.4.25", - "@vue/shared": "3.4.25" + "@vue/compiler-dom": "3.5.13", + "@vue/shared": "3.5.13" } }, "node_modules/@vue/devtools-api": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.1.2.tgz", - "integrity": "sha512-AKd49cN3BdRgttmX5Aw8op7sx6jmaPwaILcDjaa05UKc1yIHDYST7P8yGZs6zd2pKFETAQz40gmyG7+b57slsQ==", - "dev": true, + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.1.tgz", + "integrity": "sha512-Cexc8GimowoDkJ6eNelOPdYIzsu2mgNyp0scOQ3tiaYSb9iok6LOESSsJvHaI+ib3joRfqRJNLkHFjhNuWA5dg==", + "license": "MIT", "dependencies": { - "@vue/devtools-kit": "^7.1.2" + "@vue/devtools-kit": "^7.7.1" } }, "node_modules/@vue/devtools-kit": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.1.2.tgz", - "integrity": "sha512-UTrcUSOhlI9eXqbPMHUWwA6NQiiPT3onzXsVk2JHGR8ZFFSkzsWTTpHyVA1woG8zvgu2HNV/wigW2k87p858zw==", - "dev": true, + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.1.tgz", + "integrity": "sha512-yhZ4NPnK/tmxGtLNQxmll90jIIXdb2jAhPF76anvn5M/UkZCiLJy28bYgPIACKZ7FCosyKoaope89/RsFJll1w==", + "license": "MIT", "dependencies": { - "@vue/devtools-shared": "^7.1.2", + "@vue/devtools-shared": "^7.7.1", + "birpc": "^0.2.19", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", - "speakingurl": "^14.0.1" - }, - "peerDependencies": { - "vue": "^3.0.0" + "speakingurl": "^14.0.1", + "superjson": "^2.2.1" } }, "node_modules/@vue/devtools-shared": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.1.2.tgz", - "integrity": "sha512-r9cUf93VMhKSsxF2/cBbf6Lm1nRBx+r1pRuji5CiAf3JIPYPOjeEqJ13OuwP1fauYh1tyBFcCxt3eJPvHT59gg==", - "dev": true, + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.1.tgz", + "integrity": "sha512-BtgF7kHq4BHG23Lezc/3W2UhK2ga7a8ohAIAGJMBr4BkxUFzhqntQtCiuL1ijo2ztWnmusymkirgqUrXoQKumA==", + "license": "MIT", "dependencies": { - "rfdc": "^1.3.1" + "rfdc": "^1.4.1" } }, "node_modules/@vue/reactivity": { - "version": "3.4.25", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.25.tgz", - "integrity": "sha512-mKbEtKr1iTxZkAG3vm3BtKHAOhuI4zzsVcN0epDldU/THsrvfXRKzq+lZnjczZGnTdh3ojd86/WrP+u9M51pWQ==", - "dev": true, + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", + "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", + "license": "MIT", "dependencies": { - "@vue/shared": "3.4.25" + "@vue/shared": "3.5.13" } }, "node_modules/@vue/runtime-core": { - "version": "3.4.25", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.25.tgz", - "integrity": "sha512-3qhsTqbEh8BMH3pXf009epCI5E7bKu28fJLi9O6W+ZGt/6xgSfMuGPqa5HRbUxLoehTNp5uWvzCr60KuiRIL0Q==", - "dev": true, + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", + "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", + "license": "MIT", "dependencies": { - "@vue/reactivity": "3.4.25", - "@vue/shared": "3.4.25" + "@vue/reactivity": "3.5.13", + "@vue/shared": "3.5.13" } }, "node_modules/@vue/runtime-dom": { - "version": "3.4.25", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.25.tgz", - "integrity": "sha512-ode0sj77kuwXwSc+2Yhk8JMHZh1sZp9F/51wdBiz3KGaWltbKtdihlJFhQG4H6AY+A06zzeMLkq6qu8uDSsaoA==", - "dev": true, + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", + "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", + "license": "MIT", "dependencies": { - "@vue/runtime-core": "3.4.25", - "@vue/shared": "3.4.25", + "@vue/reactivity": "3.5.13", + "@vue/runtime-core": "3.5.13", + "@vue/shared": "3.5.13", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.4.25", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.25.tgz", - "integrity": "sha512-8VTwq0Zcu3K4dWV0jOwIVINESE/gha3ifYCOKEhxOj6MEl5K5y8J8clQncTcDhKF+9U765nRw4UdUEXvrGhyVQ==", - "dev": true, + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", + "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", + "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.4.25", - "@vue/shared": "3.4.25" + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13" }, "peerDependencies": { - "vue": "3.4.25" + "vue": "3.5.13" } }, "node_modules/@vue/shared": { - "version": "3.4.25", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.25.tgz", - "integrity": "sha512-k0yappJ77g2+KNrIaF0FFnzwLvUBLUYr8VOwz+/6vLsmItFp51AcxLL7Ey3iPd7BIRyWPOcqUjMnm7OkahXllA==", - "dev": true + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", + "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", + "license": "MIT" }, "node_modules/@vueuse/core": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.9.0.tgz", - "integrity": "sha512-/1vjTol8SXnx6xewDEKfS0Ra//ncg4Hb0DaZiwKf7drgfMsKFExQ+FnnENcN6efPen+1kIzhLQoGSy0eDUVOMg==", - "dev": true, + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.3.0.tgz", + "integrity": "sha512-7OC4Rl1f9G8IT6rUfi9JrKiXy4bfmHhZ5x2Ceojy0jnd3mHNEvV4JaRygH362ror6/NZ+Nl+n13LPzGiPN8cKA==", + "license": "MIT", "dependencies": { "@types/web-bluetooth": "^0.0.20", - "@vueuse/metadata": "10.9.0", - "@vueuse/shared": "10.9.0", - "vue-demi": ">=0.14.7" + "@vueuse/metadata": "11.3.0", + "@vueuse/shared": "11.3.0", + "vue-demi": ">=0.14.10" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@vueuse/core/node_modules/vue-demi": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", - "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", - "dev": true, + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", "hasInstallScript": true, + "license": "MIT", "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" @@ -1174,31 +1300,31 @@ } }, "node_modules/@vueuse/integrations": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-10.9.0.tgz", - "integrity": "sha512-acK+A01AYdWSvL4BZmCoJAcyHJ6EqhmkQEXbQLwev1MY7NBnS+hcEMx/BzVoR9zKI+UqEPMD9u6PsyAuiTRT4Q==", - "dev": true, + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-11.3.0.tgz", + "integrity": "sha512-5fzRl0apQWrDezmobchoiGTkGw238VWESxZHazfhP3RM7pDSiyXy18QbfYkILoYNTd23HPAfQTJpkUc5QbkwTw==", + "license": "MIT", "dependencies": { - "@vueuse/core": "10.9.0", - "@vueuse/shared": "10.9.0", - "vue-demi": ">=0.14.7" + "@vueuse/core": "11.3.0", + "@vueuse/shared": "11.3.0", + "vue-demi": ">=0.14.10" }, "funding": { "url": "https://github.com/sponsors/antfu" }, "peerDependencies": { - "async-validator": "*", - "axios": "*", - "change-case": "*", - "drauu": "*", - "focus-trap": "*", - "fuse.js": "*", - "idb-keyval": "*", - "jwt-decode": "*", - "nprogress": "*", - "qrcode": "*", - "sortablejs": "*", - "universal-cookie": "*" + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7" }, "peerDependenciesMeta": { "async-validator": { @@ -1240,11 +1366,11 @@ } }, "node_modules/@vueuse/integrations/node_modules/vue-demi": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", - "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", - "dev": true, + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", "hasInstallScript": true, + "license": "MIT", "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" @@ -1266,32 +1392,32 @@ } }, "node_modules/@vueuse/metadata": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.9.0.tgz", - "integrity": "sha512-iddNbg3yZM0X7qFY2sAotomgdHK7YJ6sKUvQqbvwnf7TmaVPxS4EJydcNsVejNdS8iWCtDk+fYXr7E32nyTnGA==", - "dev": true, + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.3.0.tgz", + "integrity": "sha512-pwDnDspTqtTo2HwfLw4Rp6yywuuBdYnPYDq+mO38ZYKGebCUQC/nVj/PXSiK9HX5otxLz8Fn7ECPbjiRz2CC3g==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@vueuse/shared": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.9.0.tgz", - "integrity": "sha512-Uud2IWncmAfJvRaFYzv5OHDli+FbOzxiVEQdLCKQKLyhz94PIyFC3CHcH7EDMwIn8NPtD06+PNbC/PiO0LGLtw==", - "dev": true, + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.3.0.tgz", + "integrity": "sha512-P8gSSWQeucH5821ek2mn/ciCk+MS/zoRKqdQIM3bHq6p7GXDAJLmnRRKmF5F65sAVJIfzQlwR3aDzwCn10s8hA==", + "license": "MIT", "dependencies": { - "vue-demi": ">=0.14.7" + "vue-demi": ">=0.14.10" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, "node_modules/@vueuse/shared/node_modules/vue-demi": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz", - "integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==", - "dev": true, + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", "hasInstallScript": true, + "license": "MIT", "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" @@ -1313,26 +1439,46 @@ } }, "node_modules/algoliasearch": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.23.3.tgz", - "integrity": "sha512-Le/3YgNvjW9zxIQMRhUHuhiUjAlKY/zsdZpfq4dlLqg6mEm0nL6yk+7f2hDOtLpxsgE4jSzDmvHL7nXdBp5feg==", - "dev": true, - "dependencies": { - "@algolia/cache-browser-local-storage": "4.23.3", - "@algolia/cache-common": "4.23.3", - "@algolia/cache-in-memory": "4.23.3", - "@algolia/client-account": "4.23.3", - "@algolia/client-analytics": "4.23.3", - "@algolia/client-common": "4.23.3", - "@algolia/client-personalization": "4.23.3", - "@algolia/client-search": "4.23.3", - "@algolia/logger-common": "4.23.3", - "@algolia/logger-console": "4.23.3", - "@algolia/recommend": "4.23.3", - "@algolia/requester-browser-xhr": "4.23.3", - "@algolia/requester-common": "4.23.3", - "@algolia/requester-node-http": "4.23.3", - "@algolia/transporter": "4.23.3" + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.20.0.tgz", + "integrity": "sha512-groO71Fvi5SWpxjI9Ia+chy0QBwT61mg6yxJV27f5YFf+Mw+STT75K6SHySpP8Co5LsCrtsbCH5dJZSRtkSKaQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-abtesting": "5.20.0", + "@algolia/client-analytics": "5.20.0", + "@algolia/client-common": "5.20.0", + "@algolia/client-insights": "5.20.0", + "@algolia/client-personalization": "5.20.0", + "@algolia/client-query-suggestions": "5.20.0", + "@algolia/client-search": "5.20.0", + "@algolia/ingestion": "1.20.0", + "@algolia/monitoring": "1.20.0", + "@algolia/recommend": "5.20.0", + "@algolia/requester-browser-xhr": "5.20.0", + "@algolia/requester-fetch": "5.20.0", + "@algolia/requester-node-http": "5.20.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/birpc": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.19.tgz", + "integrity": "sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/character-entities": { @@ -1345,17 +1491,78 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "engines": { - "node": ">= 10" + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/cose-base": { - "version": "1.0.3", + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/cli-color": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", + "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.64", + "es6-iterator": "^2.0.3", + "memoizee": "^0.4.15", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/cose-base": { + "version": "1.0.3", "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", "dev": true, @@ -1367,7 +1574,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "license": "MIT" }, "node_modules/cytoscape": { "version": "3.29.2", @@ -1420,6 +1627,19 @@ "dev": true, "optional": true }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/d3": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", @@ -1920,11 +2140,23 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "engines": { "node": ">=6" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/diff": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", @@ -1946,11 +2178,17 @@ "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==", "dev": true }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "license": "MIT" + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -1958,11 +2196,62 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -1996,17 +2285,51 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true + "license": "MIT" + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } }, "node_modules/focus-trap": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", - "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", - "dev": true, + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.4.tgz", + "integrity": "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==", + "license": "MIT", "dependencies": { "tabbable": "^6.2.0" } @@ -2015,7 +2338,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -2025,11 +2347,63 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/hast-util-to-html": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.4.tgz", + "integrity": "sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hookable": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", - "dev": true + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, "node_modules/iconv-lite": { "version": "0.6.3", @@ -2052,6 +2426,36 @@ "node": ">=12" } }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/katex": { "version": "0.16.10", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz", @@ -2098,26 +2502,40 @@ "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", "dev": true }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", "dev": true }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "license": "MIT", + "dependencies": { + "es5-ext": "~0.10.2" + } + }, "node_modules/magic-string": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", - "dev": true, + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/mark.js": { "version": "8.11.1", "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", - "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", - "dev": true + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==" }, "node_modules/markdown-it-task-lists": { "version": "2.1.1", @@ -2148,6 +2566,125 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast/node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/mdast-util-to-hast/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-to-hast/node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mdast-util-to-hast/node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/mdast-util-to-hast/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mdast-util-to-hast/node_modules/micromark-util-types": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.1.tgz", + "integrity": "sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/mdast-util-to-string": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", @@ -2161,6 +2698,25 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/mermaid": { "version": "10.9.3", "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.9.3.tgz", @@ -2633,16 +3189,16 @@ ] }, "node_modules/minisearch": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-6.3.0.tgz", - "integrity": "sha512-ihFnidEeU8iXzcVHy74dhkxh/dn8Dc08ERl0xwoMMGqp4+LvRSCgicb+zGqWthVokQKvCSxITlh3P08OzdTYCQ==", - "dev": true + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.1.1.tgz", + "integrity": "sha512-b3YZEYCEH4EdCAtYP7OlDyx7FdPwNzuNwLQ34SfJpM9dlbBZzeXndGavTrC+VCiRWomL21SWfMc6SCKO/U2ZNw==", + "license": "MIT" }, "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "dev": true + "license": "MIT" }, "node_modules/mri": { "version": "1.2.0", @@ -2660,16 +3216,16 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -2677,29 +3233,45 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "license": "ISC" + }, "node_modules/non-layered-tidy-tree-layout": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==", "dev": true }, + "node_modules/oniguruma-to-es": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-2.3.0.tgz", + "integrity": "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==", + "license": "MIT", + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^5.1.1", + "regex-recursion": "^5.1.1" + } + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", - "dev": true + "license": "MIT" }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", - "dev": true + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", - "dev": true, + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", + "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", "funding": [ { "type": "opencollective", @@ -2714,9 +3286,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -2724,20 +3297,55 @@ } }, "node_modules/preact": { - "version": "10.20.2", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.20.2.tgz", - "integrity": "sha512-S1d1ernz3KQ+Y2awUxKakpfOg2CEmJmwOP+6igPx6dgr6pgDvenqYviyokWso2rhHvGtTlWWnJDa7RaPbQerTg==", - "dev": true, + "version": "10.25.4", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.25.4.tgz", + "integrity": "sha512-jLdZDb+Q+odkHJ+MpW/9U5cODzqnB+fy2EiHSZES7ldV5LK7yjlVzTp7R8Xy6W6y75kfK8iWYtFVH7lvjwrCMA==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" } }, + "node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/regex": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/regex/-/regex-5.1.1.tgz", + "integrity": "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-5.1.1.tgz", + "integrity": "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==", + "license": "MIT", + "dependencies": { + "regex": "^5.1.1", + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, "node_modules/rfdc": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", - "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", - "dev": true + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" }, "node_modules/robust-predicates": { "version": "3.0.2", @@ -2749,7 +3357,6 @@ "version": "4.22.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.5" @@ -2806,50 +3413,115 @@ "dev": true }, "node_modules/search-insights": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.13.0.tgz", - "integrity": "sha512-Orrsjf9trHHxFRuo9/rzm0KIWmgzE8RMlZMzuhZOJ01Rnz3D0YBAe+V6473t6/H6c7irs6Lt48brULAiRWb3Vw==", - "dev": true, + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "license": "MIT", "peer": true }, "node_modules/shiki": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.3.0.tgz", - "integrity": "sha512-9aNdQy/etMXctnPzsje1h1XIGm9YfRcSksKOGqZWXA/qP9G18/8fpz5Bjpma8bOgz3tqIpjERAd6/lLjFyzoww==", - "dev": true, + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.29.1.tgz", + "integrity": "sha512-TghWKV9pJTd/N+IgAIVJtr0qZkB7FfFCUrrEJc0aRmZupo3D1OCVRknQWVRVA7AX/M0Ld7QfoAruPzr3CnUJuw==", + "license": "MIT", "dependencies": { - "@shikijs/core": "1.3.0" + "@shikijs/core": "1.29.1", + "@shikijs/engine-javascript": "1.29.1", + "@shikijs/engine-oniguruma": "1.29.1", + "@shikijs/langs": "1.29.1", + "@shikijs/themes": "1.29.1", + "@shikijs/types": "1.29.1", + "@shikijs/vscode-textmate": "^10.0.1", + "@types/hast": "^3.0.4" } }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/speakingurl": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", - "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/stylis": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", "dev": true }, + "node_modules/superjson": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz", + "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==", + "license": "MIT", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tabbable": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", - "dev": true + "license": "MIT" + }, + "node_modules/timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, "node_modules/ts-dedent": { "version": "2.2.0", @@ -2860,6 +3532,50 @@ "node": ">=6.10" } }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "license": "ISC" + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/unist-util-stringify-position": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", @@ -2873,6 +3589,47 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/unist-util-visit/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -2904,11 +3661,64 @@ "node": ">=8" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/vfile-message/node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile/node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/vite": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", - "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", - "dev": true, + "version": "5.4.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", + "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", + "license": "MIT", "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -2964,26 +3774,29 @@ } }, "node_modules/vitepress": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.1.3.tgz", - "integrity": "sha512-hGrIYN0w9IHWs0NQSnlMjKV/v/HLfD+Ywv5QdvCSkiT32mpNOOwUrZjnqZv/JL/WBPpUc94eghTUvmipxw0xrA==", - "dev": true, - "dependencies": { - "@docsearch/css": "^3.6.0", - "@docsearch/js": "^3.6.0", - "@shikijs/core": "^1.3.0", - "@shikijs/transformers": "^1.3.0", - "@types/markdown-it": "^14.0.1", - "@vitejs/plugin-vue": "^5.0.4", - "@vue/devtools-api": "^7.0.27", - "@vueuse/core": "^10.9.0", - "@vueuse/integrations": "^10.9.0", - "focus-trap": "^7.5.4", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.5.0.tgz", + "integrity": "sha512-q4Q/G2zjvynvizdB3/bupdYkCJe2umSAMv9Ju4d92E6/NXJ59z70xB0q5p/4lpRyAwflDsbwy1mLV9Q5+nlB+g==", + "license": "MIT", + "dependencies": { + "@docsearch/css": "^3.6.2", + "@docsearch/js": "^3.6.2", + "@iconify-json/simple-icons": "^1.2.10", + "@shikijs/core": "^1.22.2", + "@shikijs/transformers": "^1.22.2", + "@shikijs/types": "^1.22.2", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^5.1.4", + "@vue/devtools-api": "^7.5.4", + "@vue/shared": "^3.5.12", + "@vueuse/core": "^11.1.0", + "@vueuse/integrations": "^11.1.0", + "focus-trap": "^7.6.0", "mark.js": "8.11.1", - "minisearch": "^6.3.0", - "shiki": "^1.3.0", - "vite": "^5.2.9", - "vue": "^3.4.23" + "minisearch": "^7.1.0", + "shiki": "^1.22.2", + "vite": "^5.4.10", + "vue": "^3.5.12" }, "bin": { "vitepress": "bin/vitepress.js" @@ -3014,17 +3827,36 @@ "vitepress": "^1.0.0 || ^1.0.0-alpha" } }, + "node_modules/vitepress-versioning-plugin": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/vitepress-versioning-plugin/-/vitepress-versioning-plugin-1.3.0.tgz", + "integrity": "sha512-36xwYuoRlBtzFnVxB21eIsIWylHzeqKrRmRlTQ3QWY0+Ge0MasW7Y5aJRFs24Az/SoVlB1OQFbIgt+SktCns5w==", + "license": "MIT", + "dependencies": { + "@types/lodash": "^4.17.13", + "cli-color": "^2.0.4", + "json5": "^2.2.3", + "lodash": "^4.17.21", + "vite": "^5.4.11", + "vitepress": "1.5.0", + "vitepress-versioning-plugin": "file:" + } + }, + "node_modules/vitepress-versioning-plugin/node_modules/vitepress-versioning-plugin": { + "resolved": "node_modules/vitepress-versioning-plugin", + "link": true + }, "node_modules/vue": { - "version": "3.4.25", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.25.tgz", - "integrity": "sha512-HWyDqoBHMgav/OKiYA2ZQg+kjfMgLt/T0vg4cbIF7JbXAjDexRf5JRg+PWAfrAkSmTd2I8aPSXtooBFWHB98cg==", - "dev": true, + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", + "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", + "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.4.25", - "@vue/compiler-sfc": "3.4.25", - "@vue/runtime-dom": "3.4.25", - "@vue/server-renderer": "3.4.25", - "@vue/shared": "3.4.25" + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-sfc": "3.5.13", + "@vue/runtime-dom": "3.5.13", + "@vue/server-renderer": "3.5.13", + "@vue/shared": "3.5.13" }, "peerDependencies": { "typescript": "*" @@ -3040,6 +3872,16 @@ "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz", "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==", "dev": true + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index 0b2eb79..343159e 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "docs:preview": "vitepress preview docs" }, "dependencies": { - "markdown-it-task-lists": "^2.1.1" + "markdown-it-task-lists": "^2.1.1", + "vitepress-versioning-plugin": "^1.3.0" } }