activestorage-validator 0.5.0 → 0.7.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: eb2851d4b64cc39176ceaba155c80ac8989930899110e55f471c67df9d78a1bc
4
- data.tar.gz: ea620cd996d2b50cf53e058fb43acc89652c5edaaa754de8f333855bab4f177e
3
+ metadata.gz: acc9fae7338e419736df276e25e94e198cdc8a0ca9a2b80210c3175d684f5f22
4
+ data.tar.gz: 6b9be22b0d25c70bcf08cc51a05f9443982963d7e42e3817b90c60a596e5e071
5
5
  SHA512:
6
- metadata.gz: 9288e28946a1e1fb38691329eae86807c4d781116b2f64b389a6bc4e8b3c8e87c0cff3342c1bf716693126d84d64d553a0abddd2ae7a99e4fd28855f9940f0db
7
- data.tar.gz: 6098588122be931263ae237cd6e486c1a975e446578502179f6b2064ed54ef1397431e3a23ce9aa1c086ecddc3fc49e42a2d402ed725472b57587ca8cc42d975
6
+ metadata.gz: 57e1e9cd4353cfcb2aaeee8a2eef4bbedfe49b3382a3cacc7281fa1e5ec5ca1aa10158465a42e6eff8c2dbfd375b16f9ddb26c3027d1b337ca8fd0b41824c94b
7
+ data.tar.gz: 597d5cd7f32091fcf00a196fa4bd9d7404e5f129db45d0235c70768a01167d47ae9ec361b635b2732add9ba8be45c2bb2fef6db5a4b67e676130dfe7cf9fd13a
@@ -44,7 +44,7 @@ jobs:
44
44
  sudo apt update -y
45
45
  sudo apt install -y libsqlite3-dev
46
46
 
47
- - uses: actions/checkout@v4
47
+ - uses: actions/checkout@v6
48
48
 
49
49
  - name: Set up Ruby
50
50
  uses: ruby/setup-ruby@v1
data/README.md CHANGED
@@ -3,43 +3,143 @@
3
3
 
4
4
  # ActiveStorage Validator
5
5
 
6
- ActiveStorage blob validator.
6
+ A Rails gem that makes it easy to add validations such as `content_type` and file size to ActiveStorage attachments.
7
+
8
+ - Supports both `has_one_attached` and `has_many_attached` of ActiveStorage
9
+ - Flexible validations for content type and file size
7
10
 
8
11
  ## Installation
9
12
 
13
+ ### Using Bundler
14
+
10
15
  Add this line to your application's Gemfile:
11
16
 
12
17
  ```ruby
13
18
  gem 'activestorage-validator'
14
19
  ```
15
20
 
16
- And then execute:
21
+ Then execute:
17
22
 
18
- $ bundle
23
+ $ bundle install
19
24
 
20
- Or install it yourself as:
25
+ ### Install with gem command
21
26
 
22
27
  $ gem install activestorage-validator
23
28
 
24
29
  ## Usage
25
30
 
31
+ You can add validations to models with ActiveStorage attachments using the `blob` validator.
32
+
26
33
  ```ruby
27
34
  class User < ApplicationRecord
28
35
  has_one_attached :avatar
29
36
  has_many_attached :photos
30
37
 
31
- validates :avatar, presence: true, blob: { content_type: :web_image } # supported options: :web_image, :image, :audio, :video, :text
32
- validates :photos, presence: true, blob: { content_type: ['image/png', 'image/jpg', 'image/jpeg'], size_range: 1..(5.megabytes) }
33
- # validates :photos, presence: true, blob: { content_type: %r{^image/}, size_range: 1..(5.megabytes) }
38
+ # Allow only web image files
39
+ validates :avatar, presence: true, blob: { content_type: :web_image }
40
+
41
+ # Allow only JPEG/PNG, up to 5MB per file
42
+ validates :photos, presence: true, blob: { content_type: ["image/png", "image/jpg", "image/jpeg"], size_range: 1..(5.megabytes) }
43
+
44
+ # You can also use a regular expression for content_type
45
+ # validates :photos, blob: { content_type: %r{^image/}, size_range: 1..(5.megabytes) }
46
+
47
+ # Require the filename to have one of the allowed extensions
48
+ # validates :audio, blob: { content_type: "audio/mpeg", extension: %w[mp3] }
34
49
  end
35
50
  ```
36
51
 
37
- Note: For `has_many_attached`, size is validated on each file individually. In the code above, `:photos` validation allows any number of photos to be upload, each one being 5 MB or less in size.
52
+ > **Note:** For `has_many_attached`, the size validation is applied to each file individually.
53
+
54
+ ## Validation Options
55
+
56
+ | Option | Description |
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. |
61
+
62
+ ### content_type Examples
63
+
64
+ - **Symbol**
65
+ - `:web_image` ... PNG, JPEG, GIF, WebP (special handling)
66
+ - `:image`, `:audio`, `:video`, `:text` ... Calls `blob.image?`, `blob.audio?`, etc. (ActiveStorage built-in predicate)
67
+ - **Array**
68
+ - `["image/png", "image/jpg", "image/jpeg"]` ... Only allow these MIME types
69
+ - **Regexp**
70
+ - `%r{^image/}` ... Allow any image type (MIME type starts with `image/`)
71
+ - **String**
72
+ - `"application/pdf"` ... Only allow PDF files
73
+
74
+ ### extension Examples
75
+
76
+ - **String**
77
+ - `extension: "mp3"` ... Only allow `.mp3` files
78
+ - **Array**
79
+ - `extension: %w[mp3 m4a]` ... Allow either `.mp3` or `.m4a`
80
+ - **Combined with content_type (AND)**
81
+ - `blob: { content_type: "audio/mpeg", extension: %w[mp3] }` ... Reject both `my_podcast` (no extension) and `my_podcast.wav` (wrong extension), even when their MIME types match.
82
+
83
+ The leading dot is optional (`"mp3"` and `".mp3"` behave the same), and matching is case-insensitive (`PHOTO.JPG` matches `extension: "jpg"`).
84
+
85
+ ## I18n Error Message Options
86
+
87
+ Validation error messages are I18n compatible. The following interpolation keys are available in your translation files, according to the validator's implementation:
88
+
89
+ | Key | Description |
90
+ |----------------|---------------------------------------------|
91
+ | filename | The uploaded file's name |
92
+ | min_size | The minimum allowed file size (humanized) |
93
+ | max_size | The maximum allowed file size (humanized) |
94
+ | extension | The list of allowed extensions, joined by `, ` |
95
+
96
+ The following error types are used:
97
+
98
+ - `content_type` (invalid content type)
99
+ - `min_size_error` (file is too small)
100
+ - `max_size_error` (file is too large)
101
+ - `extension` (invalid file extension)
102
+
103
+ Example (config/locales/en.yml):
104
+
105
+ ```yaml
106
+ en:
107
+ errors:
108
+ messages:
109
+ content_type: "%{filename} has an invalid content type"
110
+ min_size_error: "%{filename} is too small (minimum is %{min_size})"
111
+ max_size_error: "%{filename} is too large (maximum is %{max_size})"
112
+ extension: "%{filename} has an invalid file extension (allowed: %{extension})"
113
+ ```
114
+
115
+ ## Notes
116
+
117
+ - Some features may not work depending on your ActiveStorage or Rails version.
118
+ - Validation error messages are I18n compatible and support interpolation keys as described above.
119
+
120
+ ## Agent skill
121
+
122
+ This repo ships an [agent skill](skills/activestorage-validator/) (`SKILL.md`,
123
+ plus a Japanese `SKILL-ja.md`) that teaches coding agents when and how to apply
124
+ `activestorage-validator`. Install it into your project with the GitHub CLI:
125
+
126
+ ```sh
127
+ gh skill install aki77/activestorage-validator
128
+ ```
129
+
130
+ > `gh skill` is currently a GitHub CLI preview feature.
131
+
132
+ Alternatively, if your project already pulls `activestorage-validator` in via
133
+ Bundler, the [bundler-skills](https://github.com/aki77/bundler-skills) plugin
134
+ auto-syncs this skill on `bundle install` — keeping the skill version locked to
135
+ the gem version.
38
136
 
39
137
  ## Contributing
40
138
 
41
- Bug reports and pull requests are welcome on GitHub at https://github.com/aki77/activestorage-validator. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
139
+ 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).
140
+
141
+ This project adheres to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
42
142
 
43
143
  ## License
44
144
 
45
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
145
+ This gem is released under the MIT License. See [LICENSE.txt](LICENSE.txt) for details.
@@ -17,7 +17,7 @@ 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) }
@@ -5,3 +5,4 @@ de:
5
5
  content_type: "ist kein gültiges Dateiformat"
