shrine-rom 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 78bc603cf077c91b940f0df8d4cd0304c68b4182b0ae6b086e911eb1107a1465
4
+ data.tar.gz: c0eabd2c656ddd614a21cf729f0c344de88e2dec2874d9c70052262e748b4107
5
+ SHA512:
6
+ metadata.gz: cea2d2928e38caaf138e9246dd5df636aed3e3de01ee7860a1fedec4c69ce5ba197632423d36be05b644d8d383646e9d86b56ff5b40f1f99795f0a575b82db8a
7
+ data.tar.gz: bd9e6a7cf0ac72be3ae91a3ed10665c172f202f45aa6523d53c72b06edba96905e24933a1dcf8246af691821e756706afb306a4f08837f1a22382c6973479d0f
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Janko Marohnić
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,291 @@
1
+ # Shrine::Plugins::Rom
2
+
3
+ Provides [ROM] integration for [Shrine].
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ $ bundle add shrine-rom
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ Let's asume we have "photos" that have an "image" attachment. We start by
14
+ configuring Shrine in our initializer, and loading the `rom` plugin provided by
15
+ shrine-rom:
16
+
17
+ ```rb
18
+ # Gemfile
19
+ gem "shrine", "~> 3.0"
20
+ gem "shrine-rom"
21
+ ```
22
+ ```rb
23
+ require "shrine"
24
+ require "shrine/storage/file_system"
25
+
26
+ Shrine.storages = {
27
+ cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"), # temporary
28
+ store: Shrine::Storage::FileSystem.new("public", prefix: "uploads"), # permanent
29
+ }
30
+
31
+ Shrine.plugin :rom # ROM integration, provided by shrine-rom
32
+ Shrine.plugin :cached_attachment_data # for retaining the cached file across form redisplays
33
+ Shrine.plugin :rack_file # for accepting Rack uploaded file hashes
34
+ Shrine.plugin :form_assign # for assigning file from form fields
35
+ Shrine.plugin :restore_cached_data # re-extract metadata when attaching a cached file
36
+ Shrine.plugin :validation_helpers # for validating uploaded files
37
+ Shrine.plugin :determine_mime_type # determine MIME type from file content
38
+ ```
39
+
40
+ Next, we run a migration that adds an `image_data` text or JSON column to our
41
+ `photos` table:
42
+
43
+ ```rb
44
+ ROM::SQL.migration do
45
+ change do
46
+ add_column :photos, :image_data, :text # or :jsonb
47
+ end
48
+ end
49
+ ```
50
+
51
+ Now we can define an `ImageUploader` class and include an attachment module
52
+ into our `Photo` entity:
53
+
54
+ ```rb
55
+ class ImageUploader < Shrine
56
+ # we add some basic validation
57
+ Attacher.validate do
58
+ validate_max_size 20*1024*1024
59
+ validate_mime_type %w[image/jpeg image/png image/webp]
60
+ validate_extension %w[jpg jpeg png webp]
61
+ end
62
+ end
63
+ ```
64
+ ```rb
65
+ class PhotoRepo < ROM::Repository[:photos]
66
+ commands :create, update: :by_pk, delete: :by_pk
67
+ struct_namespace Entities
68
+
69
+ def find(id)
70
+ photos.fetch(id)
71
+ end
72
+ end
73
+ ```
74
+ ```rb
75
+ module Entities
76
+ class Photo < ROM::Struct
77
+ include ImageUploader::Attachment[:image]
78
+ end
79
+ end
80
+ ```
81
+
82
+ Let's now add fields for our `image` attachment to our HTML form for creating
83
+ photos:
84
+
85
+ ```rb
86
+ # with Forme gem:
87
+ form @photo, action: "/photos", enctype: "multipart/form-data", namespace: "photo" do |f|
88
+ f.input :title, type: :text
89
+ f.input :image, type: :hidden, value: @attacher&.cached_data
90
+ f.input :image, type: :file
91
+ f.button "Create"
92
+ end
93
+ ```
94
+
95
+ Now in our controller we can attach the uploaded file from request params.
96
+ We'll assume you're using [dry-validation] for validating user input.
97
+
98
+ ```rb
99
+ post "/photos" do
100
+ @photo = Entities::Photo.new
101
+ @attacher = @photo.image_attacher
102
+
103
+ @attacher.form_assign(params["photo"]) # assigns file and performs validation
104
+
105
+ contract = CreatePhotoContract.new(image_attacher: @attacher)
106
+ result = contract.call(params["photo"])
107
+
108
+ if result.success?
109
+ @attacher.finalize # upload cached file to permanent storage
110
+
111
+ attributes = result.to_h
112
+ attributes.merge!(@attacher.column_values)
113
+
114
+ photo_repo.create(attributes)
115
+ # ...
116
+ else
117
+ # ... render view with form ...
118
+ end
119
+ end
120
+ ```
121
+ ```rb
122
+ class CreatePhotoContract < Dry::Validation::Contract
123
+ option :image_attacher
124
+
125
+ params do
126
+ required(:title).filled(:string)
127
+ end
128
+
129
+ # copy any attacher's validation errors into our dry-validation contract
130
+ rule(:image) do
131
+ key.failure("must be present") unless image_attacher.attached?
132
+ image_attacher.errors.each { |message| key.failure(message) }
133
+ end
134
+ end
135
+ ```
136
+
137
+ Once the image has been successfully attached to our photo, we can retrieve the
138
+ image URL by calling `#image_url` on the entity:
139
+
140
+ ```erb
141
+ <img src="<%= @photo.image_url %>" />
142
+ ```
143
+
144
+ If you want to see a complete example with direct uploads and backgrounding,
145
+ see the [demo app][demo].
146
+
147
+ ## Understanding
148
+
149
+ The `rom` plugin builds upon Shrine's [`entity`][entity] plugin, providing
150
+ persistence functionality.
151
+
152
+ The attachment module included into the entity provides convenience methods for
153
+ reading the data attribute:
154
+
155
+ ```rb
156
+ photo.image_data #=> '{"id":"path/to/file","storage":"store","metadata":{...}}'
157
+
158
+ photo.image #=> #<Shrine::UploadedFile @id="path/to/file" @storage_key=:store ...>
159
+ photo.image_url #=> "https://s3.amazonaws.com/..."
160
+ photo.image_attacher #=> #<Shrine::Attacher ...>
161
+ ```
162
+
163
+ ### Updating
164
+
165
+ When updating the attached file for an existing record, it's important to
166
+ initialize the attacher from that record's current attachment. That way the old
167
+ file will be automatically deleted on `Attacher#finalize`.
168
+
169
+ ```rb
170
+ photo = photo_repo.find(photo_id)
171
+ photo.image #=> #<Shrine::UploadedFile @id="foo" ...>
172
+
173
+ attacher = photo.image_attacher # has current attachment
174
+ attacher.assign(file)
175
+
176
+ photo_repo.update(photo_id, attacher.column_values)
177
+
178
+ attacher.finalize # deletes previous attachment
179
+ ```
180
+
181
+ ### Attacher state
182
+
183
+ Unlike the [`model`][model] plugin, the `entity` plugin doesn't memoize the
184
+ `Shrine::Attacher` instance:
185
+
186
+ ```rb
187
+ photo.image_attacher #=> #<Shrine::Attacher:0x00007ffe564085d8>
188
+ photo.image_attacher #=> #<Shrine::Attacher:0x00007ffe53b2f378> (different instance)
189
+ ```
190
+
191
+ So, if you want to update the attacher state, you need to first assign it to a
192
+ variable:
193
+
194
+ ```rb
195
+ attacher = photo.image_attacher
196
+ attacher.assign(file)
197
+ attacher.finalize
198
+ ```
199
+
200
+ ### Persisting
201
+
202
+ Normally you'd persist attachment changes explicitly, by using
203
+ `Attacher#column_data` or `Attacher#column_values`:
204
+
205
+ ```rb
206
+ attacher = photo.image_attacher
207
+ attacher.attach(file)
208
+
209
+ photo_repo.create(image_data: attacher.column_data)
210
+ # or
211
+ photo_repo.create(attacher.column_values)
212
+ ```
213
+
214
+ ## Backgrounding
215
+
216
+ If you want to delay promotion into a background job, you need to call
217
+ `Attacher#finalize` _after_ you've persisted the cached file, so that your
218
+ background job is able to retrieve the record. We'll assume your repository
219
+ objects are registered using [dry-container].
220
+
221
+ ```rb
222
+ Shrine.plugin :backgrounding
223
+ Shrine::Attacher.destroy_block { Attachment::DestroyJob.perform_async(self.class, data) }
224
+ ```
225
+ ```rb
226
+ attacher = photo.image_attacher
227
+ attacher.assign(file)
228
+
229
+ photo = photo_repo.create(attacher.column_values)
230
+
231
+ attacher.promote_block do |attacher|
232
+ Attachment::PromoteJob.perform_async(:photo_repo, photo.id, :image, attacher.file_data)
233
+ end
234
+
235
+ attacher.finalize # calls the promote block
236
+ ```
237
+ ```rb
238
+ class Attachment::PromoteJob
239
+ include Sidekiq::Worker
240
+
241
+ def perform(repo_name, record_id, name, file_data)
242
+ repo = Application[repo_name] # retrieve repo from container
243
+ entity = repo.find(record_id)
244
+
245
+ attacher = Shrine::Attacher.retrieve(
246
+ entity: entity,
247
+ name: name,
248
+ file: file_data,
249
+ repository: repo, # repository needs to be passed in
250
+ )
251
+
252
+ attacher.atomic_promote
253
+ rescue Shrine::AttachmentChanged, # attachment has changed
254
+ ROM::TupleCountMismatchError # record has been deleted
255
+ end
256
+ end
257
+ ```
258
+ ```rb
259
+ class Attachment::DestroyJob
260
+ include Sidekiq::Worker
261
+
262
+ def perform(attacher_class, data)
263
+ attacher = Object.const_get(attacher_class).from_data(data)
264
+ attacher.destroy
265
+ end
266
+ end
267
+ ```
268
+
269
+ ## Contributing
270
+
271
+ Tests are run with:
272
+
273
+ ```sh
274
+ $ bundle exec rake test
275
+ ```
276
+
277
+ ## Code of Conduct
278
+
279
+ Everyone interacting in the Shrine::Rom project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/janko/shrine-rom/blob/master/CODE_OF_CONDUCT.md).
280
+
281
+ ## License
282
+
283
+ [MIT](/LICENSE.txt)
284
+
285
+ [ROM]: https://rom-rb.org
286
+ [Shrine]: https://shrinerb.com
287
+ [entity]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/entity.md#readme
288
+ [model]: https://github.com/shrinerb/shrine/blob/master/doc/plugins/model.md#readme
289
+ [dry-validation]: https://dry-rb.org/gems/dry-validation/
290
+ [dry-container]: https://dry-rb.org/gems/dry-container/
291
+ [demo]: /demo
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Shrine
4
+ module Plugins
5
+ module Rom
6
+ def self.load_dependencies(uploader)
7
+ uploader.plugin :entity
8
+ uploader.plugin :_persistence, plugin: self
9
+ end
10
+
11
+ module AttachmentMethods
12
+ # Disables model behaviour for ROM::Struct and Hanami::Entity
13
+ # subclasses.
14
+ def included(klass)
15
+ @model = false if klass < ::Dry::Struct
16
+ super
17
+ end
18
+ end
19
+
20
+ module AttacherMethods
21
+ attr_reader :repository
22
+
23
+ def initialize(repository: nil, **options)
24
+ super(**options)
25
+ @repository = repository
26
+ end
27
+
28
+ # The _persistence plugin uses #rom_persist, #rom_reload and #rom? to
29
+ # implement the following methods:
30
+ #
31
+ # * Attacher#persist
32
+ # * Attacher#atomic_persist
33
+ # * Attacher#atomic_promote
34
+ private
35
+
36
+ # Updates the record with attachment column values. Used by the
37
+ # _persistence plugin.
38
+ def rom_persist
39
+ rom.update_record(column_values)
40
+ end
41
+
42
+ # Locks the database row and yields the reloaded record. Used by the
43
+ # _persistence plugin.
44
+ def rom_reload
45
+ rom.retrieve_record { |entity| yield entity }
46
+ end
47
+
48
+ # Returns true if the data attribute represents a JSON or JSONB column.
49
+ # Used by the _persistence plugin to determine whether serialization
50
+ # should be skipped.
51
+ def rom_hash_attribute?
52
+ return false unless repository
53
+
54
+ column = rom.column_type(attribute)
55
+ column && [:json, :jsonb].include?(column.to_sym)
56
+ end
57
+
58
+ # Returns whether the record is a ROM entity. Used by the _persistence
59
+ # plugin.
60
+ def rom?
61
+ record.is_a?(::ROM::Struct)
62
+ end
63
+
64
+ # Returns internal ROM wrapper object.
65
+ def rom
66
+ fail Shrine::Error, "repository is missing" unless repository
67
+
68
+ RomWrapper.new(repository: repository, record: record)
69
+ end
70
+ end
71
+
72
+ class RomWrapper
73
+ attr_reader :repository, :record_pk
74
+
75
+ def initialize(repository:, record: nil)
76
+ @repository = repository
77
+ @record_pk = record.send(relation.primary_key)
78
+ end
79
+
80
+ def update_record(attributes)
81
+ repository.update(record_pk, attributes)
82
+ end
83
+
84
+ def retrieve_record
85
+ case adapter
86
+ when :sql
87
+ repository.transaction do
88
+ yield record_relation.lock.one!
89
+ end
90
+ else
91
+ yield record_relation.one!
92
+ end
93
+ end
94
+
95
+ def column_type(attribute)
96
+ # sends "json" or "jsonb" string for JSON or JSONB column.
97
+ # returns nil for String column
98
+ relation.schema[attribute].type.meta[:db_type]
99
+ end
100
+
101
+ private
102
+
103
+ def record_relation
104
+ case adapter
105
+ when :sql, :mongo then relation.by_pk(record_pk)
106
+ when :elasticsearch then relation.get(record_pk)
107
+ else
108
+ fail Shrine::Error, "unsupported ROM adapter: #{adapter.inspect}"
109
+ end
110
+ end
111
+
112
+ def adapter
113
+ relation.adapter
114
+ end
115
+
116
+ def relation
117
+ repository.root
118
+ end
119
+ end
120
+ end
121
+
122
+ register_plugin(:rom, Rom)
123
+ end
124
+ end
@@ -0,0 +1,23 @@
1
+ Gem::Specification.new do |gem|
2
+ gem.name = "shrine-rom"
3
+ gem.version = "0.1.0"
4
+
5
+ gem.required_ruby_version = ">= 2.3"
6
+
7
+ gem.summary = "Provides rom-rb integration for Shrine."
8
+ gem.homepage = "https://github.com/shrinerb/shrine-rom"
9
+ gem.authors = ["Janko Marohnić", "Ahmad Musaffa"]
10
+ gem.email = ["janko.marohnic@gmail.com", "musaffa_csemm@yahoo.com"]
11
+ gem.license = "MIT"
12
+
13
+ gem.files = Dir["README.md", "LICENSE.txt", "lib/**/*.rb", "*.gemspec"]
14
+ gem.require_path = "lib"
15
+
16
+ gem.add_dependency "shrine", "~> 3.0"
17
+ gem.add_dependency "rom", "~> 5.0"
18
+
19
+ gem.add_development_dependency "rake"
20
+ gem.add_development_dependency "minitest"
21
+ gem.add_development_dependency "rom-sql"
22
+ gem.add_development_dependency "sqlite3"
23
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shrine-rom
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Janko Marohnić
8
+ - Ahmad Musaffa
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2022-06-06 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: shrine
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '3.0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '3.0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rom
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '5.0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '5.0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rake
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: minitest
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: rom-sql
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: sqlite3
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ description:
99
+ email:
100
+ - janko.marohnic@gmail.com
101
+ - musaffa_csemm@yahoo.com
102
+ executables: []
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - LICENSE.txt
107
+ - README.md
108
+ - lib/shrine/plugins/rom.rb
109
+ - shrine-rom.gemspec
110
+ homepage: https://github.com/shrinerb/shrine-rom
111
+ licenses:
112
+ - MIT
113
+ metadata: {}
114
+ post_install_message:
115
+ rdoc_options: []
116
+ require_paths:
117
+ - lib
118
+ required_ruby_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '2.3'
123
+ required_rubygems_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ requirements: []
129
+ rubygems_version: 3.3.3
130
+ signing_key:
131
+ specification_version: 4
132
+ summary: Provides rom-rb integration for Shrine.
133
+ test_files: []