Last active
June 3, 2020 13:53
-
-
Save mariuszkapcia/f6dbcccf20c742a6f1a2bcf5f4cf9a50 to your computer and use it in GitHub Desktop.
Let's call it CommandForm for a lack of better ideas. The whole story can be found https://medium.com/@mariuszkapcia/how-to-handle-one-big-request-as-a-batch-of-small-commands-f45dc4107865?sk=01f9dddb4c8b1435398e0486296403aa.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Users | |
class ChangeAvatar < Command | |
attribute :avatar_url, Types::Strict::String | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Users | |
class ChangePassword < Command | |
attribute :password, Types::Strict::String | |
attribute :password_confirmation, Types::Strict::String | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'dry-struct' | |
class Command < Dry::Struct | |
Invalid = Class.new(StandardError) | |
def self.new(*) | |
super | |
rescue Dry::Struct::Error => error | |
raise Invalid, error | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Form | |
include ActiveModel::Model | |
include ActiveModel::Validations | |
include ActiveModel::Conversion | |
class ValidationError < StandardError | |
attr_reader :errors | |
def initialize(errors) | |
@errors = errors | |
end | |
end | |
def self.validation_options(options) | |
@options ||= {} | |
@options.merge!(options) | |
end | |
def self.command(cmd_class) | |
cmd_attributes = cmd_class.schema.keys.map(&:name) | |
cmd_attributes.each do |attribute| | |
send(:attr_accessor, attribute) | |
end | |
@attributes ||= [] | |
@attributes += cmd_attributes | |
@attributes = @attributes.uniq | |
@commands ||= {} | |
@commands[cmd_class] = cmd_attributes | |
end | |
class << self | |
alias_method :activemodel_validates, :validates | |
alias_method :activemodel_validate, :validate | |
end | |
def self.validates(*attributes) | |
@validations ||= [] | |
@validations.push(attributes) | |
self | |
end | |
def self.validate(*args, &block) | |
if args[0].class.eql?(Symbol) | |
@custom_validations ||= [] | |
@custom_validations.push(args) | |
self | |
else | |
super | |
end | |
end | |
def self.options | |
@options ||= { skip_unchanged_attributes: false } | |
end | |
def self.attributes | |
@attributes | |
end | |
def self.commands | |
@commands | |
end | |
def self.validations | |
@validations | |
end | |
def self.custom_validations | |
@custom_validations | |
end | |
def options | |
self.class.options | |
end | |
def attributes | |
self.class.attributes | |
end | |
def commands | |
self.class.commands | |
end | |
def validations | |
self.class.validations | |
end | |
def custom_validations | |
self.class.custom_validations | |
end | |
def verify! | |
return if valid? | |
raise ValidationError, errors.to_hash.map { |k,v| v }.flatten | |
end | |
def valid? | |
self.class.clear_validators! | |
skip = options[:skip_unchanged_attributes] | |
validations.each do |validation| | |
next if skip && validation[0].in?(@changed_attributes) | |
self.class.activemodel_validates(*validation) | |
end | |
custom_validations.each do |validation| | |
next if skip && validation[0].in?(@changed_attributes) | |
self.class.activemodel_validate(*validation[1]) | |
end | |
super | |
end | |
def apply_changes(params) | |
params.each do |attribute, value| | |
attribute = attribute.to_sym | |
next unless attribute.in?(attributes) | |
if value_changed?(attribute, value) | |
@changed_attributes += [attribute] | |
end | |
assign_value(attribute, value) | |
end | |
self | |
end | |
def build_commands! | |
commands.map do |command, attributes| | |
next if (@changed_attributes & attributes).length.eql?(0) | |
build_command(command, attributes) | |
end.compact | |
end | |
private | |
def initialize(object = nil) | |
@changed_attributes = [] | |
attributes.each do |attribute| | |
assign_value(attribute, object.try(:[], attribute)) | |
end | |
end | |
def build_command(command, attributes) | |
attributes = attributes.map do |attribute| | |
[attribute, fetch_value(attribute)] | |
end | |
command.new(Hash[attributes]) | |
end | |
def value_changed?(attribute, new_value) | |
send(attribute) != new_value | |
end | |
def assign_value(attribute, value) | |
send("#{attribute}=", value) | |
end | |
def fetch_value(attribute) | |
send(attribute) | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ProfileForm | |
command Users::ChangeAvatar | |
command Users::ChangePassword | |
validation_options skip_unchanged_attributes: true | |
validates :avatar_url, | |
presence: { message: 'avatar_url_is_missing' } | |
validates :password, | |
presence: { message: 'password_is_missing' } | |
validates :password_confirmation, | |
presence: { message: 'password_confirmation_is_missing' } | |
validate :password_confirmation_match | |
private | |
def password_confirmation_match | |
if !password.eql?(password_confirmation) | |
errors.add(:password_confirmation, 'password_confirmation_mismatch') | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment