[ANN] KirbyRecord 0.0.0

L

Logan Capaldo

--Apple-Mail-4-341158523
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
charset=US-ASCII;
delsp=yes;
format=flowed

I am proud(?) to announce the first actual release of KirbyRecord.
KirbyRecord is an ORM layer for the very cool pure ruby database,
KirbyBase. KirbyRecord was initialized inspired in design by
ActiveRecord, but as you can see I've stolen ideas from Og as well now.
Here are its features:

1) Like KirbyBase, its written in pure ruby, and fits in a single file.
2) You can let it be run by the database, or you can define the
database with it
3) I wrote lots of comments that look ok after running rdoc

Lets do some examples, I don't think this whole writting a feature
list will work

class Author < KirbyRecord::Base
has_many :books
end

Just like ActiveRecord you say? Well you'd be wrong, for behold!

a = Author.find:)first) { |r| r.name == "Stephen King" }
b = Book.new( :title => "The Shining" ) # pretending I wrote the
boilerplate for Book
a.books << b # Egads! What madness is this?

Incidently as you may have noticed it now does plurals, sort of
(tacks an s on the end) and you don;t have to use those silly get_
and set_ prefixes like in the proof-of-concept


Maybe you don't want to make the database before hand with calls to
KirbyBase#create_table and such. Well have no fear, KirbyRecord now
knows how to make a database from your class definitions. Its a
little more typing than with the KirbyBase interface, but you can
almost pretend that you now have magical objects that persist from
session to session, and all you had to do was write column instead of
attr_accessor

class Author < KirbyRecord::Base
column :name, :String
col_belongs_to :publisher
has_many :books
end

class Publisher < KirbyRecord::Base
column :name, :String
has_many :books
end

class Book < KirbyRecord::Base
column :title, :String
column :author_id, :Integer
belongs_to :author # just to demonstrate the utility of
col_belongs_to
column :isbn, :Integer, 0 # Look ma, defaults! I could have also
done default :isbn => 0 to set
# a default for a
field that wasn't declared with column or didn't have
# a default yet
end

This column stuff is cool but one warning, KirbyRecord always gets
its methods from the db, if theres already a table with the name of
your class it doesn't even look at the columns you declared (except
for the default values). On the other hand if the table doesn't
exist, KirbyRecord will create it for you after you use an object for
the first time.

There are more examples of all this stuff in the comments (which you
can run rdoc on in the file). I hope someone finds a use for this
stuff, and gets back with feedback.

--Apple-Mail-4-341158523
Content-Transfer-Encoding: 7bit
Content-Type: text/x-ruby-script;
x-unix-mode=0644;
name="kirbyrecord.rb"
Content-Disposition: attachment;
filename=kirbyrecord.rb

# This code was written by Logan Capaldo. It is licensed under the same license as ruby

require 'kirbybase'

module KirbyRecord
# KirbyRecord::Base is the most used (perhaps only) class in KirbyRecord
# It acts as an abstraction for the rows in a KirbyBase database table
# eg:
# class Author < KirbyRecord::Base
# end
# KirbyRecord::Base.open_database( )
# authors = Author.find:)all)
class Base
# The following are utility methods for the actual KirbyBase database

# Get a new instance of KirbyBase
# It accepts the same arguments as KirbyBase::new
# KirbyRecord::Base.open_database( )
# or perhaps
# KirbyRecord::Base.open_database:)local, nil, nil, './data')
def self.open_database( *kirby_base_new_args )
@@db = KirbyBase.new( *kirby_base_new_args )
end

# Retrieve the KirbyBase instance being used by KirbyRecord
def self.get_database( )
@@db
end

# The following methods are involved with "reflection" on the database

# Add appropiate accessors for database fields
# You rarely will need to call this directly (I assume), but if for instance, the schema of a table changes
# during the run of a program you can call this method with an argument of true to update your Class
def self.inform_about_table( force = false )


unless force
return self if fields( )
end

db = get_database( )
unless db.table_exists?(table_name.to_sym)
create_table_for_cols
end

@fields = {}
table = db.get_table(table_name.to_sym)

table.field_names.zip( table.field_types ) do |fname, ftype|
@fields[fname] = ftype
end

# The next step is to define the getters and setters
f = @fields.dup
f.delete:)recno)
f.each do |fname, ftype|
getter_name = fname.to_sym
setter_name = "#{fname}=".to_sym

define_method(getter_name) do | |
@state[getter_name]
end

define_method(setter_name) do |value|
cast_value =
case ftype
when :Integer
value.to_i
when :String
value.to_s
when :Float
value.to_f
when :Boolean
if value then true else false end
when :Date
value
when :DateTime
value
end
@state[getter_name] = cast_value

end

end

end

# Returns a hash containing the names and types of columns for the table associated with this child of KirbyRecord::Base
def self.fields( )
@fields
end

# adds methods automatically for a table kind (use the plurarl, which is to say, tack an 's' on the end) when many of them references table self
# example:
# class Book < KirbyRecord::Base
# end
#
# class Author < KirbyRecord::Base
# has_many :books
# end
def self.has_many(kind)
table_name = Util.singularize( kind.to_s ).to_sym
class_name = Util.us_case_to_class_case( table_name.to_s )
col = Util.col_name_for_class(self.to_s)
define_method(kind.to_sym) do
klass = Object.const_get(class_name)
HasManyResultSet.new(self, klass.find:)all) { |r| r.send(col) == self.instance_eval { @state[:recno] } })
end
end

# adds methods automatically for a table kind when one of them references self's table
# if has_many is analogous to Table.find:)all), than has_one is analogous to Table.find:)first)
def self.has_one(kind)
table_name = kind.to_sym
class_name = Util.us_case_to_class_case( table_name.to_s )
col = Util.col_name_for_class(self.to_s)
define_method(kind.to_sym) do
klass = Object.const_get(class_name)
klass.find:)first) { |r| r.send(col) == self.instance_eval { @state[:recno] } }
end
end

# adds methods automatically for a table table_name when self's table references it
# example:
# db.create_table:)bio, :age, :Integer, :home, :String)
# db.create_table:)author, :name, :String, :bio_id, :Integer)
# class Bio < KirbyRecord::Base
# end
# class Author < KirbyRecord::Base
# belongs_to :bio
# end
def self.belongs_to(kind)
class_name = Util.us_case_to_class_case( kind.to_sym )
define_method(kind) do
klass = Object.const_get(class_name.to_s)
klass.find(@state["#{kind}_id".to_sym])
end

define_method("#{kind}=".to_sym) do |other|
@state[("#{kind}_id".to_sym)] = (other.save.instance_eval { @state[:recno] })
save
end

end

# The previous methods decribend relations in the database. The following methods allow one to automatically create database
# with ruby classes and objects. Just think of the class being the table, and the instance being the row

# Creates a column in the table with the associated name, and KirbyBase type :)String, :Boolean, etc.)
# You may optionally provide a default value to be inserted for this column
# example:
# class Student < KirbyRecord::Base
# column :name, :String
# column :age, :Integer
# column :major, :String, "Computer Science"
# column :student_id_num, :Integer
# end
def self.column(name, type, default_value = nil)
@columns ||= []
@columns << [ name, type ]
if default_value
default(name => default_value)
end
self
end

# Set a default value to insert for a column in the absence of a value
# Only works for KirbyBase primitive types, using KirbyRecord::Base children could have confusing semantics
# class Author < KirbyRecord::Base
# default :publisher => 'Pragmatic Press'
# end
def self.default(args)
@defaults ||= {}
@defaults = @defaults.merge( args )
end

# get the default values for columns for this class
# class FirstGrader < KirbyRecord::Base
# default :age => 7
# end
#
# FirstGrader.defaults #=> { :age => 7 }
def self.defaults
@defaults ||= {}
end
# Combine column and belongs_to
#
# col_belongs_to :book
# is equivalent to:
# column :book_id, :Integer
# belongs_to :book
def self.col_belongs_to(name)
column(Util.col_name_for_class(Util.us_case_to_class_case(name)).to_sym, :Integer)
belongs_to(name)
end

# The following methods are meant to be used by classes inheriting from KirbyRecord::Base

# Finds rows in self's table and maps them to instances of self.
# You may pass in one record number, in which case it returns that specific row, multiple record numbers in which case it returns an array of those rows.
# You can also call .find with one of :all or :first and an associated block. The block is used with a KirbyBase select statement so you can search by
# any criteria.
# .find:)all) returns an array of results and .find:)first) returns the first result
# example:
# authors = Author.find:)all) { |r| r.name =~ "Stephen" }
# stephen_king = Author.find:)first) { |r| r.name == "Stephen King" }
# john_dow = Author.find(3)
def self.find(*args) # :yields: record
inform_about_table

if args.length == 1
if (args[0].respond_to?:)to_sym))
case args[0]
when :first
if block_given?
(self.find:)all) { |r| yield(r) }).first
else
self.find:)all).first
end
when :all
if block_given?
get_table.select { |r| yield(r) }
else
get_table.select
end.map do |struct|
obj = self.new
keys = fields.keys
keys.each do |key|
obj.instance_eval { @state[key] = struct.send(key) }
end
obj
end
end
else
self.find:)first) { |r| r.recno == args[0] }
end
else
self.find:)all) { |r| args.include?(r.recno) }
end
end

