Created
April 1, 2015 05:54
-
-
Save jecompton/c90b12cde9e05cca9f49 to your computer and use it in GitHub Desktop.
Basic model for kata.rb
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
#!/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 [email protected] 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 [email protected]? 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