Skip to content

Instantly share code, notes, and snippets.

@airhorns
Last active June 12, 2019 23:48
Show Gist options
  • Save airhorns/19e3126d62768217413191f69f746ccf to your computer and use it in GitHub Desktop.
Save airhorns/19e3126d62768217413191f69f746ccf to your computer and use it in GitHub Desktop.
Example Ruby files for RnD Camp Style Exercise
class Todo
def initialize(*argv)
@argv = argv
begin
send(@argv[0])
rescue TypeError
list
rescue NoMethodError
puts "Command #{@argv[0]} not found.\n\n"
help
end
end
# Lists help information
def help
puts <<-help
Commands for Todo.rb:
add [task name] - Add a new task
list - Lists all tasks
done [task id] - Complete a task
help - Prints out this information
help
end
# Add task
def add
unless @argv[1]
puts "Lacking argument [name]"
exit
end
# Append task to file
contents = File.read('todo.td')
File.open('todo.td', 'w') do |f|
todo = contents + @argv[1] + "\n"
f.write(todo)
end
end
# List all tasks
def list
# Read content
contents = File.read('todo.td')
puts "No tasks" unless contents
# Show it with ids
i = 0
contents.each_line do |todo|
i += 1
puts "##{i} - #{todo}"
end
end
# Finished a task
def done
unless @argv[1]
puts "Lacking argument [id]"
exit
end
# Put tasks into an array
todos = File.read('todo.td').split("\n")
unless todos
puts "No tasks"
exit
end
puts "Completed task: " + todos[@argv[1].to_i - 1]
# Delete task from array and make string
todos.delete_at(@argv[1].to_i - 1)
content = todos.join("\n")
# Update file
File.open('todo.td', 'w') do |f|
f.write(content)
end
end
end
todo = Todo.new(*ARGV)
module Thwart
class DuplicateRoleError < StandardError; end
class MissingRoleError < StandardError; end
# Internal class for handling the storage, adoption, and traversal of roles and role trees.
class RoleRegistry
def initialize(role_creator = nil)
self.monitor_builder(role_creator) if !role_creator.nil?
self
end
def roles
@roles ||= []
@roles
end
# Adds a uniquely named role to the registry.
# @param {Thwart::Role} role The role to be added. Must have a unique name.
def add(role)
raise DuplicateRoleError, "Role #{role} already exists in the role registry!" if self.has_role?(role)
@roles << role
end
# Boolean describing if a role exists in the registry.
# @param {Thwart::Role} the role to search for.
def has_role?(role)
self.roles.include?(role)
end
# Queries the tree of roles to see if any have applicable rules and returns their result if they do.
# Optionally logs the query path.
# @param {Thwart::Actor} actor The actor trying to perform the action on the resource
# @param {Thwart::Resource|Class|Symbol} resource The resource to which the actor might act upon
# @param {Symbol} action The name of the action being attempted
# @return {true|false} A boolean describing weather or not the actor can preform the action on the resource.
def query(actor, resource, action)
role = self.find_actor_role(actor)
# logging setup
if Thwart.log_query_path
Thwart.last_query_path = []
Thwart.last_query_path.push({:actor => actor, :resource => resource, :action => action})
name = resource.thwart_name if resource.respond_to?(:thwart_name)
name ||= resource
Thwart.last_query_path.push({:actor_role => role.name, :resource_name => resource})
end
if role.nil? || !self.has_role?(role)
raise MissingRoleError, "Role #{role} could not be found in the registry!" if Thwart.actor_must_play_role
else
# Start DFS
q = [role]
while r = q.shift
resp = r.query(actor, resource, action)
if Thwart.log_query_path
Thwart.last_query_path.push("Querying #{r.name}")
Thwart.last_query_path.push(r)
Thwart.last_query_path.push("Response: #{resp}")
end
if resp != nil
return resp # positive/negative response from the role, a rule governs the role on this query
else
# nil response from the role, it has nothing to say about this query
q = q | r.parents.map do |a|
a = self.find_role(a) if a.is_a?(Symbol)
a
end # add this roles parents to the query queue
end
end
end
Thwart.default_query_response # return was not called above, return the default
end
# Method to get the `Thwart::Role` class played by an actor.
# @param {Thwart::Actor} actor The actor in question
# @return {Thwart::Role} The role class the actor plays.
def find_actor_role(actor)
r = actor.thwart_role if actor.respond_to?(:thwart_role)
r = r.to_sym if r.respond_to?(:to_sym)
r = find_role(r) if r.is_a?(Symbol)
r
end
# Adds callback hooks to add all built roles to the registry.
# @private
def monitor_builder(role_creator)
registry = self
unless role_creator.nil?
role_creator.class.set_callback :build_role, :after do |object|
registry.add(object.last_built_role)
end
else
@role_creator.class.reset_callbacks(:build_role)
end
@role_creator = role_creator
end
# Finds a role based on a name
# @private
def find_role(name)
self.roles.find {|a| a.name == name}
end
end
end
# Protip from Harry, don't tell Jace: There's a tool for Ruby called Rubocop that will tell you all the style issues with a file
# and will even autocorrect most of them for you. You can find Shopify's default Rubocop configuration here: https://github.com/Shopify/ruby-style-guide/blob/master/rubocop.yml
# and you can install the rubocop gem by running `gem install rubocop`. If you run `rubocop some_file.rb` that you have handy on your
# filesystem, it'll print out all your errors for you, and if you run `rubocop -a some_file.rb`, it will fix it up for you. Impress
# your RnD Camp counsellor by getting this one done quickly, but no spilling the beans!!
module IdentityCache
module QueryAPI
extend ActiveSupport::Concern
included do |base|
base.after_commit :expire_cache
end
module ClassMethods
# Similar to ActiveRecord::Base#exists? will return true if the id can be
# found in the cache or in the DB.
def exists_with_identity_cache?(id)
raise NotImplementedError, "exists_with_identity_cache? needs the primary index enabled" unless primary_cache_index_enabled
!!fetch_by_id(id)
end
# Default fetcher added to the model on inclusion, it behaves like
# ActiveRecord::Base.where(id: id).first
def fetch_by_id(id, includes: nil)
ensure_base_model
raise_if_scoped
raise NotImplementedError, "fetching needs the primary index enabled" unless primary_cache_index_enabled
id = type_for_attribute(primary_key).cast(id)
return unless id
record = if should_use_cache?
require_if_necessary do
object = nil
coder = IdentityCache.fetch(rails_cache_key(id)){ instrumented_coder_from_record(object = resolve_cache_miss(id)) }
object ||= instrumented_record_from_coder(coder)
if object && object.id != id
IdentityCache.logger.error "[IDC id mismatch] fetch_by_id_requested=#{id} fetch_by_id_got=#{object.id} for #{object.inspect[(0..100)]}"
end
object
end
else
resolve_cache_miss(id)
end
prefetch_associations(includes, [record]) if record && includes
record
end
# Default fetcher added to the model on inclusion, it behaves like
# ActiveRecord::Base.find, will raise ActiveRecord::RecordNotFound exception
# if id is not in the cache or the db.
def fetch(id, includes: nil)
fetch_by_id(id, includes: includes) or raise(ActiveRecord::RecordNotFound, "Couldn't find #{self.name} with ID=#{id}")
end
# Default fetcher added to the model on inclusion, if behaves like
# ActiveRecord::Base.find_all_by_id
def fetch_multi(*ids, includes: nil)
ensure_base_model
raise_if_scoped
raise NotImplementedError, "fetching needs the primary index enabled" unless primary_cache_index_enabled
ids.flatten!(1)
id_type = type_for_attribute(primary_key)
ids.map! { |id| id_type.cast(id) }.compact!
records = if should_use_cache?
require_if_necessary do
cache_keys = ids.map {|id| rails_cache_key(id) }
key_to_id_map = Hash[ cache_keys.zip(ids) ]
key_to_record_map = {}
coders_by_key = IdentityCache.fetch_multi(cache_keys) do |unresolved_keys|
ids = unresolved_keys.map {|key| key_to_id_map[key] }
records = find_batch(ids)
key_to_record_map = records.compact.index_by{ |record| rails_cache_key(record.id) }
records.map {|record| instrumented_coder_from_record(record) }
end
cache_keys.map{ |key| key_to_record_map[key] || instrumented_record_from_coder(coders_by_key[key]) }
end
else
find_batch(ids)
end
records.compact!
prefetch_associations(includes, records) if includes
records
end
def prefetch_associations(associations, records)
records = records.to_a
return if records.empty?
case associations
when nil
# do nothing
when Symbol
prefetch_one_association(associations, records)
when Array
associations.each do |association|
prefetch_associations(association, records)
end
when Hash
associations.each do |association, sub_associations|
next_level_records = prefetch_one_association(association, records)
if sub_associations.present?
associated_class = reflect_on_association(association).klass
associated_class.prefetch_associations(sub_associations, next_level_records)
end
end
else
raise TypeError, "Invalid associations class #{associations.class}"
end
nil
end
private
def raise_if_scoped
if current_scope
IdentityCache.logger.error("#{name} has scope: #{current_scope.to_sql} (#{current_scope.values.keys})")
raise UnsupportedScopeError, "IdentityCache doesn't support rails scopes (#{name})"
end
end
def check_association_scope(association_name)
association_reflection = reflect_on_association(association_name)
scope = association_reflection.scope
if scope && !association_reflection.klass.all.instance_exec(&scope).joins_values.empty?
raise UnsupportedAssociationError, "caching association #{self}.#{association_name} scoped with a join isn't supported"
end
end
def instrumented_record_from_coder(coder) #:nodoc:
return unless coder
ActiveSupport::Notifications.instrument('identity_cache.hydration', class: coder[:class]) do
record_from_coder(coder)
end
end
def record_from_coder(coder) #:nodoc:
if coder
klass = coder[:class].constantize
record = klass.instantiate(coder[:attributes].dup)
coder[:associations].each do |name, value|
set_embedded_association(record, name, hydrate_association_target(value))
end if coder.has_key?(:associations)
coder[:association_ids].each {|name, ids| record.instance_variable_set(:"@#{record.class.cached_has_manys[name][:ids_variable_name]}", ids) } if coder.has_key?(:association_ids)
record.readonly! if IdentityCache.fetch_read_only_records
record
end
end
def hydrate_association_target(dehydrated_value)
dehydrated_value = IdentityCache.unmap_cached_nil_for(dehydrated_value)
if dehydrated_value.is_a?(Array)
dehydrated_value.map { |coder| record_from_coder(coder) }
else
record_from_coder(dehydrated_value)
end
end
def set_embedded_association(record, association_name, association_target) #:nodoc:
model = record.class
association_options = model.send(:cached_association_options, association_name)
reflection = model.reflect_on_association(association_name)
set_inverse_of_cached_association(record, association_options, association_target)
prepopulate_method_name = association_options.fetch(:prepopulate_method_name)
record.send(prepopulate_method_name, association_target)
end
def set_inverse_of_cached_association(record, association_options, association_target)
return if association_target.nil?
associated_class = association_options.fetch(:association_reflection).klass
inverse_name = association_options.fetch(:inverse_name)
inverse_cached_association = associated_class.cached_belongs_tos[inverse_name]
return unless inverse_cached_association
prepopulate_method_name = inverse_cached_association.fetch(:prepopulate_method_name)
if association_target.is_a?(Array)
association_target.each { |child_record| child_record.send(prepopulate_method_name, record) }
else
association_target.send(prepopulate_method_name, record)
end
end
def get_embedded_association(record, association, options) #:nodoc:
embedded_variable = record.public_send(options.fetch(:cached_accessor_name))
if embedded_variable.respond_to?(:to_ary)
embedded_variable.map {|e| coder_from_record(e) }
else
coder_from_record(embedded_variable)
end
end
def instrumented_coder_from_record(record) #:nodoc:
return unless record
ActiveSupport::Notifications.instrument('identity_cache.dehydration', class: record.class.name) do
coder_from_record(record)
end
end
def coder_from_record(record) #:nodoc:
unless record.nil?
coder = {
attributes: record.attributes_before_type_cast.dup,
class: record.class.name,
}
add_cached_associations_to_coder(record, coder)
coder
end
end
def add_cached_associations_to_coder(record, coder)
klass = record.class
if (recursively_embedded_associations = klass.send(:recursively_embedded_associations)).present?
coder[:associations] = recursively_embedded_associations.each_with_object({}) do |(name, options), hash|
hash[name] = IdentityCache.map_cached_nil_for(get_embedded_association(record, name, options))
end
end
if (cached_has_manys = klass.cached_has_manys).present?
coder[:association_ids] = cached_has_manys.each_with_object({}) do |(name, options), hash|
hash[name] = record.instance_variable_get(:"@#{options[:ids_variable_name]}") unless options[:embed] == true
end
end
end
def require_if_necessary #:nodoc:
# mem_cache_store returns raw value if unmarshal fails
rval = yield
case rval
when String
rval = Marshal.load(rval)
when Array
rval.map!{ |v| v.kind_of?(String) ? Marshal.load(v) : v }
end
rval
rescue ArgumentError => e
if e.message =~ /undefined [\w\/]+ (\w+)/
ok = Kernel.const_get($1) rescue nil
retry if ok
end
raise
end
def resolve_cache_miss(id)
record = self.includes(cache_fetch_includes).reorder(nil).where(primary_key => id).first
if record
preload_id_embedded_associations([record])
record.readonly! if IdentityCache.fetch_read_only_records && should_use_cache?
end
record
end
def preload_id_embedded_associations(records)
return if records.empty?
each_id_embedded_association do |options|
reflection = options.fetch(:association_reflection)
child_model = reflection.klass
scope = child_model.all
scope = scope.instance_exec(nil, &reflection.scope) if reflection.scope
pairs = scope.where(reflection.foreign_key => records.map(&:id)).pluck(reflection.foreign_key, reflection.association_primary_key)
ids_by_parent = Hash.new{ |hash, key| hash[key] = [] }
pairs.each do |parent_id, child_id|
ids_by_parent[parent_id] << child_id
end
records.each do |parent|
child_ids = ids_by_parent[parent.id]
parent.instance_variable_set(:"@#{options.fetch(:ids_variable_name)}", child_ids)
end
end
recursively_embedded_associations.each_value do |options|
child_model = options.fetch(:association_reflection).klass
child_records = records.flat_map(&options.fetch(:cached_accessor_name).to_sym).compact
child_model.send(:preload_id_embedded_associations, child_records)
end
end
def cached_association_options(name)
cached_has_manys[name] || cached_has_ones[name] || cached_belongs_tos.fetch(name)
end
def each_id_embedded_association
cached_has_manys.each_value do |options|
yield options if options.fetch(:embed) == :ids
end
end
def recursively_embedded_associations
all_cached_associations.select do |cached_association, options|
options[:embed] == true
end
end
def all_cached_associations
cached_has_manys.merge(cached_has_ones).merge(cached_belongs_tos)
end
def embedded_associations
all_cached_associations.select do |cached_association, options|
options[:embed]
end
end
def cache_fetch_includes
associations_for_identity_cache = recursively_embedded_associations.map do |child_association, options|
child_class = reflect_on_association(child_association).try(:klass)
child_includes = child_class.send(:cache_fetch_includes)
if child_includes.blank?
child_association
else
{ child_association => child_includes }
end
end
associations_for_identity_cache.compact
end
def find_batch(ids)
return [] if ids.empty?
@id_column ||= columns.detect {|c| c.name == primary_key}
ids = ids.map{ |id| connection.type_cast(id, @id_column) }
records = where(primary_key => ids).includes(cache_fetch_includes).to_a
records.each(&:readonly!) if IdentityCache.fetch_read_only_records && should_use_cache?
preload_id_embedded_associations(records)
records_by_id = records.index_by(&:id)
ids.map{ |id| records_by_id[id] }
end
def fetch_embedded_associations(records)
associations = embedded_associations
return if associations.empty?
return unless primary_cache_index_enabled
cached_records_by_id = fetch_multi(records.map(&:id)).index_by(&:id)
associations.each_value do |options|
records.each do |record|
next unless cached_record = cached_records_by_id[record.id]
if options[:embed] == :ids
cached_association = cached_record.public_send(options.fetch(:cached_ids_name))
record.instance_variable_set(:"@#{options.fetch(:ids_variable_name)}", cached_association)
else
cached_association = cached_record.public_send(options.fetch(:cached_accessor_name))
record.instance_variable_set(:"@#{options.fetch(:records_variable_name)}", cached_association)
end
end
end
end
def prefetch_embedded_association(records, association, details)
# Make the same assumption as ActiveRecord::Associations::Preloader, which is
# that all the records have the same associations loaded, so we can just check
# the first record to see if an association is loaded.
first_record = records.first
return if first_record.association(association).loaded?
iv_name_key = details[:embed] == true ? :records_variable_name : :ids_variable_name
return if first_record.instance_variable_defined?(:"@#{details[iv_name_key]}")
fetch_embedded_associations(records)
end
def prefetch_one_association(association, records)
unless records.first.class.should_use_cache?
ActiveRecord::Associations::Preloader.new.preload(records, association)
return
end
case
when details = cached_has_manys[association]
prefetch_embedded_association(records, association, details)
if details[:embed] == true
child_records = records.flat_map(&details[:cached_accessor_name].to_sym)
else
ids_to_parent_record = records.each_with_object({}) do |record, hash|
child_ids = record.send(details[:cached_ids_name])
child_ids.each do |child_id|
hash[child_id] = record
end
end
parent_record_to_child_records = Hash.new { |h, k| h[k] = [] }
child_records = details[:association_reflection].klass.fetch_multi(*ids_to_parent_record.keys)
child_records.each do |child_record|
parent_record = ids_to_parent_record[child_record.id]
parent_record_to_child_records[parent_record] << child_record
end
parent_record_to_child_records.each do |parent, children|
parent.send(details[:prepopulate_method_name], children)
end
end
next_level_records = child_records
when details = cached_belongs_tos[association]
if details[:embed] == true
raise ArgumentError.new("Embedded belongs_to associations do not support prefetching yet.")
else
reflection = details[:association_reflection]
if reflection.polymorphic?
raise ArgumentError.new("Polymorphic belongs_to associations do not support prefetching yet.")
end
cached_iv_name = :"@#{details.fetch(:records_variable_name)}"
ids_to_child_record = records.each_with_object({}) do |child_record, hash|
parent_id = child_record.send(reflection.foreign_key)
if parent_id && !child_record.instance_variable_defined?(cached_iv_name)
hash[parent_id] = child_record
end
end
parent_records = reflection.klass.fetch_multi(ids_to_child_record.keys)
parent_records.each do |parent_record|
child_record = ids_to_child_record[parent_record.id]
child_record.send(details[:prepopulate_method_name], parent_record)
end
end
next_level_records = parent_records
when details = cached_has_ones[association]
if details[:embed] == true
prefetch_embedded_association(records, association, details)
parent_records = records.map(&details[:cached_accessor_name].to_sym).compact
else
raise ArgumentError.new("Non-embedded has_one associations do not support prefetching yet.")
end
next_level_records = parent_records
else
raise ArgumentError.new("Unknown cached association #{association} listed for prefetching")
end
next_level_records
end
end
private
def fetch_recursively_cached_association(ivar_name, association_name) # :nodoc:
ivar_full_name = :"@#{ivar_name}"
assoc = association(association_name)
if assoc.klass.should_use_cache?
if instance_variable_defined?(ivar_full_name)
instance_variable_get(ivar_full_name)
else
cached_assoc = assoc.load_target
if IdentityCache.fetch_read_only_records
cached_assoc = readonly_copy(cached_assoc)
end
instance_variable_set(ivar_full_name, cached_assoc)
end
else
assoc.load_target
end
end
def expire_primary_index # :nodoc:
return unless self.class.primary_cache_index_enabled
IdentityCache.logger.debug do
extra_keys =
if respond_to?(:updated_at)
old_updated_at = old_values_for_fields([:updated_at]).first
"expiring_last_updated_at=#{old_updated_at}"
else
""
end
"[IdentityCache] expiring=#{self.class.name} expiring_id=#{id} #{extra_keys}"
end
IdentityCache.cache.delete(primary_cache_index_key)
end
def expire_attribute_indexes # :nodoc:
cache_indexes.each do |(attribute, fields, unique)|
unless was_new_record?
old_cache_attribute_key = attribute_cache_key_for_attribute_and_previous_values(attribute, fields, unique)
IdentityCache.cache.delete(old_cache_attribute_key)
end
unless destroyed?
new_cache_attribute_key = attribute_cache_key_for_attribute_and_current_values(attribute, fields, unique)
if new_cache_attribute_key != old_cache_attribute_key
IdentityCache.cache.delete(new_cache_attribute_key)
end
end
end
end
def expire_cache # :nodoc:
expire_primary_index
expire_attribute_indexes
true
end
def was_new_record? # :nodoc:
pk = self.class.primary_key
!destroyed? && transaction_changed_attributes.has_key?(pk) && transaction_changed_attributes[pk].nil?
end
def readonly_record_copy(record)
record = record.clone
record.readonly!
record
end
def readonly_copy(record_or_records)
if record_or_records.is_a?(Array)
record_or_records.map { |record| readonly_record_copy(record) }
elsif record_or_records
readonly_record_copy(record_or_records)
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment