From: Logan Capaldo Date: 2005-09-16T07:52:18+09:00 Subject: [ANN] KirbyRecord 0.0.0 --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--