# Returns the name of the table associated with this class
# Overriding this currently will wreak havoc, so for now please leave it alone
# Eventually it will do the right thing
def self.table_name
Util.class_case_to_us_case(self.to_s.split(/::/).last)
end

# Get a reference to the classes table object
def self.get_table
get_database.get_table(table_name.to_sym)
end

# geta reference to the classes table object
def get_table
self.class.get_table
end

# calls pack on the table
def self.pack
inform_about_table
get_table.pack
end

# Creates a new instance of the object, optionally taking a hash speficing values ie
# mike = Author.new
# stephen = Author.new( :name => "Stephen King" )
def initialize(vals = {})
self.class.inform_about_table
@state = self.class.defaults( ).merge( vals )
self
end

# Inserts or updates to the database
def save
table = get_table
if @state[:recno].nil?
@state[:recno] = table.insert(self.class.defaults.merge(@state))
else
vals = @state.dup
vals.delete:)recno)
table[@state[:recno]] = vals
end
self
end

# returns true if the record has not been inserted into the db
def new_record?
@state[:recno].nil?
end

# removes the record from the database
def delete
table = get_table
table.delete { |r| r.recno == @state[:recno] }
@state[:recno] = nil
end

# protected singleton class methods
class << self
def create_table_for_cols
db = get_database( )
if @columns.length > 0
db.create_table(table_name, *( @columns.flatten ) )
else
raise RuntimeError, "Did you forget to create your database?"
end
self
end
protected :create_table_for_cols
end

end

# This is what you get back if you call a method generated by has_many, it supports all the methods of Enumerable
# It also has methods #<< and #append which are equivalent
# example:
# class Book < KirbyRecord::Base
# belongs_to :author
# end
# class Author < KirbyRecord::Base
# has_many :books
# end
# author = Author.find:)first) { |r| r.name == "Joe" }
# author.books << Book.new( :title => "Happy Days Are Here, Again" )
#
# << and append both perform an implicit save on the argument
class HasManyResultSet
include Enumerable

def initialize(owner = nil, results = [])
@owner = owner
@results = []
end

def each()
results.each { |item| yield(item) }
end

def <<(other)
field_name = (@owner.class.table_name.to_s + "_id").to_sym
other.send("#{field_name}=".to_sym, @owner.instance_eval { @state[:recno] })
other.save
end

def append(other)
self << other
end
end

# Contains some helper functions
module Util
# converts from things like user_name to things like UserName
def self.us_case_to_class_case(name)
name.to_s.split(/_/).map do |word|
word.capitalize
end.join
end

# converts from things like UserName to things like user_name
def self.class_case_to_us_case(name)
decapitalized = name.to_s.sub(/^./) { |match| match.downcase }
decapitalized.gsub(/[A-Z]/) { |match| "_" + match.downcase }
end

# Adds an s
def self.pluralize(name)
name.to_s + 's'
end

# chops an s
def self.singularize(name)
if md = name.to_s.match(/^(.+?)s\z/)
md[1]
else
raise RuntimeError, "#{name} is not plural"
end
end

def self.col_name_for_class(name)
class_case_to_us_case(name) + "_id"
end

def self.class_name_for_col(name)
us_case_to_class_case(name.sub(/_id\z/, ''))
end

end
end

--Apple-Mail-4-341158523--
 
E

Ezra Zygmuntowicz

I am proud(?) to announce the first actual release of KirbyRecord.
KirbyRecord is an ORM layer for the very cool pure ruby database,
KirbyBase. KirbyRecord was initialized inspired in design by
ActiveRecord, but as you can see I've stolen ideas from Og as well
now.
Here are its features:
<snip sweetness>
This column stuff is cool but one warning, KirbyRecord always gets
its methods from the db, if theres already a table with the name of
your class it doesn't even look at the columns you declared (except
for the default values). On the other hand if the table doesn't
exist, KirbyRecord will create it for you after you use an object
for the first time.

There are more examples of all this stuff in the comments (which
you can run rdoc on in the file). I hope someone finds a use for
this stuff, and gets back with feedback.

<kirbyrecord.rb>

Logan-
This looks like a lot of fun! Very nice that its all ruby. Keep
it coming.
Thanks-
-Ezra Zygmuntowicz
Yakima Herald-Republic
WebMaster
http://yakimaherald.com
509-577-7732
(e-mail address removed)
 
J

Jamey Cribbs

Logan said:
I am proud(?) to announce the first actual release of KirbyRecord.
KirbyRecord is an ORM layer for the very cool pure ruby database,
KirbyBase. KirbyRecord was initialized inspired in design by
ActiveRecord, but as you can see I've stolen ideas from Og as well now.

Nice job, Logan!

Jamey
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Similar Threads


Members online

Forum statistics

Threads
473,769
Messages
2,569,577
Members
45,052
Latest member
LucyCarper

Latest Threads

Top