Skip to content

Instantly share code, notes, and snippets.

@janko
Created July 31, 2016 11:24
Show Gist options
  • Save janko/13cd8b13a28a5c2953ef15f7d2ac71b8 to your computer and use it in GitHub Desktop.
Save janko/13cd8b13a28a5c2953ef15f7d2ac71b8 to your computer and use it in GitHub Desktop.
Using Shrine with ROM and dry-rb
source "https://rubygems.org"
gem "shrine", github: "janko-m/shrine"
gem "rom-repository"
gem "rom-sql"
gem "sqlite3"
gem "dry-validation"
gem "roda"
gem "sucker_punch", "~> 2.0"
gem "rack-test_app"
gem "pry"
require "rom-sql"
require "rom-repository"
$rom = ROM.container(:sql, "sqlite::memory") do |conf|
conf.default.create_table(:articles) do
primary_key :id
column :title, :string
column :body, :text
column :image_data, :text
end
end
module Relations
class Articles < ROM::Relation[:sql]
schema(infer: true)
end
end
module Repositories
class Articles < ROM::Repository[:articles]
commands :create, update: :by_pk, delete: :by_pk
def by_id(id)
articles.where(id: id).as(Article).one
end
end
end
require "dry-types"
require "dry-validation"
module Types
include Dry::Types.module
end
class Article < Dry::Types::Struct
attribute :id, Types::Int
attribute :title, Types::String
attribute :body, Types::String
attribute :image_data, Types::String
attr_writer :image_data # for Shrine
end
ArticleSchema = Dry::Validation.Form do
required(:title).filled
required(:body).filled
required(:image).filled
end
module Presenters
class Article
def initialize(article)
@article = article
end
def to_h
{
id: @article.id,
title: @article.title,
body: @article.body,
image: {
url: image.url,
metadata: image.metadata,
},
}
end
private
def image
@image ||= ImageUploader.uploaded_file(@article.image_data)
end
end
end
require "shrine"
require "shrine/storage/file_system"
require "sucker_punch"
require "sucker_punch/testing/inline" # synchronous jobs
Shrine.storages = {
cache: Shrine::Storage::FileSystem.new("uploads/cache"),
store: Shrine::Storage::FileSystem.new("uploads/store"),
}
Shrine.plugin :rack_file
Shrine.plugin :logging
Shrine.plugin :determine_mime_type
Shrine.plugin :validation_helpers
Shrine.plugin :backgrounding
Shrine::Attacher.promote { |data| PromoteJob.perform_async(data) }
Shrine::Attacher.delete { |data| DeleteJob.perform_async(data) }
class PromoteJob
include SuckerPunch::Job
def perform(data)
Shrine::Attacher.promote(data)
end
end
class DeleteJob
include SuckerPunch::Job
def perform(data)
Shrine::Attacher.delete(data)
end
end
class ImageUploader < Shrine
Attacher.validate do
validate_extension_inclusion ["jpg"]
end
end
module Shrine::Plugins::Rom
def self.configure(uploader, opts = {})
uploader.opts[:rom_repository] = opts.fetch(:repository)
end
module AttacherClassMethods
def find_record(record_class, record_id)
rom_relation(record_class).where(id: record_id).one
end
def rom_relation(record_class)
rom_repository(record_class).root.as(record_class)
end
def rom_repository(record_class)
shrine_class.opts[:rom_repository].call(record_class)
end
end
module AttacherMethods
private
def update(uploaded_file)
super
context[:record] = rom_repository.update(record.id, "#{name}_data": read)
end
def rom_repository
self.class.rom_repository(record.class)
end
end
end
Shrine.plugin Shrine::Plugins::Rom,
repository: ->(model) { Object.const_get("Repositories::#{model}s").new($rom) }
require "roda"
class App < Roda
plugin :all_verbs
plugin :json, serializer: ->(object) { JSON.pretty_generate(object) }
route do |r|
r.on "articles" do
r.is do
r.post do
attacher = ImageUploader::Attacher.new(Article.new, :image)
if r.params["image"]
attacher.assign(r.params["image"])
r.params["image"] = attacher.read
end
validation = ArticleSchema.call(r.params)
validation.messages[:image] = attacher.errors
if validation.success?
attributes = validation.output
attributes[:image_data] = attributes.delete(:image)
article = articles_repo.create(attributes)
attacher.context[:record] = Article.new(article.to_h)
attacher.finalize if attacher.attached?
Presenters::Article.new(article).to_h
else
response.status = 400
validation.messages
end
end
end
r.is ":id" do |id|
r.get do
article = articles_repo.by_id(id)
Presenters::Article.new(article).to_h
end
r.put do
article = articles_repo.by_id(id)
attacher = ImageUploader::Attacher.new(article, :image)
if r.params["image"]
attacher.assign(r.params["image"])
r.params["image"] = attacher.read
end
validation = ArticleSchema.call(article.to_h.merge(r.params))
validation.messages[:image] = attacher.errors
if validation.success?
attributes = validation.output
attributes[:image_data] = attributes.delete(:image)
article = articles_repo.update(article.id, attributes)
attacher.context[:record] = Article.new(article.to_h)
attacher.finalize if attacher.attached?
Presenters::Article.new(article).to_h
else
response.status = 400
validation.messages
end
end
r.delete do
article = articles_repo.by_id(id)
articles_repo.delete(article.id)
attacher = ImageUploader::Attacher.new(article, :image)
attacher.destroy
end
end
end
end
def articles_repo
Repositories::Articles.new($rom)
end
end
require "rack/test_app"
app = Rack::TestApp.wrap(Rack::Lint.new(App))
response = app.post("/articles", multipart: {
title: "Title",
body: "Body",
image: File.open("image.jpg", "rb"),
})
puts response.body_text
article_id = response.body_json.fetch("id")
response = app.get("/articles/#{article_id}")
puts response.body_text
response = app.put("/articles/#{article_id}", multipart: {
image: File.open("image.jpg", "rb"),
})
puts response.body_text
app.delete("/articles/#{article_id}")
puts Sequel::DATABASES.first[:articles].empty?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment