dry-monads 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +46 -0
- data/dry-monads.gemspec +1 -1
- data/lib/dry/monads/extensions/json.rb +128 -0
- data/lib/dry/monads/extensions.rb +4 -0
- data/lib/dry/monads/maybe.rb +2 -2
- data/lib/dry/monads/validated.rb +1 -1
- data/lib/dry/monads/version.rb +1 -1
- data/lib/dry/monads.rb +0 -1
- metadata +3 -3
- data/lib/json/add/dry/monads/maybe.rb +0 -32
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 74c983b4b86f5bc73455c3f8987760ae5e11d026f87fbcb4741da18051c60e10
|
|
4
|
+
data.tar.gz: 42b72b1ab09dab98fe8c5a399d17d882a2fdcdf45971ec70e29868fbb07dfcba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d7afa3e95783b08fb767e1ad0b4be5aa5e3e3bf096e534884783a419acc62ccbc141fbdf641a673b9a2808b6bf27fdfb2ca64dc7e0360adbaff7f587353b3357
|
|
7
|
+
data.tar.gz: 831f701ceb7e787bab8d4c77f159aeaa1a13c5c387c790e2c50fe78a8c50a9d9d0610893466eb3f06ae73b9cbd31c08fcf45f0a8dc6822d203949abafce23870
|
data/CHANGELOG.md
CHANGED
|
@@ -21,6 +21,52 @@ and this project adheres to [Break Versioning](https://www.taoensso.com/break-ve
|
|
|
21
21
|
|
|
22
22
|
[Unreleased]: https://github.com/dry-rb/dry-monads/compare/v1.10.0...main
|
|
23
23
|
|
|
24
|
+
## [1.11.0] - 2026-09-11
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- New `:json` extension, which builds a `JSON::Coder` that reads and writes monads. It needs the json gem 2.15.0 or later. (@timriley in #209)
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
Dry::Monads.load_extensions(:json)
|
|
32
|
+
|
|
33
|
+
coder = Dry::Monads.json_coder
|
|
34
|
+
coder.dump(Some(3)) # => %({"json_class":"Dry::Monads::Maybe::Some","value":3})
|
|
35
|
+
coder.load(%({"json_class":"Dry::Monads::Maybe::Some","value":3})) # => Some(3)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Give `json_coder` an `as_json:` and an `on_load:` callback to handle your own types in the same coder. Both run after the monad callbacks, and your types and monads can nest inside each other:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
coder = Dry::Monads.json_coder(
|
|
42
|
+
as_json: ->(object, *) {
|
|
43
|
+
object.is_a?(Time) ? {"json_class" => "Time", "value" => object.iso8601} : object
|
|
44
|
+
},
|
|
45
|
+
on_load: ->(value) {
|
|
46
|
+
value.is_a?(Hash) && value["json_class"] == "Time" ? Time.iso8601(value["value"]) : value
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Removed
|
|
52
|
+
|
|
53
|
+
- **Breaking**: removed `json/add/dry/monads/maybe`. The json gem 3.0 removed the whole `json/add` mechanism, so monads can no longer hook into `JSON.dump` and `JSON.load` globally. Use the new `:json` extension instead. (@timriley in #209)
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
# before
|
|
57
|
+
require "json/add/dry/monads/maybe"
|
|
58
|
+
JSON.unsafe_load(JSON.dump(data))
|
|
59
|
+
|
|
60
|
+
# after
|
|
61
|
+
Dry::Monads.load_extensions(:json)
|
|
62
|
+
coder = Dry::Monads.json_coder
|
|
63
|
+
coder.load(coder.dump(data))
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The JSON serialization format is the same as before, so JSON written by the old serializer still loads. One behavior differs: `JSON::Coder` will always raise a `JSON::GeneratorError` when it sees an object with no JSON counterpart raises, instead of falling back to `to_s`.
|
|
67
|
+
|
|
68
|
+
[1.11.0]: https://github.com/dry-rb/dry-monads/compare/v1.10.0...v1.11.0
|
|
69
|
+
|
|
24
70
|
## [1.10.0] - 2026-04-24
|
|
25
71
|
|
|
26
72
|
### Added
|
data/dry-monads.gemspec
CHANGED
|
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
|
|
|
28
28
|
spec.metadata["bug_tracker_uri"] = "https://github.com/dry-rb/dry-monads/issues"
|
|
29
29
|
spec.metadata["funding_uri"] = "https://github.com/sponsors/hanami"
|
|
30
30
|
|
|
31
|
-
spec.required_ruby_version = ">= 3.
|
|
31
|
+
spec.required_ruby_version = ">= 3.3"
|
|
32
32
|
|
|
33
33
|
spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
|
|
34
34
|
spec.add_runtime_dependency "dry-core", "~> 1.1"
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
# Require json 2.15, the first version with everything this extension needs:
|
|
6
|
+
#
|
|
7
|
+
# - json 2.10 has `JSON::Coder`, but ignores the `on_load:` option
|
|
8
|
+
# - json 2.11 through 2.14 never pass the hash key flag to the `as_json` callback
|
|
9
|
+
#
|
|
10
|
+
# Both of these gaps are silent and may lead to incorrect serialization, so refuse to load instead.
|
|
11
|
+
if Gem::Version.new(::JSON::VERSION) < Gem::Version.new("2.15.0")
|
|
12
|
+
raise "The Dry Monads :json extension needs json 2.15.0 or later, but json #{::JSON::VERSION} is loaded"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module Dry
|
|
16
|
+
module Monads
|
|
17
|
+
# Reads and writes monads as JSON via `JSON::Coder`.
|
|
18
|
+
#
|
|
19
|
+
# This replaces the old `json/add/dry/monads/maybe` file, no longer working from json 3.0, which
|
|
20
|
+
# removed the whole `json/add` mechanism, so monads can no longer serialize via `JSON.dump` and
|
|
21
|
+
# `JSON.load` globally. A coder does the same job locally, without patching anything.
|
|
22
|
+
#
|
|
23
|
+
# The wire format remains unchanged, so JSON written by older versions still loads.
|
|
24
|
+
#
|
|
25
|
+
# @example
|
|
26
|
+
# Dry::Monads.load_extensions(:json)
|
|
27
|
+
#
|
|
28
|
+
# coder = Dry::Monads.json_coder
|
|
29
|
+
# coder.dump(Some(3)) # => %({"json_class":"Dry::Monads::Maybe::Some","value":3})
|
|
30
|
+
# coder.load(%({"json_class":"Dry::Monads::Maybe::Some","value":3})) # => Some(3)
|
|
31
|
+
#
|
|
32
|
+
# @api public
|
|
33
|
+
module JSONCoder
|
|
34
|
+
# Key used to tag a serialized monad.
|
|
35
|
+
JSON_CLASS_KEY = "json_class"
|
|
36
|
+
|
|
37
|
+
# Called for every object the generator cannot write natively.
|
|
38
|
+
#
|
|
39
|
+
# The second argument is `true` when the object is a hash key and `false` everywhere else. We
|
|
40
|
+
# do not use it, because a monad never appears as a key, but it must be accepted.
|
|
41
|
+
#
|
|
42
|
+
# @api private
|
|
43
|
+
AS_JSON = lambda { |object, *|
|
|
44
|
+
case object
|
|
45
|
+
when Maybe
|
|
46
|
+
{JSON_CLASS_KEY => object.class.name, "value" => object.none? ? nil : object.value!}
|
|
47
|
+
else
|
|
48
|
+
object
|
|
49
|
+
end
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Called for every value the parser produces, innermost first.
|
|
53
|
+
#
|
|
54
|
+
# @api private
|
|
55
|
+
ON_LOAD = lambda { |value|
|
|
56
|
+
next value unless value.is_a?(::Hash) && value.key?(JSON_CLASS_KEY)
|
|
57
|
+
|
|
58
|
+
case value[JSON_CLASS_KEY]
|
|
59
|
+
when Maybe::Some.name then Maybe::Some.new(value["value"])
|
|
60
|
+
when Maybe::None.name then Maybe::None.instance
|
|
61
|
+
else value
|
|
62
|
+
end
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Returns a coder that reads and writes monads. Unknown objects raise `JSON::GeneratorError`.
|
|
67
|
+
#
|
|
68
|
+
# A `JSON::Coder` is frozen on creation, so you cannot add monad support to an existing coder.
|
|
69
|
+
# Build your coder here instead: provide `as_json:` to write your own types, and `on_load:` to
|
|
70
|
+
# read them back. Both will run after the monad callbacks, and both see every value, so pass
|
|
71
|
+
# through anything you do not recognize.
|
|
72
|
+
#
|
|
73
|
+
# @example a coder that reads and writes monads and times
|
|
74
|
+
# coder = Dry::Monads.json_coder(
|
|
75
|
+
# as_json: ->(object, *) {
|
|
76
|
+
# object.is_a?(Time) ? {"json_class" => "Time", "value" => object.iso8601} : object
|
|
77
|
+
# },
|
|
78
|
+
# on_load: ->(value) {
|
|
79
|
+
# if value.is_a?(Hash) && value["json_class"] == "Time"
|
|
80
|
+
# Time.iso8601(value["value"])
|
|
81
|
+
# else
|
|
82
|
+
# value
|
|
83
|
+
# end
|
|
84
|
+
# }
|
|
85
|
+
# )
|
|
86
|
+
#
|
|
87
|
+
# coder.dump(Some(Time.utc(2026)))
|
|
88
|
+
# # => %({"json_class":"Dry::Monads::Maybe::Some","value":{"json_class":"Time","value":"2026-01-01T00:00:00Z"}})
|
|
89
|
+
#
|
|
90
|
+
# Monads nest inside your types and the other way around, because the generator and the
|
|
91
|
+
# parser walk the whole document and call both callbacks at every step.
|
|
92
|
+
#
|
|
93
|
+
# `as_json:` takes a second argument, which is `true` when the object is a hash key, and `false`
|
|
94
|
+
# everywhere else. Use it if you write a type that can be a key, because for a key you must
|
|
95
|
+
# return a String or a Symbol. Anything else raises `JSON::GeneratorError`:
|
|
96
|
+
#
|
|
97
|
+
# as_json: ->(object, as_key) {
|
|
98
|
+
# next object unless object.is_a?(Time)
|
|
99
|
+
# as_key ? object.iso8601 : {"json_class" => "Time", "value" => object.iso8601}
|
|
100
|
+
# }
|
|
101
|
+
#
|
|
102
|
+
# Even if you don't need this argument, your lambda must accept it (as in `->(object, *)`), or
|
|
103
|
+
# you will see an `ArgumentError` on the first dump.
|
|
104
|
+
#
|
|
105
|
+
# @param as_json [#call, nil] runs on every object the generator cannot write natively, after
|
|
106
|
+
# the monad callback
|
|
107
|
+
# @param on_load [#call, nil] runs on every parsed value, after the monad callback
|
|
108
|
+
# @param options [Hash] passed on to `JSON::Coder.new`
|
|
109
|
+
# @return [JSON::Coder]
|
|
110
|
+
#
|
|
111
|
+
# @api public
|
|
112
|
+
def self.json_coder(as_json: nil, on_load: nil, **options, &block)
|
|
113
|
+
# `JSON::Coder.new` takes its `as_json` callback as a block. We've opted to make it an keyword
|
|
114
|
+
# argument for consistency alongside `on_load:`.
|
|
115
|
+
#
|
|
116
|
+
# A user familiar with `JSON::Coder` may provide a block, so raise an error just in case.
|
|
117
|
+
raise ArgumentError, "pass the as_json callback as `as_json:`, not as a block" if block
|
|
118
|
+
|
|
119
|
+
::JSON::Coder.new(
|
|
120
|
+
on_load: on_load ? JSONCoder::ON_LOAD >> on_load : JSONCoder::ON_LOAD,
|
|
121
|
+
**options
|
|
122
|
+
) do |object, as_key|
|
|
123
|
+
json = JSONCoder::AS_JSON.call(object, as_key)
|
|
124
|
+
as_json ? as_json.call(json, as_key) : json
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
data/lib/dry/monads/maybe.rb
CHANGED
|
@@ -142,9 +142,9 @@ module Dry
|
|
|
142
142
|
def maybe(...) = Maybe.coerce(bind(...))
|
|
143
143
|
|
|
144
144
|
# Accepts a block and runs it against the wrapped value.
|
|
145
|
-
# If the block returns a
|
|
145
|
+
# If the block returns a truthy value the result is self,
|
|
146
146
|
# otherwise None. If no block is given, the value serves
|
|
147
|
-
#
|
|
147
|
+
# as its result.
|
|
148
148
|
#
|
|
149
149
|
# @param with [#call] positional block
|
|
150
150
|
# @param block [Proc] block
|
data/lib/dry/monads/validated.rb
CHANGED
|
@@ -46,7 +46,7 @@ module Dry
|
|
|
46
46
|
def bind(...)
|
|
47
47
|
# See https://typelevel.org/cats/datatypes/validated.html for details on why
|
|
48
48
|
raise NotImplementedError,
|
|
49
|
-
|
|
49
|
+
"Validated is not a monad because it would violate the monad laws"
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
# Valid result
|
data/lib/dry/monads/version.rb
CHANGED
data/lib/dry/monads.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dry-monads
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hanakai team
|
|
@@ -132,6 +132,7 @@ files:
|
|
|
132
132
|
- lib/dry/monads/do/mixin.rb
|
|
133
133
|
- lib/dry/monads/errors.rb
|
|
134
134
|
- lib/dry/monads/extensions.rb
|
|
135
|
+
- lib/dry/monads/extensions/json.rb
|
|
135
136
|
- lib/dry/monads/extensions/pretty_print.rb
|
|
136
137
|
- lib/dry/monads/extensions/rspec.rb
|
|
137
138
|
- lib/dry/monads/extensions/super_diff.rb
|
|
@@ -149,7 +150,6 @@ files:
|
|
|
149
150
|
- lib/dry/monads/unit.rb
|
|
150
151
|
- lib/dry/monads/validated.rb
|
|
151
152
|
- lib/dry/monads/version.rb
|
|
152
|
-
- lib/json/add/dry/monads/maybe.rb
|
|
153
153
|
homepage: https://dry-rb.org/gems/dry-monads
|
|
154
154
|
licenses:
|
|
155
155
|
- MIT
|
|
@@ -165,7 +165,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
165
165
|
requirements:
|
|
166
166
|
- - ">="
|
|
167
167
|
- !ruby/object:Gem::Version
|
|
168
|
-
version: 3.
|
|
168
|
+
version: '3.3'
|
|
169
169
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
170
170
|
requirements:
|
|
171
171
|
- - ">="
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: false
|
|
2
|
-
|
|
3
|
-
require "json" unless defined?(JSON::JSON_LOADED) && JSON::JSON_LOADED
|
|
4
|
-
|
|
5
|
-
require "dry/monads"
|
|
6
|
-
|
|
7
|
-
# Inspired by standard library implementation
|
|
8
|
-
# for Time serialization/deserialization see (json/lib/json/add/time.rb)
|
|
9
|
-
#
|
|
10
|
-
module Dry
|
|
11
|
-
module Monads
|
|
12
|
-
class Maybe
|
|
13
|
-
# Deserializes JSON string by using Dry::Monads::Maybe#lift method
|
|
14
|
-
def self.json_create(serialized)
|
|
15
|
-
coerce(serialized.fetch("value"))
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Returns a hash, that will be turned into a JSON object and represent this
|
|
19
|
-
# object.
|
|
20
|
-
def as_json(*, **)
|
|
21
|
-
{
|
|
22
|
-
JSON.create_id => self.class.name,
|
|
23
|
-
value: none? ? nil : @value
|
|
24
|
-
}
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Stores class name (Dry::Monads::Maybe::Some or Dry::Monads::Maybe::None)
|
|
28
|
-
# with the monad value as JSON string
|
|
29
|
-
def to_json(...) = as_json.to_json(...)
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|