lib/materialist/materializer.rb in materialist-0.0.4 vs lib/materialist/materializer.rb in materialist-0.0.5
- old
+ new
@@ -1,17 +1,21 @@
require 'active_support/inflector'
require 'routemaster/api_client'
+require_relative './event_worker'
module Materialist
module Materializer
def self.included(base)
base.extend(Internals::ClassMethods)
base.extend(Internals::DSL)
root_mapping = []
- base.instance_variable_set(:@materialist_options, { mapping: root_mapping })
+ base.instance_variable_set(:@__materialist_options, {
+ mapping: root_mapping,
+ links_to_materialize: {}
+ })
base.instance_variable_set(:@__materialist_dsl_mapping_stack, [root_mapping])
end
module Internals
class FieldMapping
@@ -31,20 +35,24 @@
attr_reader :key, :mapping
end
module ClassMethods
- attr_reader :materialist_options, :__materialist_dsl_mapping_stack
+ attr_reader :__materialist_options, :__materialist_dsl_mapping_stack
def perform(url, action)
materializer = Materializer.new(url, self)
action == :delete ? materializer.destroy : materializer.upsert
end
end
module DSL
+ def materialize_link(key, topic: key)
+ __materialist_options[:links_to_materialize][key] = { topic: topic }
+ end
+
def materialize(key, as: key)
__materialist_dsl_mapping_stack.last << FieldMapping.new(key: key, as: as)
end
def link(key)
@@ -54,34 +62,40 @@
yield
__materialist_dsl_mapping_stack.pop
end
def use_model(klass)
- materialist_options[:model_class] = klass
+ __materialist_options[:model_class] = klass
end
def after_upsert(method_name)
- materialist_options[:after_upsert] = method_name
+ __materialist_options[:after_upsert] = method_name
end
def after_destroy(method_name)
- materialist_options[:after_destroy] = method_name
+ __materialist_options[:after_destroy] = method_name
end
end
class Materializer
def initialize(url, klass)
@url = url
@instance = klass.new
- @options = klass.materialist_options
+ @options = klass.__materialist_options
end
def upsert(retry_on_race_condition: true)
- upsert_record.tap do |entity|
- instance.send(after_upsert, entity) if after_upsert
+ return unless root_resource
+
+ if materialize_self?
+ upsert_record.tap do |entity|
+ instance.send(after_upsert, entity) if after_upsert
+ end
end
+
+ materialize_links
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
# when there is a race condition and uniqueness of :source_url
# is enforced by database index, this error is raised
# so we simply try upsert again
# if error is due to another type of uniqueness constraint
@@ -90,10 +104,11 @@
upsert(retry_on_race_condition: false) :
raise
end
def destroy
+ return unless materialize_self?
model_class.find_by(source_url: url).tap do |entity|
entity.destroy!.tap do |entity|
instance.send(after_destroy, entity) if after_destroy
end if entity
end
@@ -101,17 +116,39 @@
private
attr_reader :url, :instance, :options
+ def materialize_self?
+ options.include? :model_class
+ end
+
def upsert_record
model_class.find_or_initialize_by(source_url: url).tap do |entity|
entity.update_attributes attributes
entity.save!
end
end
+ def materialize_links
+ (options[:links_to_materialize] || [])
+ .each { |key, opts| materialize_link(key, opts) }
+ end
+
+ def materialize_link(key, opts)
+ return unless root_resource.body._links.include?(key)
+
+ # this can't happen asynchronously
+ # because the handler options are unavailable in this context
+ # :(
+ ::Materialist::EventWorker.new.perform({
+ 'topic' => opts[:topic],
+ 'url' => root_resource.body._links[key].href,
+ 'type' => 'noop'
+ })
+ end
+
def mapping
options.fetch :mapping
end
def after_upsert
@@ -125,33 +162,40 @@
def model_class
options.fetch(:model_class).to_s.camelize.constantize
end
def attributes
- build_attributes resource_at(url), mapping
+ build_attributes root_resource, mapping
end
+ def root_resource
+ @_root_resource ||= resource_at(url)
+ end
+
def build_attributes(resource, mapping)
return {} unless resource
mapping.inject({}) do |result, m|
case m
when FieldMapping
result.tap { |r| r[m.as] = resource.body[m.key] }
when LinkMapping
resource.body._links.include?(m.key) ?
- result.merge(build_attributes(resource_at(resource.send(m.key).url, allow_nil: true), m.mapping || [])) :
+ result.merge(build_attributes(resource_at(resource.send(m.key).url), m.mapping || [])) :
result
else
result
end
end
end
- def resource_at(url, allow_nil: false)
+ def resource_at(url)
api_client.get(url, options: { enable_caching: false })
rescue Routemaster::Errors::ResourceNotFound
- raise unless allow_nil
+ # this is due to a race condition between an upsert event
+ # and a :delete event
+ # when this happens we should silently ignore the case
+ nil
end
def api_client
@_api_client ||= Routemaster::APIClient.new(
response_class: Routemaster::Responses::HateoasResponse