Skip to content

Instantly share code, notes, and snippets.

@jecompton
Created April 1, 2015 05:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jecompton/c90b12cde9e05cca9f49 to your computer and use it in GitHub Desktop.
Save jecompton/c90b12cde9e05cca9f49 to your computer and use it in GitHub Desktop.
Basic model for kata.rb
#!/usr/bin/env ruby
## app for working with katas
##
## Copyright © 2015, Jonathan Compton.
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program. If not, see <http://www.gnu.org/licenses/>.
##
## Contact JECompton@gmail.com for more info.
require 'json'
require 'couchrest'
require 'pstore'
# For internal housekeeping of objects that need to persist. I don't want to use
# the native uids/keys of a specific persistence system since I want the
# flexibility to switch whenever a cooler kid arrives on the block. by having my
# own arbitrary uid/key, I'll be able to get this flexibility.
class DbItem
attr_accessor :uid
def initialize
@uid = (0..16).to_a.map{|a| rand(16).to_s(16)}.join
end
end
class Kata < DbItem
attr_accessor :name, :description, :tags
def initialize(name, description: '', tags: [], uid: nil)
super()
@name = name
# Compton: there must be a better way to guard against these nils.
@description = description || ''
if tags.nil?
@tags = []
else
@tags = tags.uniq
end
end
def add_tag(tag)
if !@tags.include? tag
@tags << tag
end
end
def has_tag?(tag)
@tags.include?(tag)
end
def delete_tag(tag)
@tags.delete(tag)
end
def to_h
kata_hash = {name: @name, description: @description, tags: @tags, uid: @uid}
end
end
# An instance of code attempting to execute a kata. Any lang.
# Probably hold code as a git repo, so edits can be stored, git leveraged.
class Attempt < DbItem
attr_accessor :kata_id, :coder, :notes, :language, :time_taken, :code
def initialize( kata_id, coder,
code: '',
notes: '',
language: :unknown,
status: :unknown,
time_taken: 0,
difficulty: nil,
ratings: [])
super()
@kata_id = kata_id
@coder = coder
@code = code
@notes = notes
# Would be cool to evaluate body with Bayesian and detect language(s)
@language = language
@status = validate_status(status)
# Might be interesting to hook into punchcard system and add vals
# "time_started" and "time_finished" to derive "time_taken" when not
# set manually. Anyway, time_taken is in minutes for now.
@time_taken = time_taken
# Subjective difficulty after coder has judged how hard it was post
# mortem. Maybe collect these values and pass them up to parent kata, or
# probably better, some stats object or just derive it at runtime when
# needed. Whichever way, it would cool if language or other factors were
# taken into account since it's harder to write an RSS feed reader in
# Assembly than in Ruby.
@difficulty = validate_difficulty(difficulty)
# Subjective ratings of how well attempt was executed. Attempt doesn't
# care who rates. It could be the original coder, peers, or even some
# algorithm. This feels like it might need to be elsewhere, but I'll put
# it here until I know better.
@ratings = ratings
end
def title
"Attempt of: #{kata_id} (uid: #{uid})"
end
def validate_status(status)
status_codes = [:successful, :incomplete, :unsuccessful, :unknown]
if status_codes.include? status
status
else
raise(NameError, "invalid status")
end
end
def status
@status
end
def status=(status)
@status = validate_status(status)
end
# subjective difficulty post-mortem as given by attemptor
def difficulty
@difficulty
end
def difficulty=(difficulty)
@difficulty = validate_difficulty(difficulty)
end
def validate_difficulty(difficulty)
if (1..7).include?(difficulty) || difficulty.nil?
difficulty
else
raise(RangeError, "Out of bounds (should be in 1 to 7)")
end
end
# Ratings of how successful the attempt was. Maybe needs to be new class.
def ratings
@ratings
end
def add_rating(rating)
@ratings << validate_rating(rating)
end
def validate_rating(rating)
if (1..7).include?(rating) || rating.nil?
rating
else
raise(RangeError, "Out of bounds (should be in 1 to 7)")
end
end
def mean_rating
@ratings.reduce {|sum, val| sum + val} / Float(@ratings.size)
end
def to_h
attempt_hash = {
kata_id: @kata_id,
coder: @coder,
notes: @notes,
language: @language,
time_taken: @time_taken,
code: @code,
uid: @uid}
end
end
class PStoreStorage
attr_accessor :container
def initialize(location)
@container = PStore.new(location)
end
def save(*items)
items.each do |item|
if(item.class == Kata)
container.transaction do
container.roots.each do |key|
if container[key].name == item.name
raise(ItemExistsError, "Kata already exists.")
end
end
container[item.uid] = item
end
elsif(item.class == Attempt)
container.transaction do
if container[item.uid]
raise(ItemExistsError, "Attempt already exists with this uid.")
elsif !container[item.kata_id]
raise(ItemMissingError, "No kata associated with this attempt.")
else
container[item.uid] = item
end
end
end
end
end
def load(item_id)
container.transaction do
if container.root? item_id
container[item_id]
else
raise(ItemMissingError, "That kata doesn't exist in this database.")
end
end
end
def delete(item_id)
container.transaction do
if container.root? item_id
container.delete(item_id)
else
raise(ItemMissingError, "Item does not exist in the db.")
end
end
end
def count
container.transaction do
container.roots.count
end
end
end
class CouchStorage
attr_accessor :container
def initialize(location)
# Compton: ugly block for auth; need cookie auth or something better
if(location != nil)
@container = CouchRest.database(location)
else
user = "jecompt"
password = "fleagolas"
server = "localhost"
database = "testdb"
port = 5984
@container = CouchRest.database("http://#{user}:#{password}@#{server}:#{port}/#{database}")
end
end
# Helper function for the main/only view I use for kata couch
def katas_by_name
container.view("views/name_doc")["rows"]
end
def objects_by_uid
container.view("views/uid_doc")["rows"]
end
# Compton: These mappings are NOT the responsibility of CouchStorage. Need
# to refactor.
def couch_to_kata(couch_record)
Kata.new(couch_record["name"],
description: couch_record["description"],
tags: couch_record["tags"],
uid: couch_record["uid"])
end
def couch_to_attempt(couch_record)
Attempt.new(couch_record["kata_id"],
couch_record["coder"],
code: couch_record["code"],
notes: couch_record["notes"],
language: couch_record["language"],
status: couch_record["status"] ||= :unknown,
time_taken: couch_record["time_taken"],
difficulty: couch_record["difficulty"],
ratings: couch_record["ratings"])
end
def save(*items)
items.each do |item|
# If Kata, make sure the kata name is unique
if(item.class == Kata)
katas_by_name.each do |doc|
if (doc["key"] == item.name)
raise(ItemExistsError, "Kata already exists.")
end
end
elsif(item.class == Attempt)
# First check to see that the target Kata exists.
kata_exists = false
objects_by_uid.each do |doc|
if(doc["key"] == item.kata_id)
kata_exists = true
end
end
if !kata_exists
raise(ItemMissingError, "Kata does not exist.")
end
end
# Now make sure object is unique.
objects_by_uid.each do |doc|
if(doc["key"] == item.uid)
raise(ItemExistsError, "Item exists with this id.")
end
end
# Ok, now it should be safe to save the item.
# This does feel unparallel, though. need to decouple timing
# somehow.
container.save_doc(item.to_h)
end
end
def load(item_id)
objects_by_uid.each do |doc|
# This conditional is very hackish and bad, but probably an
# improvement over the meta-crap I was doing.
if(doc["value"].has_key?("name"))
if(doc["key"] == item_id)
return couch_to_kata(doc["value"])
end
elsif(doc["value"].has_key?("kata_id"))
if(doc["key"] == item_id)
return couch_to_attempt(doc["value"])
end
end
end
# else it doesn't exist.
raise(ItemMissingError, "Couldn't load: Item doesn't exist in this database.")
end
def delete(item_id)
objects_by_uid.each do |doc|
if(doc["key"] == item_id)
return if container.delete_doc(doc["value"])
end
end
raise(ItemMissingError, "Couldn't delete: Item does not exist in the db.")
end
def count
katas_by_name.count
end
end
# Custom exceptions
class ItemMissingError < StandardError; end
class ItemExistsError < StandardError; end
class UnknownStorageError < StandardError; end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment