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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 846cb3be063fd58cece40a219adb68e678c855e5eada4d0ca5b8bdd97d150804
4
- data.tar.gz: 98095c54b426771b8fa036c5ac2bcdbb4358700d14877875eaab1f3a672f9e07
3
+ metadata.gz: 3643886bd1dac4dfc9750a4a6fbf08f672f6c9b33905b4cd19bedc482a527bc9
4
+ data.tar.gz: d7d95804f42d9a786a0a2647fe46a84a0c9e4ed9cfac39a598f2a9db5ae5a469
5
5
  SHA512:
6
- metadata.gz: 17a0bea0ddf4bd50cb8b3305fea5e074b3c3d5ea1510c5e3ce2945a74f7bea9ebcd24f0aafaa6ba52d1a5bec73dd5495d2ee2b107fd75c8eb5a046f60c57637c
7
- data.tar.gz: d55dc5d0189586d8d719053bfb0fa796090078d42054d7b5b828ae1cad0320af15d9114e4b95806a60e8b798222ffa7ac7ff65dcf86b9615dc83ae2c77c18df4
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.0]
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
- Inputs are isolated by default: value-like data (arrays, hashes, sets,
30
- strings) is captured as a deep-frozen snapshot at construction, while objects
31
- with identity (records) are shared by reference. Opt out per attribute with
32
- `isolate: false`.
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:
@@ -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] the cast type
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.nil?
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Serviced
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: serviced
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leonardo Bernardelli