6
6
  min_size_error: "Dateigröße sollte weniger als %{min_size} betragen"
7
7
  max_size_error: "Dateigröße sollte weniger als %{max_size} betragen"
8
+ extension: "hat eine ungültige Dateierweiterung (erlaubt: %{extension})"
@@ -5,3 +5,4 @@ en:
5
5
  content_type: "is not a valid file format"
6
6
  min_size_error: "File size should be greater than %{min_size}"
7
7
  max_size_error: "File size should be less than %{max_size}"
8
+ extension: "has an invalid file extension (allowed: %{extension})"
@@ -5,3 +5,4 @@ es:
5
5
  content_type: "no es un formato válido"
6
6
  min_size_error: "tamaño de archivo debe ser mayor a %{min_size}"
7
7
  max_size_error: "tamaño de archivo debe ser menor a %{max_size}"
8
+ extension: "tiene una extensión de archivo no válida (permitidas: %{extension})"
@@ -5,3 +5,4 @@ fr:
5
5
  content_type: "n'est pas un format de fichier valide"
6
6
  min_size_error: "La taille du fichier doit être supérieure à %{min_size}"
7
7
  max_size_error: "La taille du fichier doit être inférieure à %{max_size}"
8
+ extension: "a une extension de fichier non valide (autorisées : %{extension})"
@@ -5,3 +5,4 @@ ja:
5
5
  content_type: "のファイル形式が不正です。"
6
6
  min_size_error: "を%{min_size}以上のサイズにしてください。"
7
7
  max_size_error: "を%{max_size}以下のサイズにしてください。"
8
+ extension: "のファイル拡張子が不正です(許可: %{extension})。"
@@ -5,3 +5,4 @@ pl:
5
5
  content_type: "ma nieprawidłowy format"
6
6
  min_size_error: "Rozmiar pliku nie może być większy niż %{min_size}"
7
7
  max_size_error: "Rozmiar pliku nie może być mniejszy niż %{max_size}"
8
+ extension: "ma nieprawidłowe rozszerzenie pliku (dozwolone: %{extension})"
@@ -5,3 +5,4 @@ pt-br:
5
5
  content_type: "não é um formato válido"
6
6
  min_size_error: "tamanho do arquivo deve ser maior que %{min_size}"
7
7
  max_size_error: "tamanho do arquivo deve ser menor que %{max_size}"
8
+ extension: "tem uma extensão de arquivo inválida (permitidas: %{extension})"
@@ -16,6 +16,15 @@ module ActiveRecord
16
16
  unless valid_content_type?(value.blob)
17
17
  record.errors.add(attribute, :content_type, filename: value.blob.filename.to_s)
18
18
  end
19
+
20
+ unless valid_extension?(value.blob)
21
+ record.errors.add(
22
+ attribute,
23
+ :extension,
24
+ filename: value.blob.filename.to_s,
25
+ extension: Array(options[:extension]).map { |e| normalize_extension(e) }.join(', ')
26
+ )
27
+ end
19
28
  end
20
29
  end
21
30
 
@@ -37,6 +46,20 @@ module ActiveRecord
37
46
  options[:content_type] == blob.content_type
38
47
  end
39
48
  end
49
+
50
+ def valid_extension?(blob)
51
+ return true if options[:extension].nil?
52
+
53
+ allowed = Array(options[:extension]).map { |e| normalize_extension(e) }
54
+ actual = normalize_extension(blob.filename.extension)
55
+ return false if actual.empty?
56
+
57
+ allowed.include?(actual)
58
+ end
59
+
60
+ def normalize_extension(value)
61
+ value.to_s.downcase.delete_prefix('.')
62
+ end
40
63
  end
41
64
  end
42
65
  end
@@ -1,5 +1,5 @@
1
1
  module ActiveStorage
2
2
  module Validator
3
- VERSION = '0.5.0'
3
+ VERSION = '0.7.0'
4
4
  end
5
5
  end
