Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 8 additions & 37 deletions content/guides/dry/dry-struct/v1.8/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ title: Introduction
pages:
- nested-structs
- recipes
- extensions
- deprecated-features
- virtus
---

`dry-struct` is a gem built on top of `dry-types` which provides virtus-like DSL for defining typed struct classes.
Dry Struct is a gem built on top of [Dry Types](//org_guide/dry/dry-types) which provides a DSL for defining typed struct classes. It allows building immutable data structures with type safety and coersions, making sure that the data is in the shape you want it to be.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generally think we could still have more "why?" here, but currently I'm not sure how.


### Basic Usage

You can define struct objects which will have readers for specified attributes using a simple dsl:
You can define struct objects which will have readers for specified attributes using a simple DSL:

```ruby
require 'dry-struct'
Expand All @@ -34,31 +37,9 @@ user.age # => 21

<sub>Note: An `optional` type means that the value can be nil, not the key in the hash can be skipped.</sub>

### Value

:warning: `Dry::Struct::Value` is deprecated in 1.2.0. Structs are already meant to be immutable, freezing them doesn't add any value (no pun intended) beyond a bad example of defensive programming.

You can define value objects which will behave like structs but will be _deeply frozen_:

```ruby
class Location < Dry::Struct::Value
attribute :lat, Types::Float
attribute :lng, Types::Float
end

loc1 = Location.new(lat: 1.23, lng: 4.56)
loc2 = Location.new(lat: 1.23, lng: 4.56)

loc1.frozen? # true
loc2.frozen? # true

loc1 == loc2
# true
```

### Hash Schemas

`Dry::Struct` out of the box uses [hash schemas](//org_guide/dry/dry-types/hash-schemas) from `dry-types` for processing input hashes. `with_type_transform` and `with_key_transform` are exposed as `transform_types` and `transform_keys`:
Dry Struct out of the box uses [hash schemas](//org_guide/dry/dry-types/hash-schemas) from Dry Types for processing input hashes. `with_type_transform` and `with_key_transform` are exposed as `transform_types` and `transform_keys`:

```ruby
class User < Dry::Struct
Expand All @@ -85,16 +66,6 @@ class User < SymbolizeStruct
end
```

### Validating data with dry-struct

Please don't. Structs are meant to work with valid input, it cannot generate error messages good enough for displaying them for a user etc. Use [`dry-validation`](//org_guide/dry/dry-validation) for validating incoming data and then pass its output to structs.

### Differences between dry-struct and virtus

`dry-struct` look somewhat similar to Virtus but there are few significant differences:
### Validating data with Dry Struct

- Structs don't provide attribute writers and are meant to be used as "data objects" exclusively
- Handling of attribute values is provided by standalone type objects from `dry-types`, which gives you way more powerful features
- Handling of attribute hashes is provided by standalone hash schemas from `dry-types`, which means there are different types of constructors in `dry-struct`
- Structs are not designed as swiss-army knives, specific constructor types are used depending on the use case
- Struct classes quack like `dry-types`, which means you can use them in hash schemas, as array members or sum them
Please don't. Structs are meant to work with valid input, it cannot generate error messages good enough for displaying them for a user etc. Use [Dry Validation](//org_guide/dry/dry-validation) for validating incoming data and then pass its output to structs.
25 changes: 25 additions & 0 deletions content/guides/dry/dry-struct/v1.8/deprecated-features.md

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just saw @cllns's comment in dry-rb/dry-struct#203 about un-deprecating Value, so not sure how to go about this. Perhaps it would be good to make a decision in either way.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
title: Deprecated Features
---

## Value

:warning: `Dry::Struct::Value` is deprecated in 1.2.0. Structs are already meant to be immutable, freezing them doesn't add any value (no pun intended) beyond a bad example of defensive programming.

You can define value objects which will behave like structs but will be _deeply frozen_:

```ruby
class Location < Dry::Struct::Value
attribute :lat, Types::Float
attribute :lng, Types::Float
end

loc1 = Location.new(lat: 1.23, lng: 4.56)
loc2 = Location.new(lat: 1.23, lng: 4.56)

loc1.frozen? # true
loc2.frozen? # true

loc1 == loc2
# true
```
30 changes: 30 additions & 0 deletions content/guides/dry/dry-struct/v1.8/extensions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
title: Extensions
---

Dry Struct provides one extension.

## super_diff

With [super_diff](https://github.com/splitwise/super_diff) extension you can get nicer diffs in failed expectations in RSpec.

```
expected: #<User name="Jane" age=22>
got: #<User name="Jane" age=21>

#<User {
name: "Jane",
- age: 22
+ age: 21
}>
```

To use it, make sure you have `super_diff` in your Gemfile and enable the extension in your `spec_helper.rb`:

```ruby
# Gemfile
gem 'super_diff', group: :test

# spec_helper.rb
Dry::Struct.load_extensions(:super_diff)
```
21 changes: 21 additions & 0 deletions content/guides/dry/dry-struct/v1.8/nested-structs.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,24 @@ end
User::Address
# => User::Address
```

### Optional nested structs

If you have a `User` class defined like above and you will try to instntiate it without `address`, it will raise an exception.

```ruby
User.new(name: "Jane", address: nil)
#=> [User.new] nil (NilClass) has invalid type for :address violates constraints ([User::Address.new] :city is missing in Hash input failed) (Dry::Struct::Error)
```

You can make it optional by setting the type of address as `Dry::Struct.optional`:

```ruby
class User < Dry::Struct
attribute :name, Types::String
attribute :address, Dry::Struct.optional do
attribute :city, Types::String
attribute :street, Types::String
end
end
```
22 changes: 19 additions & 3 deletions content/guides/dry/dry-struct/v1.8/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ title: Recipes

### Symbolize input keys

By default, Dry Struct expects input keys to be symbols. If you try to pass a hash with string keys, it will raise an exception.

```ruby
User.new('name' => 'Jane')
#=> [User.new] :name is missing in Hash input (Dry::Struct::Error)
```

However, you can opt in for a more permissive behavior, where noth kinds of keys are accepted.

```ruby
require 'dry-struct'

Expand Down Expand Up @@ -95,7 +104,7 @@ User.new(name: 'Jane', age: nil)

### Creating a custom struct class

You can combine examples from this page to create a custom-purposed base struct class and the reuse it your application or gem
If you want all of your struct to share some of the modified behavior menioned above, you can create a custom-purposed base struct class and the reuse it your application or gem

```ruby
class MyStruct < Dry::Struct
Expand All @@ -116,9 +125,16 @@ class MyStruct < Dry::Struct
end
end
end

class User < MyStruct
attribute :name, Types::String.default("John Doe")
end

User.new('name' => nil)
#=> #<User name="John Doe">
```

### Set default value for a nested hash
### Set default value for a nested struct

```ruby
class Foo < Dry::Struct
Expand Down Expand Up @@ -157,7 +173,7 @@ end
User.new(name: 'Quispe', city: 'La Paz', country: 'Bolivia')
```

Composition can happen within a nested attribute:
Composition can happen within a nested attribute too:

```ruby
class User < Dry::Struct
Expand Down
15 changes: 15 additions & 0 deletions content/guides/dry/dry-struct/v1.8/virtus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
title: Virtus Legacy
---

Dry Struct is a successor of earlier project called [Virtus](https://github.com/solnic/virtus). Virtus gained relative popularity in Ruby world with 3.7k stars on Github and over 125M downloads on RubyGems (stats for April 2026). It was officially discontinued in 2021 in favor of Dry ecosystem.

### Differences between Dry Struct and Virtus

Dry Struct` look somewhat similar to Virtus but there are few significant differences:

- Structs don't provide attribute writers and are meant to be used as "data objects" exclusively
- Handling of attribute values is provided by standalone type objects from `dry-types`, which gives you way more powerful features
- Handling of attribute hashes is provided by standalone hash schemas from `dry-types`, which means there are different types of constructors in `dry-struct`
- Structs are not designed as swiss-army knives, specific constructor types are used depending on the use case
- Struct classes quack like `dry-types`, which means you can use them in hash schemas, as array members or sum them
Loading