serviced 0.1.0 → 0.1.1
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 +8 -6
- data/README.md +21 -0
- data/lib/serviced/typed.rb +18 -2
- data/lib/serviced/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3643886bd1dac4dfc9750a4a6fbf08f672f6c9b33905b4cd19bedc482a527bc9
|
|
4
|
+
data.tar.gz: d7d95804f42d9a786a0a2647fe46a84a0c9e4ed9cfac39a598f2a9db5ae5a469
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6404482276c8e3057a978f94c9a46580258b2c981968f05ee031f09bb5e870edc0bce494301fbd2938bbff42671c3aa3b9e28f018d72c80fc7db9b349b7c9d29
|
|
7
|
+
data.tar.gz: 6aab0347f47d2fd19edaf017990c31e329e990999ea1047f03a1d9229121a5e60b9a1993153e9e77cdb07164e5b8475f2240723b7ef84c31f7e94995a16a7115
|
data/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
## [0.1.
|
|
10
|
+
## [0.1.1]
|
|
11
11
|
|
|
12
12
|
### Added
|
|
13
13
|
|
|
@@ -25,10 +25,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
25
25
|
`quote_column`, `sanitize`, `count_of`), and raises `Serviced::InvalidQuery`
|
|
26
26
|
on invalid input.
|
|
27
27
|
- `Serviced::Typed`: shared concern providing typed, immutable, validatable
|
|
28
|
-
attributes; included by both `Serviced::Service` and `Serviced::Query`.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
attributes; included by both `Serviced::Service` and `Serviced::Query`. An
|
|
29
|
+
attribute type can be an ActiveModel type symbol (coerced) or a class (the
|
|
30
|
+
value must be an instance of it: ActiveRecord records, POROs, anything;
|
|
31
|
+
subclasses count, `nil` is allowed). Inputs are isolated by default:
|
|
32
|
+
value-like data (arrays, hashes, sets, strings) is captured as a deep-frozen
|
|
33
|
+
snapshot at construction, while objects with identity (records) are shared by
|
|
34
|
+
reference. Opt out per attribute with `isolate: false`.
|
|
33
35
|
- `Serviced.configure` with a pluggable `transaction_handler` (defaults to
|
|
34
36
|
`ActiveRecord::Base.transaction` when ActiveRecord is available).
|
data/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
+
<a href="https://github.com/lbernardelli/serviced/actions/workflows/ci.yml"><img src="https://github.com/lbernardelli/serviced/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
6
7
|
<a href="https://rubygems.org/gems/serviced"><img src="https://img.shields.io/gem/v/serviced?color=CC342D" alt="Gem Version"></a>
|
|
7
8
|
<img src="https://img.shields.io/badge/ruby-%3E%3D%203.1-CC342D" alt="Ruby >= 3.1">
|
|
8
9
|
<a href="LICENSE.txt"><img src="https://img.shields.io/badge/license-MIT-blue" alt="License: MIT"></a>
|
|
@@ -162,6 +163,26 @@ report.limit # => 50 (default)
|
|
|
162
163
|
|
|
163
164
|
Unknown keys are ignored, which is what lets a service drop cleanly into a [flow](#flows) without matching the exact shape of the context.
|
|
164
165
|
|
|
166
|
+
Types are not just scalars. A symbol coerces the value through an ActiveModel type; a class requires the value to be an instance of it, so you can type an attribute as an ActiveRecord record, a plain object, or anything:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
class ChargeSubscription < Serviced::Service
|
|
170
|
+
attribute :account, Account # must be an Account (record, PORO, subclass), or nil
|
|
171
|
+
attribute :coupon, Coupon # must be a Coupon, or nil
|
|
172
|
+
attribute :cents, :integer # coerced to Integer
|
|
173
|
+
|
|
174
|
+
def call
|
|
175
|
+
# account and coupon are guaranteed to be the right type here
|
|
176
|
+
success(account.charge!(cents, coupon:))
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
ChargeSubscription.call(account: "oops", cents: 500)
|
|
181
|
+
# => Failure(:invalid), errors: ["Account must be an instance of Account"]
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
The check allows `nil` (make it required with `validates :account, presence: true`) and accepts subclasses. The object is shared by reference, not coerced, so a record stays the same record.
|
|
185
|
+
|
|
165
186
|
### Immutability that actually holds
|
|
166
187
|
|
|
167
188
|
Inputs are read-only. There is no writer to reassign them:
|
data/lib/serviced/typed.rb
CHANGED
|
@@ -73,17 +73,22 @@ module Serviced
|
|
|
73
73
|
# ActiveModel::Attributes.attribute, plus +isolate:+.
|
|
74
74
|
#
|
|
75
75
|
# @param name [Symbol] attribute name
|
|
76
|
-
# @param type [Symbol, ActiveModel::Type::Value, nil]
|
|
76
|
+
# @param type [Symbol, ActiveModel::Type::Value, Class, Module, nil] an
|
|
77
|
+
# ActiveModel type symbol/instance (the value is coerced), or a
|
|
78
|
+
# class/module (the value must be an instance of it: records, POROs,
|
|
79
|
+
# anything), or nil for an untyped pass-through
|
|
77
80
|
# @param isolate [Boolean] capture an immutable snapshot of the value at
|
|
78
81
|
# construction (default); pass +false+ to share it by reference
|
|
79
82
|
# @param options [Hash] forwarded to ActiveModel (e.g. +default:+)
|
|
80
83
|
def attribute(name, type = nil, isolate: true, **options)
|
|
81
|
-
if type.
|
|
84
|
+
klass = type if type.is_a?(Module)
|
|
85
|
+
if type.nil? || klass
|
|
82
86
|
super(name, **options)
|
|
83
87
|
else
|
|
84
88
|
super(name, type, **options)
|
|
85
89
|
end
|
|
86
90
|
private(:"#{name}=")
|
|
91
|
+
validate_instance_of(name, klass) if klass
|
|
87
92
|
return unless isolate
|
|
88
93
|
|
|
89
94
|
isolated_attribute_names << name.to_s
|
|
@@ -100,6 +105,17 @@ module Serviced
|
|
|
100
105
|
instance_variable_set(ivar, Serviced.snapshot(super()))
|
|
101
106
|
end
|
|
102
107
|
end
|
|
108
|
+
|
|
109
|
+
# Adds a validation that the attribute holds an instance of +klass+
|
|
110
|
+
# (subclasses count). nil is allowed; require it with +presence: true+.
|
|
111
|
+
def validate_instance_of(name, klass)
|
|
112
|
+
validate do
|
|
113
|
+
value = public_send(name)
|
|
114
|
+
next if value.nil? || value.is_a?(klass)
|
|
115
|
+
|
|
116
|
+
errors.add(name, "must be an instance of #{klass.name || klass.inspect}")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
103
119
|
end
|
|
104
120
|
end
|
|
105
121
|
end
|
data/lib/serviced/version.rb
CHANGED