@@ -0,0 +1,132 @@
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 | Allowed MIME type(s). See matching below. |
58
+ | `size_range` | Range | Allowed byte size, e.g. `1..(5.megabytes)`. |
59
+ | `extension` | String / Array | Allowed filename extension(s). |
60
+
61
+ ### `content_type` matching
62
+
63
+ - **`:web_image`** — special-cased to `ActiveStorage.web_image_content_types`
64
+ (PNG, JPEG, GIF, WebP). Use this instead of `:image` when you want only
65
+ browser-safe images.
66
+ - **Other Symbol** (`:image`, `:audio`, `:video`, `:text`) — calls the
67
+ ActiveStorage built-in predicate `blob.image?` / `blob.audio?` / etc.
68
+ - **Array** — `["image/png", "image/jpeg"]`: exact membership of the blob's
69
+ content type in the list.
70
+ - **Regexp** — `%r{^image/}`: matched against the blob's content type.
71
+ - **String** — `"application/pdf"`: exact equality.
72
+
73
+ ### `extension` matching
74
+
75
+ - Accepts a String (`"mp3"`) or Array (`%w[mp3 m4a]`).
76
+ - Leading dot is optional (`"mp3"` == `".mp3"`).
77
+ - Case-insensitive (`PHOTO.JPG` matches `extension: "jpg"`).
78
+ - **A file with no extension is always rejected** when `extension` is set.
79
+ - When combined with `content_type`, both must pass (AND). E.g.
80
+ `{ content_type: "audio/mpeg", extension: %w[mp3] }` rejects a `.wav` file
81
+ even if its MIME type happens to be `audio/mpeg`.
82
+
83
+ ## I18n error messages
84
+
85
+ Errors are I18n-compatible. Error types and their interpolation keys:
86
+
87
+ | Error type | Interpolation keys | When |
88
+ | --- | --- | --- |
89
+ | `content_type` | `filename` | content type not allowed |
90
+ | `min_size_error` | `filename`, `min_size` | smaller than `size_range.min` |
91
+ | `max_size_error` | `filename`, `max_size` | larger than `size_range.max` |
92
+ | `extension` | `filename`, `extension` | extension not allowed (or missing) |
93
+
94
+ `min_size` / `max_size` are humanized (`ActiveSupport::NumberHelper.number_to_human_size`).
95
+ `extension` is the allowed list joined by `, `.
96
+
97
+ ```yaml
98
+ # config/locales/en.yml
99
+ en:
100
+ errors:
101
+ messages:
102
+ content_type: "%{filename} has an invalid content type"
103
+ min_size_error: "%{filename} is too small (minimum is %{min_size})"
104
+ max_size_error: "%{filename} is too large (maximum is %{max_size})"
105
+ extension: "%{filename} has an invalid file extension (allowed: %{extension})"
106
+ ```
107
+
108
+ The gem ships default messages for 7 locales: `en`, `ja`, `de`, `es`, `fr`,
109
+ `pl`, `pt-br`. Override them in your app's locale files using the keys above.
110
+
111
+ ## Behavior details
112
+
113
+ - **Unattached values are skipped.** Validation returns early unless the
114
+ attachment is attached, so `blob:` does not enforce presence — add
115
+ `presence: true` separately if the attachment is required.
116
+ - **Size boundaries are inclusive.** A blob is valid when
117
+ `size_range.min <= byte_size <= size_range.max`. Below min → `min_size_error`;
118
+ above max → `max_size_error`.
119
+ - **Extension normalization** is `downcase` + strip a single leading dot; an
120
+ empty resulting extension (no extension on the file) fails.
121
+ - Options are independent — omit any you don't need; only the present ones run.
122
+
123
+ ## Pitfalls
124
+
125
+ - A file **without an extension** is rejected whenever `extension:` is set —
126
+ even if its content type is allowed. Don't set `extension:` if you accept
127
+ extension-less uploads.
128
+ - For `has_many_attached`, `size_range` applies **per file**, not to the total.
129
+ - `blob:` alone does **not** require an attachment; pair it with `presence: true`
130
+ when the upload is mandatory.
131
+ - No special read/preload path exists — this is a plain `EachValidator`; it runs
132
+ 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.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - aki
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-06-15 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
@@ -115,6 +115,7 @@ files:
115
115
  - lib/activestorage/validator.rb
116
116
  - lib/activestorage/validator/blob.rb
117
117
  - lib/activestorage/validator/version.rb
118
+ - skills/activestorage-validator/SKILL.md
118
119
  homepage: https://github.com/aki77/activestorage-validator
119
120
  licenses:
120
121
  - MIT
@@ -133,7 +134,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
133
134
  - !ruby/object:Gem::Version
134
135
  version: '0'
135
136
  requirements: []
136
- rubygems_version: 3.6.2
137
+ rubygems_version: 4.0.10
137
138
  specification_version: 4
138
139
  summary: ActiveStorage blob validator.
139
140
  test_files: []