ribs 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,329 @@
1
+ module Ribs
2
+ # Ribs::ClassMethods contains all the methods that gets mixed in to
3
+ # a model class.
4
+ module ClassMethods
5
+ # Get the metadata for the current model
6
+ attr_reader :ribs_metadata
7
+
8
+ # Create a new instance of this model object, optionally setting
9
+ # properties based on +attrs+.
10
+ def new(attrs = {})
11
+ obj = super()
12
+ attrs.each do |k,v|
13
+ obj.send("#{k}=", v)
14
+ end
15
+ obj
16
+ end
17
+
18
+ # First creates a model object based on the values in +attrs+ and
19
+ # then saves this to the database directly.
20
+ def create(attrs = {})
21
+ val = new(attrs)
22
+ val.save
23
+ val
24
+ end
25
+
26
+ # Depending on the value of +id_or_sym+, tries to find the model
27
+ # with a specified id, or if +id_or_sym+ is <tt>:all</tt> returns
28
+ # all instances of this model.
29
+ def find(id_or_sym)
30
+ Ribs.with_session do |s|
31
+ s.find(self.ribs_metadata.persistent_class.entity_name, id_or_sym)
32
+ end
33
+ end
34
+
35
+ # Destroys the model with the id +id+.
36
+ def destroy(id)
37
+ Ribs.with_session do |s|
38
+ s.delete(find(id))
39
+ end
40
+ end
41
+
42
+
43
+ # TODO: add inspect here
44
+ end
45
+
46
+ # Ribs::InstanceMethods provides the methods that gets mixed in to
47
+ # all instances of a model object.
48
+ module InstanceMethods
49
+ # Returns the Meat instance for this instance.
50
+ def __ribs_meat
51
+ @__ribs_meat ||= Ribs::Meat.new(self)
52
+ end
53
+
54
+ # Returns an inspection based on current values of the data in the
55
+ # database.
56
+ def inspect
57
+ "#<#{self.class.name}: #{self.__ribs_meat.properties.inspect}>"
58
+ end
59
+
60
+ # Saves this instance to the database. If the instance already
61
+ # exists, it will update the database, otherwise it will create
62
+ # it.
63
+ def save
64
+ Ribs.with_session do |s|
65
+ s.save(self)
66
+ end
67
+ end
68
+
69
+ # Removes this instance from the database.
70
+ def destroy!
71
+ __ribs_meat.destroyed = true
72
+ Ribs.with_session do |s|
73
+ s.delete(self)
74
+ end
75
+ end
76
+ end
77
+
78
+ # Collects all the meta data about a specific Ribs model
79
+ class MetaData
80
+ # The table to connect to
81
+ attr_accessor :table
82
+ # The persistent class that Hibernate uses as a definition for
83
+ # this model.
84
+ attr_accessor :persistent_class
85
+ # The Rib that defines all the mapping data
86
+ attr_accessor :rib
87
+
88
+ # Return the property instance for the given +name+.
89
+ def [](name)
90
+ self.persistent_class.get_property(name.to_s) rescue nil
91
+ end
92
+
93
+ # Return all the properties for this model.
94
+ def properties
95
+ self.persistent_class.property_iterator.to_a.inject({}) do |h, value|
96
+ h[value.name] = value
97
+ h
98
+ end
99
+ end
100
+ end
101
+
102
+ # Contains the mapping data that gets created when calling the
103
+ # {Ribs!} method.
104
+ class Rib
105
+ # List of all the columns defined
106
+ attr_reader :columns
107
+ # List of all primary keys defined
108
+ attr_reader :primary_keys
109
+ # List of all columns to avoid
110
+ attr_reader :to_avoid
111
+
112
+ # Initializes object
113
+ def initialize
114
+ @columns = { }
115
+ @primary_keys = { }
116
+ @to_avoid = []
117
+ end
118
+
119
+ # Gets or sets the table name to work with. If +name+ is nil,
120
+ # returns the table name, if not sets the table name to +name+.
121
+ def table(name = nil)
122
+ if name
123
+ @table = name
124
+ else
125
+ @table
126
+ end
127
+ end
128
+
129
+ # Adds a new column mapping for a specific column.
130
+ def col(column, property = column, options = {})
131
+ @columns[column.to_s.downcase] = [property.to_s, options]
132
+ end
133
+
134
+ # Adds a new primary key mapping for a column.
135
+ def primary_key(column, property = column, options = {})
136
+ @primary_keys[column.to_s.downcase] = property.to_s
137
+ @columns[column.to_s.downcase] = [property.to_s, options]
138
+ end
139
+
140
+ # Avoids all the provided columns
141
+ def avoid(*columns)
142
+ @to_avoid += columns.map{|s| s.to_s.downcase}
143
+ end
144
+ end
145
+
146
+ Table = org.hibernate.mapping.Table
147
+ Column = org.hibernate.mapping.Column
148
+ Property = org.hibernate.mapping.Property
149
+ SimpleValue = org.hibernate.mapping.SimpleValue
150
+
151
+ # A simple helper class that allows the Java parts of the system to
152
+ # get the Ruby class from the PersistentClass instance.
153
+ class RubyRootClass < org.hibernate.mapping.RootClass
154
+ include org.jruby.ribs.WithRubyClass
155
+
156
+ # The Ruby class
157
+ attr_accessor :ruby_class
158
+
159
+ def initialize(*args)
160
+ super
161
+ end
162
+
163
+ # Get the Ruby class. Implementation of the WithRubyClass
164
+ # interface.
165
+ def getRubyClass
166
+ @ruby_class
167
+ end
168
+ end
169
+
170
+ class << self
171
+ # Define a rib for the class +on+. If a block is given, will first
172
+ # yield an instance of Rib to it and then base the mapping
173
+ # definition on that.
174
+ #
175
+ # +options+ have several possible values:
176
+ # * <tt>:db</tt> - the database to connect this model to
177
+ #
178
+ def define_ribs(on, options = {})
179
+ rib = Rib.new
180
+ yield rib if block_given?
181
+
182
+ define_metadata_on_class on
183
+ rm = on.ribs_metadata
184
+ rm.rib = rib
185
+
186
+ db = nil
187
+ with_session(options[:db] || :default) do |s|
188
+ db = s.db
189
+ m = s.meta_data
190
+ name = rib.table || table_name_for(on.name, m)
191
+
192
+ tables = m.get_tables nil, nil, name.to_s, %w(TABLE VIEW ALIAS SYNONYM).to_java(:String)
193
+ if tables.next
194
+ table = Table.new(tables.get_string(3))
195
+ rm.table = table
196
+ c = tables.get_string(1)
197
+ table.catalog = c if c && c.length > 0
198
+ c = tables.get_string(2)
199
+ table.schema = c if c && c.length > 0
200
+
201
+ columns_rs = m.get_columns table.catalog, table.schema, table.name, nil
202
+
203
+ while columns_rs.next
204
+ c = Column.new(columns_rs.get_string(4))
205
+ c.default_value = columns_rs.get_string(13)
206
+ c.length = columns_rs.get_int(7)
207
+ c.nullable = columns_rs.get_string(18) == 'YES'
208
+ c.precision = columns_rs.get_int(10)
209
+ c.scale = columns_rs.get_int(9)
210
+ c.sql_type = columns_rs.get_string(6)
211
+ c.sql_type_code = java.lang.Integer.new(columns_rs.get_int(5))
212
+
213
+ table.add_column(c)
214
+ end
215
+ columns_rs.close rescue nil
216
+ tables.close rescue nil
217
+
218
+ pc = RubyRootClass.new
219
+ pc.ruby_class = on
220
+ pc.entity_name = on.name
221
+ pc.table = table
222
+ pc.add_tuplizer(org.hibernate.EntityMode::MAP, "org.jruby.ribs.RubyTuplizer")
223
+
224
+ rm.persistent_class = pc
225
+
226
+ table.column_iterator.each do |c|
227
+ unless rib.to_avoid.include?(c.name.downcase)
228
+ prop = Property.new
229
+ prop.persistent_class = pc
230
+ prop.name = ((v=rib.columns[c.name.downcase]) && v[0]) || c.name
231
+ val = SimpleValue.new(table)
232
+ val.add_column(c)
233
+ val.type_name = get_type_for_sql(c.sql_type, c.sql_type_code)
234
+ prop.value = val
235
+
236
+ if (!rib.primary_keys.empty? && rib.primary_keys[c.name.downcase]) || c.name.downcase == 'id'
237
+ pc.identifier_property = prop
238
+ pc.identifier = val
239
+ else
240
+ pc.add_property(prop)
241
+ end
242
+
243
+ define_meat_accessor(on, prop.name)
244
+ end
245
+ end
246
+ pc.create_primary_key
247
+ db.mappings.add_class(pc)
248
+ else
249
+ tables.close rescue nil
250
+ raise "No table found for: #{name}"
251
+ end
252
+ end
253
+
254
+ db.reset_session_factory!
255
+ end
256
+
257
+ JTypes = java.sql.Types
258
+
259
+ private
260
+ # Defines the actual accessor for a specific property on a
261
+ # class. This will define methods that use the Meat to get data.
262
+ def define_meat_accessor(clazz, name)
263
+ downcased = name.downcase
264
+ clazz.class_eval <<CODE
265
+ def #{downcased}
266
+ self.__ribs_meat[:"#{name}"]
267
+ end
268
+
269
+ def #{downcased}=(value)
270
+ self.__ribs_meat[:"#{name}"] = value
271
+ end
272
+ CODE
273
+ end
274
+
275
+ # Returns the Java type for a specific combination of a type name
276
+ # and an SQL type code
277
+ def get_type_for_sql(name, code)
278
+ case code
279
+ when JTypes::VARCHAR
280
+ "string"
281
+ when JTypes::INTEGER
282
+ "int"
283
+ when JTypes::TIME
284
+ "java.sql.Time"
285
+ when JTypes::DATE
286
+ "java.sql.Date"
287
+ when JTypes::TIMESTAMP
288
+ "java.sql.Timestamp"
289
+ when JTypes::BLOB
290
+ "java.sql.Blob"
291
+ when JTypes::CLOB
292
+ "java.sql.Clob"
293
+ when JTypes::DOUBLE
294
+ "double"
295
+ when JTypes::SMALLINT
296
+ "boolean"
297
+ when JTypes::DECIMAL
298
+ "java.math.BigDecimal"
299
+ else
300
+ $stderr.puts [name, code].inspect
301
+ nil
302
+ end
303
+ end
304
+
305
+ # Tries to figure out if a table name should be in lower case or
306
+ # upper case, and returns the table name based on that.
307
+ def table_name_for(str, metadata)
308
+ if metadata.stores_lower_case_identifiers
309
+ str.downcase
310
+ elsif metadata.stores_upper_case_identifiers
311
+ str.upcase!
312
+ else
313
+ str
314
+ end
315
+ end
316
+
317
+ # Defines the meta data information on a class, and mixes in
318
+ # ClassMethods and InstanceMethods to it.
319
+ def define_metadata_on_class(clazz)
320
+ clazz.instance_variable_set :@ribs_metadata, Ribs::MetaData.new
321
+ class << clazz
322
+ include ClassMethods
323
+ end
324
+ clazz.class_eval do
325
+ include InstanceMethods
326
+ end
327
+ end
328
+ end
329
+ end
data/lib/ribs/meat.rb ADDED
@@ -0,0 +1,33 @@
1
+ module Ribs
2
+ # The Meat is what actually includes data. This is the holder object
3
+ # for all data that comes from a database. This allows Ribs to avoid
4
+ # polluting model objects with loads of instance variables. Instead
5
+ # everything is kept in one instance of Meat. The Meat also keeps
6
+ # track of whether the object is persistent or if it's been
7
+ # destroyed. Note that Meat is very implementation dependent, and
8
+ # should not be relied upon.
9
+ class Meat
10
+ # All the data for this instance
11
+ attr_reader :properties
12
+ # Is this instance persistent yet, or not?
13
+ attr_accessor :persistent
14
+ # Has this instance been destroyed?
15
+ attr_accessor :destroyed
16
+
17
+ # +inside+ is the instance this Meat belongs to.
18
+ def initialize(inside)
19
+ @inside = inside
20
+ @properties = {}
21
+ end
22
+
23
+ # Gets the current value of a property
24
+ def [](ix)
25
+ @properties[ix]
26
+ end
27
+
28
+ # Sets the current value of a property
29
+ def []=(ix, value)
30
+ @properties[ix] = value
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,237 @@
1
+ module Ribs
2
+ # A Ribs session maps many-to-one with a Hibernate session. When
3
+ # there are no more Ribs sessions left, the Hibernate session will
4
+ # be released too.
5
+ #
6
+ # The methods in Session is not to be used outside the framework in
7
+ # most cases, since they are internal, brittle, not based on
8
+ # Hibernate in all cases, and very much subject to change. Currently
9
+ # they provide capabilities that aren't part of the framework yet,
10
+ # such as migrations and setting up test data.
11
+ #
12
+ class Session
13
+ # This error is thrown when an operation on a session is
14
+ # attempted but the internal Hibernate session has already
15
+ # been closed.
16
+ #
17
+ class NotConnectedError < StandardError;end
18
+
19
+ class << self
20
+ # Returns a session object from the database +from+. The
21
+ # available values for from is either one of the existing
22
+ # defined database names, or <tt>:default</tt>, which will give
23
+ # a session to the default database.
24
+ #
25
+ def get(from = :default)
26
+ db = case from
27
+ when :default
28
+ Ribs::DB::get
29
+ when Ribs::DB
30
+ from
31
+ else
32
+ Ribs::DB::get(from)
33
+ end
34
+ db.session
35
+ end
36
+ end
37
+
38
+ # The current database for this session
39
+ attr_reader :db
40
+
41
+ # Creates a new session that points to the provided +db+ and
42
+ # +hibernate_session+
43
+ #
44
+ def initialize(db, hibernate_session)
45
+ @db = db
46
+ @connected = true
47
+ @hibernate_session = hibernate_session
48
+ end
49
+
50
+ # Releases this session from the database. This will possibly
51
+ # result in closing the internal Hibernate session, if this is the
52
+ # last Ribs session pointing to that Hibernate session.
53
+ def release
54
+ chk_conn
55
+ @connected = false
56
+ @db.release(self)
57
+ end
58
+
59
+ # LOW LEVEL - shouldn't be used except by Ribs
60
+ def hibernate_session # :nodoc:
61
+ @hibernate_session
62
+ end
63
+
64
+ # LOW LEVEL - shouldn't be used except by Ribs
65
+ def find(entity_name, id) # :nodoc:
66
+ chk_conn
67
+ if id == :all
68
+ @hibernate_session.create_criteria(entity_name).list.to_a
69
+ else
70
+ @hibernate_session.get(entity_name, java.lang.Integer.new(id))
71
+ end
72
+ end
73
+
74
+ # LOW LEVEL - shouldn't be used except by Ribs
75
+ def save(obj) # :nodoc:
76
+ chk_conn
77
+ tx = @hibernate_session.begin_transaction
78
+ if obj.__ribs_meat.persistent
79
+ @hibernate_session.update(obj)
80
+ else
81
+ @hibernate_session.save(obj)
82
+ obj.__ribs_meat.persistent = true
83
+ end
84
+ tx.commit
85
+ obj
86
+ end
87
+
88
+ # LOW LEVEL - shouldn't be used except by Ribs
89
+ def delete(obj) # :nodoc:
90
+ chk_conn
91
+ tx = @hibernate_session.begin_transaction
92
+ @hibernate_session.delete(obj)
93
+ tx.commit
94
+ obj
95
+ end
96
+
97
+ # LOW LEVEL - shouldn't be used except by Ribs
98
+ def meta_data # :nodoc:
99
+ chk_conn
100
+ @hibernate_session.connection.meta_data
101
+ end
102
+
103
+ # LOW LEVEL - shouldn't be used except by Ribs
104
+ def ddl(string) # :nodoc:
105
+ chk_conn
106
+ execute(string)
107
+ end
108
+
109
+ # LOW LEVEL - shouldn't be used except by Ribs
110
+ def insert(template, *data) # :nodoc:
111
+ chk_conn
112
+ conn = @hibernate_session.connection
113
+ stmt = conn.prepare_statement(template)
114
+ data.each do |d|
115
+ d.each_with_index do |item, index|
116
+ if item.kind_of?(Array)
117
+ set_prepared_statement(stmt, item[0], index+1, item[1])
118
+ else
119
+ set_prepared_statement(stmt, item, index+1, nil)
120
+ end
121
+ end
122
+ stmt.execute_update
123
+ end
124
+ conn.commit
125
+ ensure
126
+ stmt.close rescue nil
127
+ end
128
+
129
+ # LOW LEVEL - shouldn't be used except by Ribs
130
+ def select(string) # :nodoc:
131
+ chk_conn
132
+ conn = @hibernate_session.connection
133
+ stmt = conn.create_statement
134
+ rs = stmt.execute_query(string)
135
+ result = []
136
+ meta = rs.meta_data
137
+ cols = meta.column_count
138
+ while rs.next
139
+ row = []
140
+ (1..cols).each do |n|
141
+ row << from_database_type(rs.get_object(n))
142
+ end
143
+ result << row
144
+ end
145
+ result
146
+ ensure
147
+ rs.close rescue nil
148
+ stmt.close rescue nil
149
+ end
150
+
151
+ private
152
+ # Raise a NotConnectedError if not connected
153
+ def chk_conn
154
+ raise NotConnectedError unless @connected
155
+ end
156
+
157
+ # Tries to turn a Java object from the database into an equivalent
158
+ # Ruby object. Currently supports:
159
+ #
160
+ # * Strings
161
+ # * Floats
162
+ # * Integers
163
+ # * nil
164
+ # * booleans
165
+ # * Dates
166
+ # * Times
167
+ # * Timestamps
168
+ # * Blobs
169
+ # * Clobs
170
+ # * BigDecimals
171
+ def from_database_type(obj)
172
+ case obj
173
+ when String, Float, Integer, NilClass, TrueClass, FalseClass
174
+ obj
175
+ when java.sql.Date, java.sql.Time, java.sql.Timestamp
176
+ Time.at(obj.time/1000)
177
+ when java.sql.Blob
178
+ String.from_java_bytes(obj.get_bytes(1,obj.length))
179
+ when java.sql.Clob
180
+ obj.get_sub_string(1, obj.length)
181
+ when java.math.BigDecimal
182
+ BigDecimal.new(obj.to_s)
183
+ else
184
+ raise "Can't find correct type to convert #{obj.inspect} into"
185
+ end
186
+ end
187
+
188
+ # Tries to set an entry on a prepared statement, based on type
189
+ def set_prepared_statement(stmt, item, index, type)
190
+ case item
191
+ when NilClass
192
+ stmt.set_object index, nil
193
+ when String
194
+ case type
195
+ when :binary
196
+ stmt.set_bytes index, item.to_java_bytes
197
+ when :text
198
+ stmt.set_string index, item
199
+ else
200
+ stmt.set_string index, item
201
+ end
202
+ when Symbol
203
+ stmt.set_string index, item.to_s
204
+ when Integer
205
+ stmt.set_int index, item
206
+ when Float
207
+ stmt.set_float index, item
208
+ when Time
209
+ case type
210
+ when :date
211
+ stmt.set_date index, item.to_java_sql_date
212
+ when :time
213
+ stmt.set_time index, item.to_java_sql_time
214
+ when :timestamp
215
+ stmt.set_timestamp index, item.to_java_sql_time_stamp
216
+ end
217
+ when BigDecimal
218
+ stmt.set_big_decimal index, java.math.BigDecimal.new(item.to_s('F'))
219
+ when TrueClass, FalseClass
220
+ stmt.set_boolean index, item
221
+ else
222
+ raise "Can't find correct type to set prepared statement for #{item.inspect}"
223
+ end
224
+ end
225
+
226
+ # Executes a SQL string against the current hibernate
227
+ # session. This should be an updating SQL string, not a query.
228
+ def execute(string)
229
+ conn = @hibernate_session.connection
230
+ stmt = conn.create_statement
231
+ stmt.execute_update(string)
232
+ conn.commit
233
+ ensure
234
+ stmt.close rescue nil
235
+ end
236
+ end
237
+ end