unmagic-enum 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5b6ca718c4633bd4b4416c9e5b4f835b04515982c718fe328944eaa8a3e35447
4
- data.tar.gz: 47868614586456e22c218b68157898ca35f927141316e7a8499682f92b2e8768
3
+ metadata.gz: ccf66f61ae488422b3d515b215511b3a0be62d10d72c56123a175ff779666782
4
+ data.tar.gz: 1ee14dcf4a39545056630e5793bc4928eb7096225c61c2eb8b3588a0c3067789
5
5
  SHA512:
6
- metadata.gz: 9c13abf83c50bcb036ee3631de10d5ffb14fde58a78e154e4af60574c94d6af1ad89c3424b03f25fa332de852bbd9f7e1f6755cfa728d5378c86118a6ce05516
7
- data.tar.gz: 4f564234de61c4d44c3fd425c29518c99f938f8e9adda553ea1930bbe0332055cec4bea52fb83d8f6ca6bd6ca0134bd426befed056ac7013afd697376a9cc174
6
+ metadata.gz: 8b5fe7f97a7ccecf276d2657af7c530f594fa64aef1eff9b0d38b05139cd050d62f7423944ba8e69baf42bd30e52504c4a40b05e1e0fb13caa17bf287c2c1fef
7
+ data.tar.gz: 442edaf6a0ed84cd1486cb5e297448d93726271b1c4452bfb0ca01443023f760983f438bd42be19e985a15e622f0e9050f80a9bae69b2b74527cbd174e11c3ef
data/CHANGELOG.md CHANGED
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] - 2026-06-12
11
+
12
+ ### Added
13
+ - `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.
14
+
10
15
  ## [0.2.0] - 2026-06-09
11
16
 
12
17
  ### Added
@@ -33,6 +38,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
33
38
  - Rails presence support (`blank?`/`present?`) and JSON serialization (`as_json`)
34
39
  - Empty strings treated as `nil`
35
40
 
36
- [Unreleased]: https://github.com/unreasonable-magic/unmagic-enum/compare/v0.1.1...HEAD
41
+ [Unreleased]: https://github.com/unreasonable-magic/unmagic-enum/compare/v0.3.0...HEAD
42
+ [0.3.0]: https://github.com/unreasonable-magic/unmagic-enum/compare/v0.2.0...v0.3.0
43
+ [0.2.0]: https://github.com/unreasonable-magic/unmagic-enum/compare/v0.1.1...v0.2.0
37
44
  [0.1.1]: https://github.com/unreasonable-magic/unmagic-enum/compare/v0.1.0...v0.1.1
38
45
  [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,77 @@ 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 the serialized forms. Order is significant.
128
+ def changed_in_place?(raw_old_value, new_value)
129
+ serialize(new_value) != raw_old_value
130
+ end
131
+ end
132
+
74
133
  module ClassMethods
75
134
  # For ActiveRecord attribute type definition. `validate:` mirrors
76
135
  # 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] ||=
136
+ # true = let model validations handle it). `array: true` returns a type
137
+ # for columns holding multiple values of the enum as a JSON array.
138
+ # Memoised per option set.
139
+ def column_type(validate: false, array: false)
140
+ (@column_types ||= {})[[validate, array]] ||= if array
141
+ Unmagic::Enum::ActiveRecordExtensions::ArrayColumnType.new(self, validate: validate)
142
+ else
80
143
  Unmagic::Enum::ActiveRecordExtensions::ColumnType.new(self, validate: validate)
144
+ end
81
145
  end
82
146
  end
83
147
 
@@ -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.0"
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.0
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-06-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport