unmagic-enum 0.2.0 → 0.3.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: 5b6ca718c4633bd4b4416c9e5b4f835b04515982c718fe328944eaa8a3e35447
4
- data.tar.gz: 47868614586456e22c218b68157898ca35f927141316e7a8499682f92b2e8768
3
+ metadata.gz: e68600515b09d6a74e37e5f192337e3de577f002995460d10f89e49faba5d952
4
+ data.tar.gz: ed2bbb98d79b193bd2008ef4f74b323b34b7da58e56830543c78e0690173e2f8
5
5
  SHA512:
6
- metadata.gz: 9c13abf83c50bcb036ee3631de10d5ffb14fde58a78e154e4af60574c94d6af1ad89c3424b03f25fa332de852bbd9f7e1f6755cfa728d5378c86118a6ce05516
7
- data.tar.gz: 4f564234de61c4d44c3fd425c29518c99f938f8e9adda553ea1930bbe0332055cec4bea52fb83d8f6ca6bd6ca0134bd426befed056ac7013afd697376a9cc174
6
+ metadata.gz: e0f659e46165feea78f11c05acd4350f6f4880ccb529a59ced02aa81ff0be583d706cfb540bc2227b0789790a83321516a4d8ed6f40e96513f4ab4461bde3854
7
+ data.tar.gz: 6b523a02f77243db8483308ce5e3ed5b8fcb14acbc3d3098f17b7d68c9594acd7c1231a6840f77dcaf6a3649ddfbe44863ab7d03dd84995d3329cbd2bcd7388e
data/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.1] - 2026-08-27
11
+
12
+ ### Fixed
13
+ - `ArrayColumnType#changed_in_place?` compared `ActiveSupport::JSON.encode`'s compact serialization against the database's raw text, so any formatting difference — PostgreSQL's `jsonb` prints `["a", "b"]` with a space after each comma — flagged every multi-element record as changed the moment it loaded (dirtying `changes`/`saved_changes` and rewriting the column on every save). It now compares the deserialized raw value against the cast value, which also stops a stored value the enum no longer recognises (dropped on read) from marking an unmutated record as changed.
14
+
15
+ ## [0.3.0] - 2026-06-12
16
+
17
+ ### Added
18
+ - `column_type(array: true)` for columns holding multiple values of one enum as a JSON array (a `json`/`jsonb` column). Elements get the same treatment as a scalar `column_type`: cast to enum instances, validated eagerly on assignment (honouring `validate:`), serialized to database values. Blank elements are dropped on cast — so the blank entry a check-box collection's auxiliary hidden field submits never reaches the stored array — and unknown stored values are dropped on read.
19
+
10
20
  ## [0.2.0] - 2026-06-09
11
21
 
12
22
  ### Added
@@ -33,6 +43,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
33
43
  - Rails presence support (`blank?`/`present?`) and JSON serialization (`as_json`)
34
44
  - Empty strings treated as `nil`
35
45
 
36
- [Unreleased]: https://github.com/unreasonable-magic/unmagic-enum/compare/v0.1.1...HEAD
46
+ [Unreleased]: https://github.com/unreasonable-magic/unmagic-enum/compare/v0.3.0...HEAD
47
+ [0.3.0]: https://github.com/unreasonable-magic/unmagic-enum/compare/v0.2.0...v0.3.0
48
+ [0.2.0]: https://github.com/unreasonable-magic/unmagic-enum/compare/v0.1.1...v0.2.0
37
49
  [0.1.1]: https://github.com/unreasonable-magic/unmagic-enum/compare/v0.1.0...v0.1.1
38
50
  [0.1.0]: https://github.com/unreasonable-magic/unmagic-enum/releases/tag/v0.1.0
data/README.md CHANGED
@@ -81,6 +81,33 @@ message.state # => Message::State::SENT
81
81
  message.state.sent? # => true
82
82
  ```
83
83
 
84
+ ### Multiple Values (JSON Array Columns)
85
+
86
+ For a `json`/`jsonb` column holding several values of one enum, pass `array: true`:
87
+
88
+ ```ruby
89
+ class Article < ApplicationRecord
90
+ class Topic < Unmagic::Enum
91
+ SCIENCE = new("science")
92
+ POLITICS = new("politics")
93
+ SPORT = new("sport")
94
+ end
95
+
96
+ attribute :topics, Topic.column_type(array: true)
97
+ end
98
+
99
+ article = Article.new(topics: ["science", "sport"])
100
+ article.topics # => [Article::Topic::SCIENCE, Article::Topic::SPORT]
101
+ ```
102
+
103
+ Each element behaves like a scalar `column_type`: cast to an enum instance,
104
+ validated eagerly on assignment (an unknown element raises, unless built with
105
+ `validate: true`), and serialized to its database value. Blank elements are
106
+ dropped on cast — so the blank entry a check-box collection's auxiliary hidden
107
+ field submits never reaches the stored array — and stored values the enum no
108
+ longer recognises are dropped on read. The attribute always reads as an array,
109
+ never `nil`.
110
+
84
111
  ### Key/Value Separation
85
112
 
86
113
  Useful when database values differ from code identifiers:
@@ -71,13 +71,82 @@ module Unmagic
71
71
  end
72
72
  end
73
73
 
74
+ # A column that stores multiple values of one enum, as a JSON array of the
75
+ # enum's database values (a `json`/`jsonb` column). Each element gets the
76
+ # same treatment ColumnType gives a single value: cast to an enum
77
+ # instance, validated eagerly on assignment, serialized to its database
78
+ # value. Blank elements are dropped during cast — so the blank entry a
79
+ # check-box collection's auxiliary hidden field submits never reaches the
80
+ # stored array — and unknown stored values are dropped on read, so the
81
+ # attribute always reads as an array of valid enum instances (never nil).
82
+ class ArrayColumnType < ActiveRecord::Type::Value
83
+ def initialize(enum_class, validate: false)
84
+ @enum_class = enum_class
85
+ @element_type = ColumnType.new(enum_class, validate: validate)
86
+ super()
87
+ end
88
+
89
+ def type
90
+ :json
91
+ end
92
+
93
+ # Cast to an array of enum instances. A single value wraps into a
94
+ # one-element array; nil casts to []. Elements cast leniently like
95
+ # ColumnType#cast (blank or unknown resolves to nil) and are compacted
96
+ # away — eager rejection of unknowns happens in #assert_valid_value.
97
+ def cast(value)
98
+ Array(value).map { |element| @element_type.cast(element) }.compact
99
+ end
100
+
101
+ # Deserialize the database's JSON document. Lenient like
102
+ # ColumnType#deserialize: an element the enum no longer recognises is
103
+ # dropped rather than raising on read.
104
+ def deserialize(value)
105
+ return [] if value.nil? || value == ''
106
+
107
+ value = ActiveSupport::JSON.decode(value) if value.is_a?(::String)
108
+
109
+ Array(value).map { |element| @element_type.deserialize(element) }.compact
110
+ end
111
+
112
+ # Validate each element at assignment time, the way ColumnType does for
113
+ # a single value: blanks are allowed (they're dropped by #cast); an
114
+ # unknown element raises unless the type was built with validate: true.
115
+ def assert_valid_value(value)
116
+ Array(value).each { |element| @element_type.assert_valid_value(element) }
117
+ end
118
+
119
+ # Serialize to a JSON array of database values for storage.
120
+ def serialize(value)
121
+ return nil if value.nil?
122
+
123
+ ActiveSupport::JSON.encode(Array(value).map { |element| @element_type.serialize(element) })
124
+ end
125
+
126
+ # Detect in-place mutation (e.g. `record.statuses << Status::ACTIVE`)
127
+ # by comparing what the raw database value reads as against the current
128
+ # value. Order is significant. Deliberately not a string comparison of
129
+ # serialized forms: the database's own text rendering of a JSON document
130
+ # (e.g. PostgreSQL jsonb prints `["a", "b"]` with spaces) never matches
131
+ # ActiveSupport::JSON.encode's compact form, which would flag every
132
+ # multi-element record as changed the moment it loads.
133
+ def changed_in_place?(raw_old_value, new_value)
134
+ deserialize(raw_old_value) != cast(new_value)
135
+ end
136
+ end
137
+
74
138
  module ClassMethods
75
139
  # For ActiveRecord attribute type definition. `validate:` mirrors
76
140
  # ActiveRecord::Enum (default false = raise eagerly on an unknown value;
77
- # true = let model validations handle it). Memoised per option value.
78
- def column_type(validate: false)
79
- (@column_types ||= {})[validate] ||=
141
+ # true = let model validations handle it). `array: true` returns a type
142
+ # for columns holding multiple values of the enum as a JSON array.
143
+ # Memoised per option set.
144
+ def column_type(validate: false, array: false)
145
+ (@column_types ||= {})[[validate, array]] ||= if array
146
+ Unmagic::Enum::ActiveRecordExtensions::ArrayColumnType.new(self, validate: validate)
147
+ else
80
148
  Unmagic::Enum::ActiveRecordExtensions::ColumnType.new(self, validate: validate)
149
+ end
81
150
  end
82
151
  end
83
152
 
@@ -3,6 +3,6 @@
3
3
  module Unmagic
4
4
  class Enum
5
5
  # Current version of the unmagic-enum gem
6
- VERSION = "0.2.0"
6
+ VERSION = "0.3.1"
7
7
  end
8
8
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: unmagic-enum
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keith Pitt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-08 00:00:00.000000000 Z
11
+ date: 2026-08-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport