christiank-turntable 0.7.1.1 → 0.999.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -27,6 +27,9 @@ p sqlite_db_columns
27
27
  # Turntable ids and SQL ids are different...
28
28
  if sqlite_db_columns.index 'id'
29
29
  sqlite_db_columns[sqlite_db_columns.index('id')] = 'old_id'
30
+ else
31
+ raise "#{table_name} needs an INTEGER PRIMARY KEY column called 'id'"
32
+ exit 1
30
33
  end
31
34
 
32
35
  # Convert the remaining column names to symbols
@@ -34,6 +37,6 @@ sqlite_db_columns.collect! { |column| column.to_sym }
34
37
 
35
38
  # Create the new Turntable and fill it up
36
39
  turntable_db = Turntable.new "#{table_name}.turn", sqlite_db_columns
37
- sqlite_db_rows.each { |row| turntable_db.insert_array row }
40
+ sqlite_db_rows.each { |row| turntable_db.push row }
38
41
 
39
- puts "Created #{turntable_db.filename}"
42
+ puts "Created #{turntable_db[:filename]}"
File without changes
data/turntable.rb CHANGED
@@ -1,157 +1,131 @@
1
1
  #
2
- # turntable v0.7.1.1
2
+ # Turntable v0.999.1
3
3
  # Christian Koch <ckoch002@student.ucr.edu>
4
4
  #
5
+ # Turntable is an alternative relational database for Ruby. A Turntable object
6
+ # is actually a dictionary of row objects (which in turn are dictionaries)
7
+ # which adhere to the row/column model and are marshalled to an external file.
8
+ #
5
9
 
10
+ $:.unshift File.join(File.dirname(__FILE__), 'lib')
6
11
  require 'dictionary'
7
12
  require 'ostruct'
8
13
 
9
- class Turntable < Array
14
+ class Turntable
15
+
16
+ VERSION = 'v0.999.1'
10
17
 
11
- @@version = 'v0.7.1.1'
18
+ include Enumerable
19
+ def each
20
+ all_numbered_rows.each { |row| yield row }
21
+ end
12
22
 
13
- # Creates a new Turntable database. Accepts either a list of column names, or
14
- # an array of symbols which contain the same thing. Requires at least 2
15
- # column names.
23
+ # Creates a new Turntable object and corresponding file. Requires a filename
24
+ # and a list of column names, or one array which represents the column names.
16
25
  def initialize filename, *columns
26
+ raise ArgumentError, 'expecting column names' if columns.empty?
27
+
28
+ @table = Dictionary.new
17
29
  if columns.length == 1
18
- if columns[0].is_a?(Array) and (columns.all? { |c| c.is_a?(Symbol) })
19
- @columns = columns[0]
20
- else
21
- raise ArgumentError, 'Turntable.new() requires a filename and at least two column names'
22
- end
23
- elsif columns.empty?
24
- raise ArgumentError, 'Turntable.new() requires a filename and at least two column names'
30
+ columns[0].unshift :id
31
+ @table[:columns] = columns[0]
25
32
  else
26
- @columns = columns
33
+ columns.unshift :id
34
+ @table[:columns] = columns
27
35
  end
28
36
 
29
- @filename = File.expand_path(filename)
30
-
31
- self.save_to @filename
37
+ @table[:filename] = File.expand_path(filename)
32
38
  end
33
39
 
34
- # Loads an existing Turntable. You can't just Marshal.load() the database
35
- # because that also marshals back the old @filename, in case the database is
36
- # moved or renamed. Instead, we load the old database and then reassign
37
- # its @filename. However, I don't want @filename to be writeable by the user.
38
- # So, I'm exploiting the Object#send hack, which may or may not be a good
39
- # idea. There's got to be a non-controversial way of doing this.
40
+ # Loads an existing Turntable database file.
40
41
  def Turntable.load filename
41
- @filename = File.expand_path(filename)
42
+ filename = File.expand_path(filename)
43
+ raise ArgumentError, "#{filename}: file not found" unless File.file?(filename)
42
44
 
43
- f = File.open @filename
45
+ f = File.open filename
46
+ f.rewind
44
47
  turn = Marshal.load f
45
48
  f.close
46
49
 
47
- turn.send :filename=, @filename
50
+ turn.update { turn[:filename] = filename } unless turn[:filename] == filename
48
51
  turn
49
52
  end
50
53
 
51
- # Reader methods for this Turntable's metadata.
52
- def Turntable.version; @@version; end
53
- attr_reader :filename, :columns
54
-
55
- # Inserts a new column to the database. All preexisting rows are filled with
56
- # nil. Returns the new columns list.
57
- def add_column column_name
58
- @columns.push column_name
59
- self.each { |row| eval "row.#{column_name} = nil" }
54
+ # Allows you to access Turntable rows and/or metadata like an array or hash.
55
+ def [](index)
56
+ @table[index]
57
+ end
60
58
 
61
- self.save_to @filename
62
- self.columns
59
+ # Only metadata can be written with []= notation.
60
+ def []=(index, value)
61
+ @table[index] = value unless index.is_a?(Fixnum)
63
62
  end
64
63
 
65
- # Removes a specific row (actually, replaces the specified row with nil).
66
- # Returns the deleted row.
67
- def delete id
68
- before_table = self.dup
69
- self.update { self[id] = nil }
70
- self.save_to @filename
71
- before_table[id]
64
+ # Inserts a new column. Fills all of the pre-existing rows with nil. Returns
65
+ # the new databse.
66
+ def add_column column_name
67
+ raise ArgumentError, "column #{column_name} already exists" if @table[:columns].include?(column_name)
68
+
69
+ @table[:columns].push column_name
70
+ @table.each { |key, value| eval("@table[key].#{column_name} = nil") if key.is_a?(Fixnum) }
71
+ self.update
72
+ @table
72
73
  end
73
74
 
74
- # Returns a boolean, indicating whether the select statement would return
75
- # any data.
76
- def include?
75
+ # Returns an array of all of the non-metadata rows.
76
+ def all_numbered_rows
77
77
  result = []
78
- self.each { |row| result.push(row) if yield(row) }
79
- result.any?
78
+ @table.each_key { |key| result.push(@table[key]) if key.is_a?(Fixnum) }
79
+ result
80
80
  end
81
81
 
82
- # Pushes self with a new row. It accepts a list of values to be inserted, or
83
- # one array which contains the same thing. Returns the newly created row.
84
- def insert *args
85
- args = one_array_or_list(args, @columns)
86
-
87
- row = Row.new
88
- self.empty? ? (row.id = 0) : (row.id = self.last.id + 1)
89
- @columns.each { |column| eval "row.#{column} = args.shift" }
90
- self.push row
91
-
92
- self.save_to @filename
93
- row
82
+ # Removes a given row. Returns the new database.
83
+ def delete id
84
+ @table.delete id
85
+ self.update
86
+ @table
94
87
  end
95
88
 
96
- # Custom inspect() makes working in IRB much cleaner.
97
- def inspect
98
- "#<#{self.class}:#{self.object_id} @filename=#{self.filename.inspect}, @columns=#{self.columns.inspect}>"
99
- end
100
-
101
- # Prints the entire database to STDOUT. It's just a wrapper for
102
- # Array#to_stdout.
103
- def to_stdout
104
- self.to_a.to_stdout
89
+ # A much healthier inspect() makes dealing with Turntable in IRB easier.
90
+ def inspect; self.to_s; end
91
+
92
+ # Returns a dictionary of all this Turntable's metadata.
93
+ def metadata
94
+ result = Dictionary.new
95
+ @table.each { |key, value| result[key] = value unless key.is_a?(Fixnum) }
96
+ result
105
97
  end
98
+
99
+ # Pushes self with a new row. Accepts either a list of arguments, or one
100
+ # array which represents the same thing.
101
+ def push *args
102
+ args = args[0] if args.length == 1 and args[0].is_a?(Array)
106
103
 
107
- # Changes to preexisting rows must occur inside an update() block. It's sort
108
- # of like PStore#transaction.
109
- def update
110
- what_happened = yield
111
- self.save_to @filename
112
- what_happened
113
- end
114
-
115
- protected
116
- # Users shouldn't reassign @filename themselves.
117
- attr_writer :filename
118
-
119
- # Checks whether the user provided one array of values, or a list which
120
- # contains the same thing.
121
- def one_array_or_list args, columns
122
- if args.length == 1
123
- unless args[0].is_a?(Array) and (args[0].length == columns.length)
124
- raise ArgumentError, "wrong number of elements (#{args.length} for #{columns.length})"
125
- end
126
- return args[0]
127
- else
128
- if args.length != @columns.length
129
- raise ArgumentError, "wrong number of arguments (#{args.length} for #{columns.length})"
130
- end
131
- return args
132
- end
133
- end
134
-
135
- # Saves self to an external file.
136
- def save_to filename
137
- f = File.new filename, 'w'
138
- Marshal.dump self, f
139
- f.close
104
+ if args.length != (@table[:columns].length - 1)
105
+ raise ArgumentError, "wrong number of arguments (#{args.length} for #{@table[:columns].length - 1})"
140
106
  end
107
+
108
+ @before_table = @table.dup
141
109
 
142
- end
110
+ row = Row.new
111
+ row.id = all_numbered_rows.empty? ? (0) : (all_numbered_rows.last.id + 1)
112
+ @table[:columns].dup.delete_if { |column| column == :id }.each { |column| eval "row.#{column} = args.shift" }
113
+ @table[row.id] = row
143
114
 
144
- class Array
115
+ @pushing = true
116
+ self.update
117
+ row
118
+ end
145
119
 
146
- # Returns an array of rows in a HTML-formatted table.
120
+ # Returns a string ready for HTML.
147
121
  def to_html
148
122
  string = "<table>\n"
149
123
 
150
124
  string += "\t<tr>\n"
151
- self.first.table.each_key { |column| string += "\t\t<th>#{column}</th>\n" }
125
+ @table[:columns].each { |column| string += "\t\t<th>#{column}</th>\n" }
152
126
  string += "\t</tr>\n"
153
127
 
154
- self.compact.each do |row|
128
+ self.each do |row|
155
129
  string += "\t<tr>\n"
156
130
  row.table.each_value { |value| string += "\t\t<td>#{value}</td>\n" }
157
131
  string += "\t</tr>\n"
@@ -159,18 +133,13 @@ class Array
159
133
  string += '</table>'
160
134
  end
161
135
 
162
- # Prints an array of rows to STDOUT.
163
- def to_stdout
164
- puts self.to_textile
165
- end
166
-
167
- # Returns an array of rows in a Textile-formatted table.
136
+ # Returns a string ready for Textile.
168
137
  def to_textile
169
138
  string = ''
170
139
 
171
- self.first.table.each_key { |column| string += "|_.#{column}" }
140
+ @table[:columns].each { |column| string += "|_.#{column}" }
172
141
  string += "|\n"
173
- self.compact.each do |row|
142
+ self.each do |row|
174
143
  row.table.each_value { |value| string += "|#{value}" }
175
144
  string += "|\n"
176
145
  end
@@ -178,13 +147,46 @@ class Array
178
147
  string
179
148
  end
180
149
 
150
+ # Any changes to the external file must occur within this block. If something
151
+ # goes wrong (like trying to push a Proc object) then no changes are made.
152
+ def update
153
+ @pushing ? (@before_table) : (@before_table = @table.dup)
154
+ what_happened = yield if block_given?
155
+
156
+ begin
157
+ f = File.new @table[:filename], 'w'
158
+ Marshal.dump self, f
159
+ rescue
160
+ warn 'warning: invalid update parameters, changes were not commited'
161
+ @table = @before_table
162
+ f.rewind
163
+ Marshal.dump self, f
164
+ ensure
165
+ f.close
166
+ @pushing = false
167
+ end
168
+
169
+ what_happened if block_given?
170
+ end
171
+
181
172
  end
182
173
 
183
- # Rows are custom OpenStructs, based on dictionaries and not hashes.
174
+ # Rows are custom OpenStructs, made with dictionaries and not hashes.
184
175
  class Row < OpenStruct
185
176
  def initialize; @table = Dictionary.new; end
186
177
  public :table
187
178
  end
188
179
 
189
- # Turn off Object#id warnings.
190
- class Object; undef id; end
180
+ class Object
181
+
182
+ # Turn off Object#id warnings.
183
+ undef id
184
+
185
+ # Just like regular puts(), except putting a Turntable calls
186
+ # Turntable#to_textile.
187
+ def puts obj
188
+ super obj
189
+ puts obj.to_textile if obj.is_a?(Turntable)
190
+ end
191
+
192
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: christiank-turntable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1.1
4
+ version: 0.999.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christian Koch
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-07-10 00:00:00 -07:00
12
+ date: 2009-07-13 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -23,14 +23,14 @@ extra_rdoc_files: []
23
23
 
24
24
  files:
25
25
  - turntable.rb
26
- - dictionary.rb
26
+ - lib/dictionary.rb
27
27
  has_rdoc: false
28
28
  homepage: http://github.com/christiank/Turntable
29
29
  post_install_message:
30
30
  rdoc_options: []
31
31
 
32
32
  require_paths:
33
- - .
33
+ - lib
34
34
  required_ruby_version: !ruby/object:Gem::Requirement
35
35
  requirements:
36
36
  - - ">="