Snapi

Section 3

Curator

Curator

Responsibilities

  1. Takes a default response and transforms it over a series of functions.

  2. Finds/Searches/Builds into the response.collection

  3. Validates response.collection

  4. Authorizes response.collection

  5. Creates/Updates/Deletes into the database

  6. Publishes messages to rabbitmq

  7. Decorates response.collection

  8. 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

  1. Takes a default response and transforms it
    over a series of functions

  2. Finds/Searches/Builds into the response.collection

  3. Validates response.collection

  4. Authorizes response.collection

  5. Creates/Updates/Deletes into the database

  6. Publishes messages to rabbitmq

  7. Decorates response.collection

  8. Any other business logic

Thank you!

Snapi Section 03 Curator

By Dustin McCraw

Snapi Section 03 Curator

  • 868