Snapi
Section 3
Curator
Curator
Responsibilities
-
Takes a default response and transforms it over a series of functions.
-
Finds/Searches/Builds into the response.collection
-
Validates response.collection
-
Authorizes response.collection
-
Creates/Updates/Deletes into the database
-
Publishes messages to rabbitmq
-
Decorates response.collection
-
Etc...
Concepts
- BaseCurator
- include (magic)
- default_response
- pipe
class ContactCurator < BaseCurator
include Mufasa.validation(ContactAffirmation)
include Omicron.authorization(ContactAuthorizer)
include Omicron.construction(ContactBuilder)
include Omicron.decoration(ContactDecorator)
include Omicron.destruction(ContactMapper)
include Omicron.publishing(ContactPublisher)
include Mufasa.find(ContactMapper)
include Mufasa.search(
ContactMapper,
:search_conditions => [:id, :team_id, :member_id, :user_id],
:boolean_search_params => [:can_receive_push_notifications]
)
include Omicron.persistance(
ContactMapper, :location => "/contacts/search?id="
)
include ContactAppHelpers
def search_action
pipe(default_response, :through => [
:search, :filter_readable, :apply_readables, :set_is_editable, :decorate
])
end
def show_action
pipe(default_response, :through => [
:find, :authorize_to_read, :apply_readables, :set_is_editable, :decorate
])
end
end
BaseCurator
- BaseApp < Mufasa::Curator
-
Provides functions like
- authentication_response
- current_user
- default_response
- oauth_application
- template
BaseCurator
class BaseCurator < Mufasa::Curator
def authenticated?
!!authentication_response
end
def authentication_response
artifact.authentication_response
end
def current_user
if authenticated? && authentication_response.user
authentication_response.user
else
NullUser.new
end
end
def oauth_application
authentication_response.oauth_application
end
Include Magic
Omicron.include and Mufasa.include inject functions into the curator.
Those injected function call other functions in the class provided in the include.
The tricky part is the function injected doesn't always match the function being called in the class being included.
include Mufasa.validation(ContactAffirmation)
include Omicron.authorization(ContactAuthorizer)
include Omicron.construction(ContactBuilder)
include Omicron.decoration(ContactDecorator)
include Omicron.destruction(ContactMapper)
include Omicron.publishing(ContactPublisher)
include Mufasa.find(ContactMapper)
include Mufasa.search(
ContactMapper,
:search_conditions => [:id, :team_id, :member_id, :user_id],
:boolean_search_params => [:can_receive_push_notifications]
)
include Omicron.persistance(
ContactMapper, :location => "/contacts/search?id="
)
include Mufasa.validation(ContactAffirmation) ===> validate
include Omicron.authorization(ContactAuthorizer)
===> filter_readable, authorize_to_read, authorize_to_write
include Omicron.construction(ContactBuilder) ===> build
include Omicron.decoration(ContactDecorator) ===> decorate
include Omicron.destruction(ContactMapper) ===> delete
include Omicron.publishing(ContactPublisher) ===> publish_xxxx
include Mufasa.find(ContactMapper) ===> find
include Mufasa.search(
ContactMapper, :search_conditions => [:id, :team_id, :member_id],
) ===> search
include Omicron.persistance(
ContactMapper, :location => "/contacts/search?id="
) ===> create, update
Include Magic
class ContactCurator < BaseCurator
include Mufasa.validation(ContactAffirmation)
include Omicron.authorization(ContactAuthorizer)
include Omicron.construction(ContactBuilder)
include Omicron.decoration(ContactDecorator)
include Omicron.destruction(ContactMapper)
include Omicron.publishing(ContactPublisher)
include Mufasa.find(ContactMapper)
include Mufasa.search(
ContactMapper,
:search_conditions => [:id, :team_id, :member_id, :user_id],
:boolean_search_params => [:can_receive_push_notifications]
)
include Omicron.persistance(
ContactMapper, :location => "/contacts/search?id="
)
include ContactAppHelpers
def search_action
pipe(default_response, :through => [
:search, :filter_readable, :apply_readables, :set_is_editable, :decorate
])
end
def show_action
pipe(default_response, :through => [
:find, :authorize_to_read, :apply_readables, :set_is_editable, :decorate
])
end
end
default_response
class Response
include Virtus.value_object
values do
attribute :status, Integer, :default => 200
attribute :headers, Hash, :default => {}
attribute :collection, Array, :default => []
attribute :original_collection, Array, :default => []
attribute :page_information, Hash
attribute :error_message, String
end
end
default_response just returns a Response.new
class ContactCurator < BaseCurator
def search_action
pipe(default_response, :through => [
:search, :filter_readable, :apply_readables, :set_is_editable, :decorate
])
end
def show_action
pipe(default_response, :through => [
:find, :authorize_to_read, :apply_readables, :set_is_editable, :decorate
])
end
def show_action
pipe(default_response, :through => [
:find, :authorize_to_read, :apply_readables, :set_is_editable, :decorate
])
end
def create_action
pipe(default_response, :through => [
:build, :validate, :authorize_to_write, :create, :block_suspicious_names,
:apply_readables, :publish_create, :decorate
])
end
def delete_action
pipe(default_response, :through => [
:find, :authorize_to_write, :publish_delete, :delete
])
end
end
pipe
def search_action
pipe(default_response, :through => [
:search, :filter_readable, :apply_readables,
:set_is_editable, :decorate
])
end
pipe function signature:
def function(response)
It must return a response
pipe allows us to transform a response over multiple functions in a specific order.
pipe stops if any !response.success? occurs.
pipe
pipe is configured in Mufasa::Curator.
It will stop processing if response.success? == false
pipe (cont.)
def validate_user_id(response)
if user_id == :not_provided
response.with(
:status => 400,
:error_message => I18n.t(
"errors.endpoints.teams.active.invalid"
)
)
else
response
end
end
Here is a function that is called that isn't included but lives in the curator.
Notice the response.with because of the read-only nature of the response.
pipe (cont.)
def block_suspicious_names(response)
response.tap {
response.collection.each do |object|
if has_suspicious_string?(object)
Spamalot::Blacklist.new(current_user.id).hellban
Spamalot::BlockedIpAddresses.block!(remote_ip)
Spamalot::Slack
.delay
.notify_contact_hellban(current_user)
end
end
}
end
More business logic that exists only in the curator. Notice the use of response.tap.
This allows us to return response automatically.
Curator
Responsibilities
-
Takes a default response and transforms it
over a series of functions -
Finds/Searches/Builds into the response.collection
-
Validates response.collection
-
Authorizes response.collection
-
Creates/Updates/Deletes into the database
-
Publishes messages to rabbitmq
-
Decorates response.collection
-
Any other business logic
Thank you!
Snapi Section 03 Curator
By Dustin McCraw
Snapi Section 03 Curator
- 868