activestorage-validator 0.6.0 → 0.8.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: 6b66feb405437ad34641613c818d1fee5d8130235ab77b30ab07630ca7a09560
4
- data.tar.gz: 00417b87e52603376cca1afc253e336b8ac5a090fbdbb386f43ae2062a73bdb0
3
+ metadata.gz: 7dd3596f7f7c5643a6bb22c897877b3d622acf3d525884108b047e02bfb36792
4
+ data.tar.gz: f705a000c6d22a36770bee09629c28c8fd3f5322c7e7ab48194aa8b5a976571d
5
5
  SHA512:
6
- metadata.gz: 457cd27983de69c7d3e1e0f9c9226fb5b67410be17e4f64132220ce0953a4c4a9345eed292628343eb4f373bbdd83aab501f6036eb689a2627f86cff22207c49
7
- data.tar.gz: a7ea46c6d09ea311b7329bc63b94355c543a568a562d8db413f447a479238aae295d42edbcf29665969b4083af97624ad16a8d984a9e41120fee2dac68135f92
6
+ metadata.gz: bb4c1f92c407d40b7bc8658f59bb94e5126db2f6418b9667ad45538c8a1cd91251b896db728a422c403a5c04292bb93f8759f6c9db3e606e71c767940f91a6fb
7
+ data.tar.gz: 50763f805cf34602d88bd07b805dcf0321e5f64ab2b50078209d6a201cfa95095cf7009cef688ebf85706eb8105f2669dee43656ba046a7ec86b542c7304f0a6
@@ -23,21 +23,15 @@ jobs:
23
23
  fail-fast: false
24
24
  matrix:
25
25
  include:
26
- - { ruby: "3.0", gemfile: "rails61" }
27
- - { ruby: "3.1", gemfile: "rails61" }
28
- - { ruby: "3.2", gemfile: "rails61" }
29
- - { ruby: "3.0", gemfile: "rails70" }
30
- - { ruby: "3.1", gemfile: "rails70" }
31
- - { ruby: "3.2", gemfile: "rails70" }
32
- - { ruby: "3.0", gemfile: "rails71" }
33
- - { ruby: "3.1", gemfile: "rails71" }
34
- - { ruby: "3.2", gemfile: "rails71" }
35
- - { ruby: "3.2", gemfile: "rails72" }
36
26
  - { ruby: "3.3", gemfile: "rails72" }
37
27
  - { ruby: "3.4", gemfile: "rails72" }
38
- - { ruby: "3.2", gemfile: "rails80" }
28
+ - { ruby: "4.0", gemfile: "rails72" }
39
29
  - { ruby: "3.3", gemfile: "rails80" }
40
30
  - { ruby: "3.4", gemfile: "rails80" }
31
+ - { ruby: "4.0", gemfile: "rails80" }
32
+ - { ruby: "3.3", gemfile: "rails81" }
33
+ - { ruby: "3.4", gemfile: "rails81" }
34
+ - { ruby: "4.0", gemfile: "rails81" }
41
35
  steps:
42
36
  - name: Install packages
43
37
  run: |
data/.gitignore CHANGED
@@ -14,3 +14,4 @@
14
14
  gemfiles/*.lock
15
15
  *.sqlite
16
16
  Gemfile.local
17
+ CLAUDE.local.md
data/README.md CHANGED
@@ -55,9 +55,9 @@ end
55
55
 
56
56
  | Option | Description |
57
57
  |--------------|---------------------------------------------------------------------------------------------|
58
- | content_type | Allowed MIME types. Accepts a symbol (`:web_image`, `:image`, `:audio`, `:video`, `:text`), an array of MIME types, a regular expression, or a string (single MIME type). |
59
- | size_range | Allowed file size range (e.g. `1..5.megabytes`) |
60
- | extension | Allowed file extensions. Accepts a String or an Array of Strings. Case-insensitive, leading dot optional. Files without an extension are rejected. |
58
+ | content_type | Allowed MIME types. Accepts a symbol (`:web_image`, `:image`, `:audio`, `:video`, `:text`), an array of MIME types, a regular expression, or a string (single MIME type). Or a Proc/lambda returning one of the above. |
59
+ | size_range | Allowed file size range (e.g. `1..5.megabytes`). Or a Proc/lambda returning a Range. |
60
+ | extension | Allowed file extensions. Accepts a String or an Array of Strings. Case-insensitive, leading dot optional. Files without an extension are rejected. Or a Proc/lambda returning one of the above. |
61
61
 
62
62
  ### content_type Examples
63
63
 
@@ -82,6 +82,29 @@ end
82
82
 
83
83
  The leading dot is optional (`"mp3"` and `".mp3"` behave the same), and matching is case-insensitive (`PHOTO.JPG` matches `extension: "jpg"`).
84
84
 
85
+ ## Dynamic Options (Proc/lambda)
86
+
87
+ Every option (`content_type`, `size_range`, `extension`) also accepts a Proc/lambda,
88
+ so you can decide the allowed values per record at validation time:
89
+
90
+ ```ruby
91
+ validates :avatar, blob: {
92
+ content_type: ->(record) { record.premium? ? %w[image/png image/jpeg] : :web_image },
93
+ size_range: ->(record) { 1..(record.premium? ? 20.megabytes : 5.megabytes) }
94
+ }
95
+ ```
96
+
97
+ The Proc is resolved with the same arity convention as Rails' built-in validators
98
+ (the same rule used by `if:` / `unless:`):
99
+
100
+ - **No argument** (`-> { ... }`) — called as-is.
101
+ - **One argument** (`->(record) { ... }`) — the record is passed in.
102
+
103
+ The Proc must return a value of one of the types the option already accepts
104
+ (a Symbol/Array/Regexp/String for `content_type`, a Range for `size_range`, a
105
+ String/Array for `extension`). For `has_many_attached`, the Proc is evaluated
106
+ once per record (not per attached file).
107
+
85
108
  ## I18n Error Message Options
86
109
 
87
110
  Validation error messages are I18n compatible. The following interpolation keys are available in your translation files, according to the validator's implementation:
@@ -117,6 +140,23 @@ en:
117
140
  - Some features may not work depending on your ActiveStorage or Rails version.
118
141
  - Validation error messages are I18n compatible and support interpolation keys as described above.
119
142
 
143
+ ## Agent skill
144
+
145
+ This repo ships an [agent skill](skills/activestorage-validator/) (`SKILL.md`,
146
+ plus a Japanese `SKILL-ja.md`) that teaches coding agents when and how to apply
147
+ `activestorage-validator`. Install it into your project with the GitHub CLI:
148
+
149
+ ```sh
150
+ gh skill install aki77/activestorage-validator
151
+ ```
152
+
153
+ > `gh skill` is currently a GitHub CLI preview feature.
154
+
155
+ Alternatively, if your project already pulls `activestorage-validator` in via
156
+ Bundler, the [bundler-skills](https://github.com/aki77/bundler-skills) plugin
157
+ auto-syncs this skill on `bundle install` — keeping the skill version locked to
158
+ the gem version.
159
+
120
160
  ## Contributing
121
161
 
122
162
  Bug reports and pull requests are welcome on GitHub via [issues](https://github.com/aki77/activestorage-validator/issues) or [pull requests](https://github.com/aki77/activestorage-validator/pulls).
@@ -17,16 +17,16 @@ Gem::Specification.new do |spec|
17
17
  # Specify which files should be added to the gem when it is released.
18
18
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
19
  spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
20
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) || f.match(%r{^skills/.*-ja\.md$}) }
21
21
  end
22
22
  spec.bindir = "exe"
23
23
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
24
  spec.require_paths = ["lib"]
25
25
 
26
- spec.required_ruby_version = '>= 3.0.0'
26
+ spec.required_ruby_version = '>= 3.3.0'
27
27
 
28
- spec.add_dependency "rails", ">= 6.1.0"
29
- spec.add_development_dependency "bundler", "~> 2.0"
28
+ spec.add_dependency "rails", ">= 7.2.0"
29
+ spec.add_development_dependency "bundler", ">= 2.0"
30
30
  spec.add_development_dependency "rake", ">= 12.3.3"
31
31
  spec.add_development_dependency "rspec", "~> 3.0"
32
32
  spec.add_development_dependency "combustion"
@@ -1,4 +1,4 @@
1
- source "http://rubygems.org"
1
+ source "https://rubygems.org"
2
2
 
3
3
  gem 'rails', '~> 7.2.0'
4
4
  gem 'sqlite3'
@@ -1,4 +1,4 @@
1
- source "http://rubygems.org"
1
+ source "https://rubygems.org"
2
2
 
3
3
  gem 'rails', '~> 8.0.0'
4
4
  gem 'sqlite3'
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'rails', '~> 8.1.0'
4
+ gem 'sqlite3'
5
+
6
+ gemspec path: '../'
@@ -4,25 +4,23 @@ module ActiveRecord
4
4
  def validate_each(record, attribute, values) # rubocop:disable Metrics/AbcSize
5
5
  return unless values.attached?
6
6
 
7
+ size_range = resolve_option(record, options[:size_range])
8
+ content_type = resolve_option(record, options[:content_type])
9
+ extension = resolve_option(record, options[:extension])
10
+
7
11
  Array(values).each do |value|
8
- if options[:size_range].present?
9
- if options[:size_range].min > value.blob.byte_size
10
- record.errors.add(attribute, :min_size_error, min_size: ActiveSupport::NumberHelper.number_to_human_size(options[:size_range].min), filename: value.blob.filename.to_s)
11
- elsif options[:size_range].max < value.blob.byte_size
12
- record.errors.add(attribute, :max_size_error, max_size: ActiveSupport::NumberHelper.number_to_human_size(options[:size_range].max), filename: value.blob.filename.to_s)
13
- end
14
- end
12
+ validate_size(record, attribute, value, size_range) if size_range.present?
15
13
 
16
- unless valid_content_type?(value.blob)
14
+ unless valid_content_type?(value.blob, content_type)
17
15
  record.errors.add(attribute, :content_type, filename: value.blob.filename.to_s)
18
16
  end
19
17
 
20
- unless valid_extension?(value.blob)
18
+ unless valid_extension?(value.blob, extension)
21
19
  record.errors.add(
22
20
  attribute,
23
21
  :extension,
24
22
  filename: value.blob.filename.to_s,
25
- extension: Array(options[:extension]).map { |e| normalize_extension(e) }.join(', ')
23
+ extension: Array(extension).map { |e| normalize_extension(e) }.join(', ')
26
24
  )
27
25
  end
28
26
  end
@@ -30,27 +28,46 @@ module ActiveRecord
30
28
 
31
29
  private
32
30
 
33
- def valid_content_type?(blob)
34
- return true if options[:content_type].nil?
31
+ # Resolve only when the value is a Proc, using the same arity convention
32
+ # as Rails' built-in validators: arity 0 calls the proc as-is, otherwise
33
+ # the record is passed in. Anything else (Symbol/String/Array/Regexp/
34
+ # Range/nil) is returned untouched, preserving backward compatibility.
35
+ def resolve_option(record, value)
36
+ return value unless value.is_a?(Proc)
37
+
38
+ value.arity.zero? ? value.call : value.call(record)
39
+ end
40
+
41
+ def validate_size(record, attribute, value, size_range)
42
+ byte_size = value.blob.byte_size
43
+ if size_range.min > byte_size
44
+ record.errors.add(attribute, :min_size_error, min_size: ActiveSupport::NumberHelper.number_to_human_size(size_range.min), filename: value.blob.filename.to_s)
45
+ elsif size_range.max < byte_size
46
+ record.errors.add(attribute, :max_size_error, max_size: ActiveSupport::NumberHelper.number_to_human_size(size_range.max), filename: value.blob.filename.to_s)
47
+ end
48
+ end
49
+
50
+ def valid_content_type?(blob, content_type)
51
+ return true if content_type.nil?
35
52
 
36
- case options[:content_type]
53
+ case content_type
37
54
  when Regexp
38
- options[:content_type].match?(blob.content_type)
55
+ content_type.match?(blob.content_type)
39
56
  when Array
40
- options[:content_type].include?(blob.content_type)
57
+ content_type.include?(blob.content_type)
41
58
  when :web_image
42
59
  ActiveStorage.web_image_content_types.include?(blob.content_type)
43
60
  when Symbol
44
- blob.public_send("#{options[:content_type]}?")
61
+ blob.public_send("#{content_type}?")
45
62
  else
46
- options[:content_type] == blob.content_type
63
+ content_type == blob.content_type
47
64
  end
48
65
  end
49
66
 
50
- def valid_extension?(blob)
51
- return true if options[:extension].nil?
67
+ def valid_extension?(blob, extension)
68
+ return true if extension.nil?
52
69
 
53
- allowed = Array(options[:extension]).map { |e| normalize_extension(e) }
70
+ allowed = Array(extension).map { |e| normalize_extension(e) }
54
71
  actual = normalize_extension(blob.filename.extension)
55
72
  return false if actual.empty?
56
73
 
@@ -1,5 +1,5 @@
1
1
  module ActiveStorage
2
2
  module Validator
3
- VERSION = '0.6.0'
3
+ VERSION = '0.8.0'
4
4
  end
5
5
  end
@@ -0,0 +1,138 @@
1
+ ---
2
+ name: activestorage-validator
3
+ description: "Validate ActiveStorage attachments (content type, file size, file extension) with the activestorage-validator gem's `blob` validator. Use when adding validations to has_one_attached / has_many_attached attributes."
4
+ ---
5
+
6
+ # activestorage-validator
7
+
8
+ A Rails gem that adds a `blob` validator for ActiveStorage attachments. It
9
+ validates content type, file size, and file extension on both
10
+ `has_one_attached` and `has_many_attached` attributes.
11
+
12
+ ## First: determine applicability
13
+
14
+ Use this gem **only** for validating ActiveStorage attachments. For ordinary
15
+ (non-attachment) attributes, use the standard Rails validators (`presence`,
16
+ `length`, `numericality`, etc.).
17
+
18
+ | What you validate | Tool |
19
+ | --- | --- |
20
+ | ActiveStorage attachment (content type / size / extension) | **`blob:` validator (this gem)** |
21
+ | Plain string/number/boolean columns | Standard Rails validators |
22
+ | Presence of the attachment itself | `presence: true` (works alongside `blob:`) |
23
+
24
+ There is no special read path — declare `validates ..., blob: { ... }` and the
25
+ attachment is validated like any other Rails validation on `save`/`valid?`.
26
+
27
+ ## Usage
28
+
29
+ Declare validations on the model with the `blob:` validator.
30
+
31
+ ```ruby
32
+ class User < ApplicationRecord
33
+ has_one_attached :avatar
34
+ has_many_attached :photos
35
+
36
+ # Allow only web image files (PNG, JPEG, GIF, WebP)
37
+ validates :avatar, presence: true, blob: { content_type: :web_image }
38
+
39
+ # Allow only JPEG/PNG, up to 5MB per file
40
+ validates :photos, presence: true, blob: {
41
+ content_type: ["image/png", "image/jpg", "image/jpeg"],
42
+ size_range: 1..(5.megabytes)
43
+ }
44
+
45
+ # Regexp content_type + required extension (AND condition)
46
+ validates :audio, blob: { content_type: "audio/mpeg", extension: %w[mp3] }
47
+ end
48
+ ```
49
+
50
+ > For `has_many_attached`, every option (including `size_range`) is applied to
51
+ > **each file individually**.
52
+
53
+ ## Options (all optional, combine freely)
54
+
55
+ | Option | Accepts | Notes |
56
+ | --- | --- | --- |
57
+ | `content_type` | Symbol / Array / Regexp / String / Proc | Allowed MIME type(s). See matching below. |
58
+ | `size_range` | Range / Proc | Allowed byte size, e.g. `1..(5.megabytes)`. |
59
+ | `extension` | String / Array / Proc | Allowed filename extension(s). |
60
+
61
+ Any option also accepts a **Proc/lambda** returning a value of the listed type,
62
+ resolved with the same arity convention as Rails' built-in validators (`if:` /
63
+ `unless:`): `-> { ... }` is called as-is, `->(record) { ... }` receives the
64
+ record. For `has_many_attached` the Proc is evaluated once per record. Example:
65
+ `content_type: ->(record) { record.premium? ? %w[image/png] : :web_image }`.
66
+
67
+ ### `content_type` matching
68
+
69
+ - **`:web_image`** — special-cased to `ActiveStorage.web_image_content_types`
70
+ (PNG, JPEG, GIF, WebP). Use this instead of `:image` when you want only
71
+ browser-safe images.
72
+ - **Other Symbol** (`:image`, `:audio`, `:video`, `:text`) — calls the
73
+ ActiveStorage built-in predicate `blob.image?` / `blob.audio?` / etc.
74
+ - **Array** — `["image/png", "image/jpeg"]`: exact membership of the blob's
75
+ content type in the list.
76
+ - **Regexp** — `%r{^image/}`: matched against the blob's content type.
77
+ - **String** — `"application/pdf"`: exact equality.
78
+
79
+ ### `extension` matching
80
+
81
+ - Accepts a String (`"mp3"`) or Array (`%w[mp3 m4a]`).
82
+ - Leading dot is optional (`"mp3"` == `".mp3"`).
83
+ - Case-insensitive (`PHOTO.JPG` matches `extension: "jpg"`).
84
+ - **A file with no extension is always rejected** when `extension` is set.
85
+ - When combined with `content_type`, both must pass (AND). E.g.
86
+ `{ content_type: "audio/mpeg", extension: %w[mp3] }` rejects a `.wav` file
87
+ even if its MIME type happens to be `audio/mpeg`.
88
+
89
+ ## I18n error messages
90
+
91
+ Errors are I18n-compatible. Error types and their interpolation keys:
92
+
93
+ | Error type | Interpolation keys | When |
94
+ | --- | --- | --- |
95
+ | `content_type` | `filename` | content type not allowed |
96
+ | `min_size_error` | `filename`, `min_size` | smaller than `size_range.min` |
97
+ | `max_size_error` | `filename`, `max_size` | larger than `size_range.max` |
98
+ | `extension` | `filename`, `extension` | extension not allowed (or missing) |
99
+
100
+ `min_size` / `max_size` are humanized (`ActiveSupport::NumberHelper.number_to_human_size`).
101
+ `extension` is the allowed list joined by `, `.
102
+
103
+ ```yaml
104
+ # config/locales/en.yml
105
+ en:
106
+ errors:
107
+ messages:
108
+ content_type: "%{filename} has an invalid content type"
109
+ min_size_error: "%{filename} is too small (minimum is %{min_size})"
110
+ max_size_error: "%{filename} is too large (maximum is %{max_size})"
111
+ extension: "%{filename} has an invalid file extension (allowed: %{extension})"
112
+ ```
113
+
114
+ The gem ships default messages for 7 locales: `en`, `ja`, `de`, `es`, `fr`,
115
+ `pl`, `pt-br`. Override them in your app's locale files using the keys above.
116
+
117
+ ## Behavior details
118
+
119
+ - **Unattached values are skipped.** Validation returns early unless the
120
+ attachment is attached, so `blob:` does not enforce presence — add
121
+ `presence: true` separately if the attachment is required.
122
+ - **Size boundaries are inclusive.** A blob is valid when
123
+ `size_range.min <= byte_size <= size_range.max`. Below min → `min_size_error`;
124
+ above max → `max_size_error`.
125
+ - **Extension normalization** is `downcase` + strip a single leading dot; an
126
+ empty resulting extension (no extension on the file) fails.
127
+ - Options are independent — omit any you don't need; only the present ones run.
128
+
129
+ ## Pitfalls
130
+
131
+ - A file **without an extension** is rejected whenever `extension:` is set —
132
+ even if its content type is allowed. Don't set `extension:` if you accept
133
+ extension-less uploads.
134
+ - For `has_many_attached`, `size_range` applies **per file**, not to the total.
135
+ - `blob:` alone does **not** require an attachment; pair it with `presence: true`
136
+ when the upload is mandatory.
137
+ - No special read/preload path exists — this is a plain `EachValidator`; it runs
138
+ on standard `valid?` / `save`.
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activestorage-validator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - aki
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-04-29 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails
@@ -15,26 +15,26 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 6.1.0
18
+ version: 7.2.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: 6.1.0
25
+ version: 7.2.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: bundler
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - "~>"
30
+ - - ">="
31
31
  - !ruby/object:Gem::Version
32
32
  version: '2.0'
33
33
  type: :development
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - "~>"
37
+ - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '2.0'
40
40
  - !ruby/object:Gem::Dependency
@@ -107,14 +107,13 @@ files:
107
107
  - config/locales/ja.yml
108
108
  - config/locales/pl.yml
109
109
  - config/locales/pt-br.yml
110
- - gemfiles/rails61.gemfile
111
- - gemfiles/rails70.gemfile
112
- - gemfiles/rails71.gemfile
113
110
  - gemfiles/rails72.gemfile
114
111
  - gemfiles/rails80.gemfile
112
+ - gemfiles/rails81.gemfile
115
113
  - lib/activestorage/validator.rb
116
114
  - lib/activestorage/validator/blob.rb
117
115
  - lib/activestorage/validator/version.rb
116
+ - skills/activestorage-validator/SKILL.md
118
117
  homepage: https://github.com/aki77/activestorage-validator
119
118
  licenses:
120
119
  - MIT
@@ -126,14 +125,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
126
125
  requirements:
127
126
  - - ">="
128
127
  - !ruby/object:Gem::Version
129
- version: 3.0.0
128
+ version: 3.3.0
130
129
  required_rubygems_version: !ruby/object:Gem::Requirement
131
130
  requirements:
132
131
  - - ">="
133
132
  - !ruby/object:Gem::Version
134
133
  version: '0'
135
134
  requirements: []
136
- rubygems_version: 3.6.2
135
+ rubygems_version: 4.0.10
137
136
  specification_version: 4
138
137
  summary: ActiveStorage blob validator.
139
138
  test_files: []
@@ -1,7 +0,0 @@
1
- source "http://rubygems.org"
2
-
3
- gem 'rails', '6.1.4'
4
- gem 'sqlite3', '~> 1.4'
5
- gem 'concurrent-ruby', '< 1.3.5'
6
-
7
- gemspec path: '../'
@@ -1,7 +0,0 @@
1
- source "http://rubygems.org"
2
-
3
- gem 'rails', '~> 7.0.0'
4
- gem 'sqlite3', '~> 1.4'
5
- gem 'concurrent-ruby', '< 1.3.5'
6
-
7
- gemspec path: '../'
@@ -1,6 +0,0 @@
1
- source "http://rubygems.org"
2
-
3
- gem 'rails', '~> 7.1.0'
4
- gem 'sqlite3'
5
-
6
- gemspec path: '../'