Last active
June 12, 2019 23:48
-
-
Save airhorns/19e3126d62768217413191f69f746ccf to your computer and use it in GitHub Desktop.
Example Ruby files for RnD Camp Style Exercise
This file contains hidden or 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 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) |
This file contains hidden or 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 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 |
This file contains hidden or 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
# 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