kabuki-heresy 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +50 -0
- data/VERSION.yml +4 -0
- data/lib/heresy-with-gems.rb +5 -0
- data/lib/heresy.rb +18 -0
- data/lib/heresy/formatting/json.rb +28 -0
- data/lib/heresy/index.rb +135 -0
- data/lib/heresy/model.rb +188 -0
- data/lib/heresy/schema.rb +48 -0
- data/lib/heresy/schema/column.rb +34 -0
- data/lib/heresy/schema/fields.rb +66 -0
- data/test/fields_test.rb +88 -0
- data/test/formatting/json_test.rb +15 -0
- data/test/index_test.rb +74 -0
- data/test/model_test.rb +78 -0
- data/test/schema_test.rb +43 -0
- data/test/test_helper.rb +51 -0
- metadata +114 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 kabuki
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
= heresy
|
2
|
+
|
3
|
+
Heresy is a schema free wrapper around your database, heavily inspired by both CouchDB and FriendFeed[1]. You create Heresy models that work with generic schema-less tables.
|
4
|
+
|
5
|
+
class Entry < Heresy::Model
|
6
|
+
fields do |f|
|
7
|
+
f.string :title
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
Entry.schema.create # create the table in the database
|
12
|
+
|
13
|
+
This class will use a table that looks like this:
|
14
|
+
|
15
|
+
create_table :entries do |t|
|
16
|
+
t.primary_key :id, :int
|
17
|
+
t.column :uuid, :binary
|
18
|
+
t.column :updated_at, :timestamp
|
19
|
+
t.column :body, :blob
|
20
|
+
end
|
21
|
+
|
22
|
+
Now you can create and retrieve models:
|
23
|
+
|
24
|
+
@entry = Entry.new :title => 'testing'
|
25
|
+
@entry.save
|
26
|
+
|
27
|
+
@entry = Entry.find('some_entry_uuid')
|
28
|
+
@entry.body = "updated"
|
29
|
+
@entry.save
|
30
|
+
|
31
|
+
UUIDs are automatically generated and used as the main ID for each record.
|
32
|
+
|
33
|
+
1: http://bret.appspot.com/entry/how-friendfeed-uses-mysql
|
34
|
+
|
35
|
+
== TODO
|
36
|
+
|
37
|
+
- Indexes
|
38
|
+
- Associations
|
39
|
+
- Timezone conversions
|
40
|
+
|
41
|
+
== NOT TODO
|
42
|
+
|
43
|
+
- Validations
|
44
|
+
- Callbacks
|
45
|
+
|
46
|
+
Use Validateable, ActiveSupport::Callbacks, ActiveModel, etc
|
47
|
+
|
48
|
+
== Copyright
|
49
|
+
|
50
|
+
Copyright (c) 2009 kabuki. See LICENSE for details.
|
data/VERSION.yml
ADDED
data/lib/heresy.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__))
|
2
|
+
|
3
|
+
%w(array/extract_options).each { |lib| require "active_support/core_ext/#{lib}" }
|
4
|
+
class Array #:nodoc:
|
5
|
+
include ActiveSupport::CoreExtensions::Array::ExtractOptions
|
6
|
+
end
|
7
|
+
|
8
|
+
%w(basic_object time_with_zone values/time_zone inflector core_ext/object core_ext/duplicable core_ext/blank core_ext/class core_ext/module core_ext/date core_ext/numeric core_ext/time duration).each { |lib| require "active_support/#{lib}" }
|
9
|
+
%w(uuid sequel_core tzinfo heresy/schema heresy/schema/column heresy/schema/fields heresy/index heresy/model).each { |lib| require lib }
|
10
|
+
|
11
|
+
module Heresy
|
12
|
+
class << self
|
13
|
+
attr_accessor :time_zone
|
14
|
+
attr_accessor :db
|
15
|
+
end
|
16
|
+
|
17
|
+
Time.zone = self.time_zone = "UTC"
|
18
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'zlib'
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
module Heresy
|
6
|
+
module Formatting
|
7
|
+
module Json
|
8
|
+
DATE_REGEX = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/
|
9
|
+
|
10
|
+
extend self
|
11
|
+
def encode(body)
|
12
|
+
s = StringIO.new
|
13
|
+
z = Zlib::GzipWriter.new(s)
|
14
|
+
z.write body.to_json
|
15
|
+
z.close
|
16
|
+
s.string
|
17
|
+
end
|
18
|
+
|
19
|
+
def decode(body)
|
20
|
+
s = StringIO.new(body)
|
21
|
+
z = Zlib::GzipReader.new(s)
|
22
|
+
hash = JSON.parse(z.read)
|
23
|
+
z.close
|
24
|
+
hash
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/heresy/index.rb
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
module Heresy
|
2
|
+
# This class is responsible for defining the index tables that models use.
|
3
|
+
# Since model data is stored as an encoded string, it is impossible to perform
|
4
|
+
# any searches or queries on it. Therefore, separate tables with these data
|
5
|
+
# relationships must be created.
|
6
|
+
#
|
7
|
+
# @index = Index.new(Heresy::Model.db, :entry_titles, :entry_id)
|
8
|
+
# @index.field :title, :varchar, :size => 255
|
9
|
+
# @index.create
|
10
|
+
#
|
11
|
+
# This will create a simple table that looks something like this:
|
12
|
+
#
|
13
|
+
# create_table :entry_titles do |t|
|
14
|
+
# t.column :entry_id, :binary
|
15
|
+
# t.column :title, :varchar
|
16
|
+
# t.primary_key [:title, :entry_id]
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# You can then fill the index as models are saved:
|
20
|
+
#
|
21
|
+
# @entry.save
|
22
|
+
# @index << {:entry_id => @entry.uuid, :title => @entry.title}
|
23
|
+
#
|
24
|
+
# Once the index is filled, you can perform searches:
|
25
|
+
#
|
26
|
+
# uuids = @index.find(:title => "Test")
|
27
|
+
# entries = Entry.find(uuids)
|
28
|
+
#
|
29
|
+
class Index
|
30
|
+
attr_accessor :name, :key
|
31
|
+
attr_reader :fields, :field_names, :uuid_fields
|
32
|
+
|
33
|
+
def initialize(db, name, key)
|
34
|
+
@db = db
|
35
|
+
@name = name
|
36
|
+
@key = Heresy::Schema::Column.new(key, :uuid, :unique => true)
|
37
|
+
@fields = []
|
38
|
+
@field_names = {}
|
39
|
+
@uuid_fields = [@key.name]
|
40
|
+
yield self if block_given?
|
41
|
+
end
|
42
|
+
|
43
|
+
# Inserts the given data into the index. The columns should probably
|
44
|
+
# match the Index's schema, or you'll probably see an error.
|
45
|
+
#
|
46
|
+
# @index << {:entry_id => @entry.uuid, :title => @entry.title}
|
47
|
+
#
|
48
|
+
def insert(data)
|
49
|
+
@uuid_fields.each do |name|
|
50
|
+
if data.key?(name)
|
51
|
+
data[name] = data[name].to_a.pack("H*")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
dataset << data
|
55
|
+
end
|
56
|
+
|
57
|
+
alias << insert
|
58
|
+
|
59
|
+
# Clears all data from the index.
|
60
|
+
def clear
|
61
|
+
dataset.delete
|
62
|
+
end
|
63
|
+
|
64
|
+
# Counts the number of indexed entries.
|
65
|
+
def count
|
66
|
+
dataset.count
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns all the UUIDs from a given query. The query is first passed to #filter.
|
70
|
+
def find(params)
|
71
|
+
filter(params).to_a.map! { |row| row[@key.name].unpack("H*").first }
|
72
|
+
end
|
73
|
+
|
74
|
+
# Filters the Sequel dataset object with the given query. The resulting dataset
|
75
|
+
# can be modified further to introduce ordering or more advanced filtering if desired.
|
76
|
+
def filter(options)
|
77
|
+
dataset.filter(options).select(@key.name)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Index schema methods
|
81
|
+
|
82
|
+
# Adds a field to the index. By default, a :uuid type is used. All
|
83
|
+
# model-to-model associations are done via UUID.
|
84
|
+
def field(name, type = :uuid, options = {})
|
85
|
+
col = Heresy::Schema::Column.new(name, type, options)
|
86
|
+
@fields << col
|
87
|
+
@uuid_fields << col.name if col.type == :uuid
|
88
|
+
@field_names[name] = col
|
89
|
+
end
|
90
|
+
|
91
|
+
# Index database creation/deletion
|
92
|
+
|
93
|
+
# Checks if the Index table exists in the database.
|
94
|
+
def exists?
|
95
|
+
@db.table_exists?(@name)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Creates the Index table
|
99
|
+
def create
|
100
|
+
to_sql.each { |sql| @db.execute_ddl(sql) }
|
101
|
+
end
|
102
|
+
|
103
|
+
# Drops the index table
|
104
|
+
def drop
|
105
|
+
@db.drop_table @name
|
106
|
+
end
|
107
|
+
|
108
|
+
# Returns the SQL that will be used to create the Index table.
|
109
|
+
def to_sql
|
110
|
+
@db.create_table_sql_list @name, *generate.create_info
|
111
|
+
end
|
112
|
+
|
113
|
+
def inspect
|
114
|
+
"<Heresy::Index:#{@name} #{@fields.map { |f| f.name } * ", "}>"
|
115
|
+
end
|
116
|
+
|
117
|
+
protected
|
118
|
+
def dataset
|
119
|
+
@dataset ||= @db[@name].select(@key.name)
|
120
|
+
end
|
121
|
+
|
122
|
+
def generate
|
123
|
+
gen = Sequel::Schema::Generator.new(@db)
|
124
|
+
@key.create(gen)
|
125
|
+
keys = []
|
126
|
+
@fields.each do |field|
|
127
|
+
keys << field.name
|
128
|
+
field.create(gen)
|
129
|
+
end
|
130
|
+
keys << @key.name
|
131
|
+
gen.primary_key keys
|
132
|
+
gen
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
data/lib/heresy/model.rb
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
module Heresy
|
2
|
+
# This class is responsible for storing and retrieving the model data in a schema-free manner.
|
3
|
+
class Model
|
4
|
+
class << self
|
5
|
+
attr_writer :db, :schema, :formatter
|
6
|
+
|
7
|
+
# Accesses the database schema for this model. The basic schema will look something like this:
|
8
|
+
#
|
9
|
+
# create_table :entries do |t|
|
10
|
+
# t.primary_key :id, :int
|
11
|
+
# t.column :uuid, :binary
|
12
|
+
# t.column :updated_at, :timestamp
|
13
|
+
# t.column :body, :blob
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# You can modify any of those from the schema directly.
|
17
|
+
#
|
18
|
+
# class Entry < Heresy::Model
|
19
|
+
# schema :custom_table_name do |s|
|
20
|
+
# s.id.type = :medium_int
|
21
|
+
# s.id.options[:size] = 11
|
22
|
+
# s.body.type = :medium_blob
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# The types are any valid type from the Sequel ruby library. There are a few convenience types, however.
|
27
|
+
# (See Heresy::Schema::Column)
|
28
|
+
#
|
29
|
+
# All models have the same 4 database fields:
|
30
|
+
# - an auto-incremented ID and updated_at timestamp
|
31
|
+
# - a 32 character hex UUID that gets stored as a 16 character binary string. This is how items are retrieved.
|
32
|
+
# - a BLOB body character for storing the model's data.
|
33
|
+
#
|
34
|
+
def schema(table_name = name.demodulize.underscore.pluralize)
|
35
|
+
@schema ||= Heresy::Schema.new(db, table_name.to_sym)
|
36
|
+
yield @schema if block_given?
|
37
|
+
@schema
|
38
|
+
end
|
39
|
+
|
40
|
+
# Yields a Schema::Fields object for specifying fields for this model. These fields get assembled into
|
41
|
+
# a hash and stored in the BODY attribute of the schema.
|
42
|
+
#
|
43
|
+
# By default, fields are all strings. Other types such as integers and dates can be specified for automatic
|
44
|
+
# conversions.
|
45
|
+
#
|
46
|
+
# class Entry < Heresy::Model
|
47
|
+
# fields do |f|
|
48
|
+
# f.string :title
|
49
|
+
# f.integer :comments_count
|
50
|
+
# f.datetime :published_at
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
def fields
|
55
|
+
yield Schema::Fields.new(self)
|
56
|
+
end
|
57
|
+
|
58
|
+
# This is a hash of all specified fields and their type converters. If a type converter is set (see #fields),
|
59
|
+
# values are parsed in the field attribute writers, and encoded when saving to the database.
|
60
|
+
def body_fields
|
61
|
+
@body_fields ||= {}
|
62
|
+
end
|
63
|
+
|
64
|
+
# Finds a single or an array of entries by UUID.
|
65
|
+
def find(uuid)
|
66
|
+
if uuid.is_a?(Array)
|
67
|
+
rows = dataset.filter(:uuid => uuid.map { |u| u.to_a.pack("H*") }).to_a
|
68
|
+
rows.map! { |row| retrieve(row) }
|
69
|
+
else
|
70
|
+
new(:uuid => uuid).reload
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Reference to the Sequel DB object
|
75
|
+
def db
|
76
|
+
@db = Heresy.db
|
77
|
+
end
|
78
|
+
|
79
|
+
# Reference to the Sequel dataset for this model's table.
|
80
|
+
def dataset
|
81
|
+
@dataset ||= db[schema.name]
|
82
|
+
end
|
83
|
+
|
84
|
+
# The formatter is what converts the assembled model hash data into the string
|
85
|
+
# that gets saved in the database. The default formatter stores zlib compressed
|
86
|
+
# JSON hashes.
|
87
|
+
def formatter
|
88
|
+
@formatter ||= \
|
89
|
+
if superclass.respond_to?(:formatter)
|
90
|
+
superclass.formatter
|
91
|
+
else
|
92
|
+
:json
|
93
|
+
end
|
94
|
+
|
95
|
+
if @formatter.is_a?(Symbol)
|
96
|
+
require "heresy/formatting/#{@formatter}"
|
97
|
+
@formatter = Heresy::Formatting.const_get(@formatter.to_s.capitalize)
|
98
|
+
end
|
99
|
+
|
100
|
+
@formatter
|
101
|
+
end
|
102
|
+
|
103
|
+
protected
|
104
|
+
# Used by #find to fill up an empty model record with data using a row from the db.
|
105
|
+
def retrieve(row)
|
106
|
+
record = new(:uuid => row[:uuid].unpack("H*").first)
|
107
|
+
record.retrieve(row)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
attr_reader :id
|
112
|
+
attr_reader :updated_at
|
113
|
+
|
114
|
+
def initialize(attributes = {})
|
115
|
+
set_attributes attributes
|
116
|
+
end
|
117
|
+
|
118
|
+
def uuid
|
119
|
+
@uuid ||= UUID.generate(:compact)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Saves the record in the database.
|
123
|
+
def save
|
124
|
+
if new?
|
125
|
+
self.class.dataset << {:uuid => uuid.to_a.pack("H*"), :body => self.class.formatter.encode(assemble_body)}
|
126
|
+
reload
|
127
|
+
else
|
128
|
+
rowset.update({:updated_at => nil})
|
129
|
+
reload
|
130
|
+
end
|
131
|
+
true
|
132
|
+
end
|
133
|
+
|
134
|
+
def new?
|
135
|
+
@id.nil?
|
136
|
+
end
|
137
|
+
|
138
|
+
# Reloads a model's data from the database using the existing UUID.
|
139
|
+
def reload
|
140
|
+
retrieve(rowset.select(:id, :body, :updated_at).first)
|
141
|
+
end
|
142
|
+
|
143
|
+
def ==(other)
|
144
|
+
other.class == self.class && other.uuid == uuid
|
145
|
+
end
|
146
|
+
|
147
|
+
def inspect
|
148
|
+
"<#{self.class.name} (#{uuid}) #{self.class.body_fields.keys.map { |k| "@#{k}=#{send(k).inspect}" } * ', '}>"
|
149
|
+
end
|
150
|
+
|
151
|
+
def retrieve(row)
|
152
|
+
@id = row[:id]
|
153
|
+
@updated_at = row[:updated_at]
|
154
|
+
set_attributes self.class.formatter.decode(row[:body])
|
155
|
+
self
|
156
|
+
end
|
157
|
+
|
158
|
+
protected
|
159
|
+
# Assembles the model data into an encoded hash, ready to be formatted and stored.
|
160
|
+
def assemble_body
|
161
|
+
hash = {}
|
162
|
+
self.class.body_fields.each do |key, type|
|
163
|
+
if value = instance_variable_get("@#{key}")
|
164
|
+
hash[key] = type ? type.encode(value) : value
|
165
|
+
end
|
166
|
+
end
|
167
|
+
hash
|
168
|
+
end
|
169
|
+
|
170
|
+
# Performs a mass assignment of the model's data.
|
171
|
+
def set_attributes(attributes)
|
172
|
+
attributes.each do |key, value|
|
173
|
+
next if key.blank?
|
174
|
+
key = key.to_sym
|
175
|
+
if key == :uuid
|
176
|
+
@uuid = value
|
177
|
+
elsif self.class.body_fields.key?(key)
|
178
|
+
send "#{key}=", value
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# A Sequel filter for the current row in the database.
|
184
|
+
def rowset
|
185
|
+
@rowset ||= self.class.dataset.filter(:uuid => uuid.to_a.pack("H*"))
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Heresy
|
2
|
+
# This class is responsible for defining and creating the table used to store model data.
|
3
|
+
class Schema
|
4
|
+
attr_accessor :name
|
5
|
+
attr_reader :id, :uuid, :updated_at, :body
|
6
|
+
|
7
|
+
def initialize(db, name)
|
8
|
+
@db = db
|
9
|
+
@name = name
|
10
|
+
@id = Column.new(:id, :int, :size => 11, :unsigned => true)
|
11
|
+
@uuid = Column.new(:uuid, :uuid, :unique => true)
|
12
|
+
@updated_at = Column.new(:updated_at, :timestamp, :default => 'CURRENT_TIMESTAMP'.lit, :index => true)
|
13
|
+
@body = Column.new(:body, :blob)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Checks whether the model table exists or not.
|
17
|
+
def exists?
|
18
|
+
@db.table_exists?(@name)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Creates the model table.
|
22
|
+
def create
|
23
|
+
to_sql.each { |sql| @db.execute_ddl(sql) }
|
24
|
+
end
|
25
|
+
|
26
|
+
# Drops the model table.
|
27
|
+
def drop
|
28
|
+
@db.drop_table @name
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns an array of the SQL statements used to create the table and any indexes.
|
32
|
+
def to_sql
|
33
|
+
@db.create_table_sql_list @name, *generate.create_info
|
34
|
+
end
|
35
|
+
|
36
|
+
def inspect
|
37
|
+
"<Heresy::Schema:#{@name}>"
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
def generate
|
42
|
+
gen = Sequel::Schema::Generator.new(@db)
|
43
|
+
@id.create(gen, :primary_key)
|
44
|
+
[@uuid, @updated_at, @body].each { |c| c.create(gen) }
|
45
|
+
gen
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Heresy
|
2
|
+
class Schema
|
3
|
+
# This class defines a single column in the database, using the same options
|
4
|
+
# as the Sequel library. There are a few convenience types though:
|
5
|
+
#
|
6
|
+
# - :uuid: Creates a binary(16) database field
|
7
|
+
#
|
8
|
+
class Column
|
9
|
+
attr_accessor :name, :type, :options
|
10
|
+
|
11
|
+
# The actual database type. This may be different if the given type is a
|
12
|
+
# Heresy convenience type, or if the database supports the type under a different name.
|
13
|
+
attr_reader :db_type
|
14
|
+
|
15
|
+
def initialize(name, type, options = {})
|
16
|
+
if type == :uuid
|
17
|
+
@db_type = :binary
|
18
|
+
options.update(:size => 16, :null => false)
|
19
|
+
else
|
20
|
+
@db_type = type
|
21
|
+
end
|
22
|
+
@name = name
|
23
|
+
@type = type
|
24
|
+
@options = options
|
25
|
+
end
|
26
|
+
|
27
|
+
# Creates the column using the Sequel schema generator. By default, the #column
|
28
|
+
# method is used. An alternative method like :primary_key can be specified.
|
29
|
+
def create(gen, method = :column)
|
30
|
+
gen.send(method, @name, @db_type, @options)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Heresy
|
2
|
+
class Schema
|
3
|
+
# This class is a helper for setting up fields in the model. This is likely called
|
4
|
+
# from the model class and never used directly:
|
5
|
+
#
|
6
|
+
# class Entry < Heresy::Model
|
7
|
+
# fields do |f|
|
8
|
+
# f.field :title, :string
|
9
|
+
# f.string :body # Uses #string convenience method
|
10
|
+
# end
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
class Fields
|
14
|
+
module Integer
|
15
|
+
extend self
|
16
|
+
def parse(input) input.to_i end
|
17
|
+
def encode(input) input end
|
18
|
+
end
|
19
|
+
|
20
|
+
module Float
|
21
|
+
extend self
|
22
|
+
def parse(input) input.to_f end
|
23
|
+
def encode(input) input end
|
24
|
+
end
|
25
|
+
|
26
|
+
module DateTime
|
27
|
+
extend self
|
28
|
+
def parse(input)
|
29
|
+
case input
|
30
|
+
when Time then input
|
31
|
+
when String then Time.parse(input)
|
32
|
+
else input.to_time
|
33
|
+
end
|
34
|
+
end
|
35
|
+
def encode(input) input.xmlschema end
|
36
|
+
end
|
37
|
+
|
38
|
+
@@types = {}
|
39
|
+
def self.types() @@types end
|
40
|
+
def self.type(key, type = nil)
|
41
|
+
class_eval "def #{key}(name) field(name, :#{key}) end"
|
42
|
+
@@types[key] = type
|
43
|
+
end
|
44
|
+
|
45
|
+
type :string
|
46
|
+
type :integer, Integer
|
47
|
+
type :float, Float
|
48
|
+
type :datetime, DateTime
|
49
|
+
|
50
|
+
def initialize(model)
|
51
|
+
@model = model
|
52
|
+
end
|
53
|
+
|
54
|
+
# Adds a field of the given type to the model by creating an attribute reader and writer.
|
55
|
+
def field(name, type_name)
|
56
|
+
if type = self.class.types[type_name]
|
57
|
+
@model.send(:attr_reader, name)
|
58
|
+
@model.class_eval "def #{name}=(v) @#{name} = self.class.body_fields[:#{name}].parse(v) end"
|
59
|
+
else
|
60
|
+
@model.send(:attr_accessor, name)
|
61
|
+
end
|
62
|
+
@model.body_fields[name] = type
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/test/fields_test.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
|
2
|
+
|
3
|
+
module HeresyTest
|
4
|
+
class FieldsTest < Heresy::TestCase
|
5
|
+
describe "field types from mass assignment" do
|
6
|
+
before :all do
|
7
|
+
@entry = Entry.new :title => 'whoa', :a => 'ignored', :comments_count => "3.0", :rating => '5', :published_at => Time.utc(2008, 1, 1)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "sets a string" do
|
11
|
+
@entry.title.should == 'whoa'
|
12
|
+
end
|
13
|
+
|
14
|
+
it "sets an integer" do
|
15
|
+
@entry.comments_count.should == 3
|
16
|
+
end
|
17
|
+
|
18
|
+
it "sets a float" do
|
19
|
+
@entry.rating.should == 5.0
|
20
|
+
@entry.rating.to_s.should == "5.0"
|
21
|
+
end
|
22
|
+
|
23
|
+
it "sets a time" do
|
24
|
+
@entry.published_at.should == Time.utc(2008, 1, 1)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "field types from individual set" do
|
29
|
+
before :all do
|
30
|
+
@entry = Entry.new
|
31
|
+
end
|
32
|
+
|
33
|
+
it "sets a string" do
|
34
|
+
@entry.title = 'whoa'
|
35
|
+
@entry.title.should == 'whoa'
|
36
|
+
end
|
37
|
+
|
38
|
+
it "sets an integer" do
|
39
|
+
@entry.comments_count = '3.0'
|
40
|
+
@entry.comments_count.should == 3
|
41
|
+
end
|
42
|
+
|
43
|
+
it "sets a float" do
|
44
|
+
@entry.rating = 5
|
45
|
+
@entry.rating.should == 5.0
|
46
|
+
@entry.rating.to_s.should == "5.0"
|
47
|
+
end
|
48
|
+
|
49
|
+
it "sets a time from an xml schema" do
|
50
|
+
@entry.published_at = Time.utc(2008, 1, 1).xmlschema
|
51
|
+
@entry.published_at.should == Time.utc(2008, 1, 1)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "sets a time from a time" do
|
55
|
+
@entry.published_at = Time.utc(2008, 1, 1)
|
56
|
+
@entry.published_at.should == Time.utc(2008, 1, 1)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "field types from retrieved model" do
|
61
|
+
before :all do
|
62
|
+
@entry = Entry.new :title => 'whoa', :a => 'ignored', :comments_count => "3.0", :rating => '5', :published_at => Time.utc(2008, 1, 1)
|
63
|
+
@entry.save
|
64
|
+
@entry = Entry.find(@entry.uuid)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "sets a string" do
|
68
|
+
@entry.title = 'whoa'
|
69
|
+
@entry.title.should == 'whoa'
|
70
|
+
end
|
71
|
+
|
72
|
+
it "sets an integer" do
|
73
|
+
@entry.comments_count = '3.0'
|
74
|
+
@entry.comments_count.should == 3
|
75
|
+
end
|
76
|
+
|
77
|
+
it "sets a float" do
|
78
|
+
@entry.rating = 5
|
79
|
+
@entry.rating.should == 5.0
|
80
|
+
@entry.rating.to_s.should == "5.0"
|
81
|
+
end
|
82
|
+
|
83
|
+
it "sets a time" do
|
84
|
+
@entry.published_at.should == Time.utc(2008, 1, 1)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper'))
|
2
|
+
require 'heresy/formatting/json'
|
3
|
+
|
4
|
+
module HeresyTest::Formatting
|
5
|
+
class JsonTest < Heresy::TestCase
|
6
|
+
before :all do
|
7
|
+
@formatter = Heresy::Formatting::Json
|
8
|
+
end
|
9
|
+
|
10
|
+
it "encodes and decodes hash with strings" do
|
11
|
+
input = {:a => 1, :b => 'two'}
|
12
|
+
@formatter.decode(@formatter.encode(input)).should == {'a' => 1, 'b' => 'two'}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/test/index_test.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
|
2
|
+
|
3
|
+
module HeresyTest
|
4
|
+
class IndexTest < Heresy::TestCase
|
5
|
+
describe "Index creation" do
|
6
|
+
before :all do
|
7
|
+
@index = Heresy::Index.new(Heresy.db, :foo_bar_baz, :foo_id) { |idx| idx.field(:name, :varchar, :size => 255) }
|
8
|
+
end
|
9
|
+
|
10
|
+
after do
|
11
|
+
@index.drop rescue nil
|
12
|
+
end
|
13
|
+
|
14
|
+
it "generates CREATE TABLE sql" do
|
15
|
+
@index.to_sql.first.should =~ /CREATE TABLE/
|
16
|
+
end
|
17
|
+
|
18
|
+
it "detects existence of table" do
|
19
|
+
assert !@index.exists?
|
20
|
+
end
|
21
|
+
|
22
|
+
it "creates table" do
|
23
|
+
assert !@index.exists?
|
24
|
+
@index.create
|
25
|
+
assert @index.exists?
|
26
|
+
end
|
27
|
+
|
28
|
+
it "drops table" do
|
29
|
+
assert !@index.exists?
|
30
|
+
@index.create
|
31
|
+
assert @index.exists?
|
32
|
+
@index.drop
|
33
|
+
assert !@index.exists?
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "Index" do
|
38
|
+
before :all do
|
39
|
+
@index = Heresy::Index.new(Heresy.db, :foo_bar_baz, :foo_id) { |idx| idx.field(:name, :varchar, :size => 255) }
|
40
|
+
@index.create
|
41
|
+
end
|
42
|
+
|
43
|
+
it "allows data to be inserted" do
|
44
|
+
old = @index.count
|
45
|
+
@index << {:foo_id => UUID.generate(:compact), :name => 'bob'}
|
46
|
+
@index.count.should == old + 1
|
47
|
+
end
|
48
|
+
|
49
|
+
it "clears data" do
|
50
|
+
@index << {:foo_id => UUID.generate(:compact), :name => 'bob'}
|
51
|
+
@index.clear
|
52
|
+
@index.count.should == 0
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "(data retrieval)" do
|
56
|
+
before :all do
|
57
|
+
@index.clear
|
58
|
+
@uuid1 = UUID.generate(:compact)
|
59
|
+
@uuid2 = UUID.generate(:compact)
|
60
|
+
@index << {:foo_id => @uuid1, :name => 'bob'}
|
61
|
+
@index << {:foo_id => @uuid2, :name => 'bob'}
|
62
|
+
end
|
63
|
+
|
64
|
+
it "#find fetches data" do
|
65
|
+
@index.find(:name => 'bob').should == [@uuid1, @uuid2]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
after :all do
|
70
|
+
@index.drop rescue nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/test/model_test.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
|
2
|
+
|
3
|
+
module HeresyTest
|
4
|
+
class ModelTest < Heresy::TestCase
|
5
|
+
before :all do
|
6
|
+
@entry = Entry.new(:title => 'whoa', :body => 'test')
|
7
|
+
@entry.save
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "#save on new entry" do
|
11
|
+
before :all do
|
12
|
+
@new = Entry.new :title => 'whoa', :a => 'ignored', :comments_count => "3.0", :rating => '5'
|
13
|
+
@new.save
|
14
|
+
end
|
15
|
+
|
16
|
+
it "sets id" do
|
17
|
+
@new.id.should_not == nil
|
18
|
+
end
|
19
|
+
|
20
|
+
it "sets uuid" do
|
21
|
+
@new.uuid.should_not == nil
|
22
|
+
end
|
23
|
+
|
24
|
+
it "sets updated_at" do
|
25
|
+
@new.updated_at.class.should == Time
|
26
|
+
end
|
27
|
+
|
28
|
+
it "sets title" do
|
29
|
+
@new.title.should == 'whoa'
|
30
|
+
end
|
31
|
+
|
32
|
+
it "sets comments_count" do
|
33
|
+
@new.comments_count.should == 3
|
34
|
+
end
|
35
|
+
|
36
|
+
it "sets rating" do
|
37
|
+
@new.rating.should == 5.0
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "#save on existing entry" do
|
42
|
+
before :all do
|
43
|
+
sleep 1
|
44
|
+
@old_id = @entry.id
|
45
|
+
@old_uuid = @entry.uuid
|
46
|
+
@old_updated = @entry.updated_at
|
47
|
+
@entry.save
|
48
|
+
end
|
49
|
+
|
50
|
+
it "keeps id" do
|
51
|
+
@entry.id.should == @old_id
|
52
|
+
end
|
53
|
+
|
54
|
+
it "keeps uuid" do
|
55
|
+
@entry.uuid.should == @old_uuid
|
56
|
+
end
|
57
|
+
|
58
|
+
it "sets updated_at" do
|
59
|
+
@entry.updated_at.should_not == @old_updated
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "#find" do
|
64
|
+
before :all do
|
65
|
+
@entry2 = Entry.new(:title => 'whoa', :body => 'test')
|
66
|
+
@entry2.save
|
67
|
+
end
|
68
|
+
|
69
|
+
it "fetches by uuid" do
|
70
|
+
Entry.find(@entry.uuid).should == @entry
|
71
|
+
end
|
72
|
+
|
73
|
+
it "fetches by array uuids" do
|
74
|
+
Entry.find([@entry.uuid, @entry2.uuid]).should == [@entry, @entry2]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/test/schema_test.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
|
2
|
+
|
3
|
+
module HeresyTest
|
4
|
+
class SchemaTest < Heresy::TestCase
|
5
|
+
describe "Heresy Model Schema instance" do
|
6
|
+
it "sets model schema name to plural underscored class name" do
|
7
|
+
Entry.schema.name.should == :entries
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "Schema" do
|
12
|
+
before :all do
|
13
|
+
@schema = Heresy::Schema.new(Heresy.db, :foo_bar_baz)
|
14
|
+
end
|
15
|
+
|
16
|
+
after do
|
17
|
+
@schema.drop rescue nil
|
18
|
+
end
|
19
|
+
|
20
|
+
it "generates CREATE TABLE sql" do
|
21
|
+
@schema.to_sql.first.should =~ /CREATE TABLE/
|
22
|
+
end
|
23
|
+
|
24
|
+
it "detects existence of table" do
|
25
|
+
assert !@schema.exists?
|
26
|
+
end
|
27
|
+
|
28
|
+
it "creates table" do
|
29
|
+
assert !@schema.exists?
|
30
|
+
@schema.create
|
31
|
+
assert @schema.exists?
|
32
|
+
end
|
33
|
+
|
34
|
+
it "drops table" do
|
35
|
+
assert !@schema.exists?
|
36
|
+
@schema.create
|
37
|
+
assert @schema.exists?
|
38
|
+
@schema.drop
|
39
|
+
assert !@schema.exists?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
5
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
6
|
+
|
7
|
+
require 'rubygems'
|
8
|
+
gem 'jeremymcanally-matchy'
|
9
|
+
gem 'jeremymcanally-context'
|
10
|
+
gem 'rr'
|
11
|
+
|
12
|
+
require 'context'
|
13
|
+
require 'matchy'
|
14
|
+
require 'rr'
|
15
|
+
require 'logger'
|
16
|
+
require 'heresy-with-gems'
|
17
|
+
|
18
|
+
# TODO: remove this once it gets fixed in context
|
19
|
+
class String
|
20
|
+
def to_method_name
|
21
|
+
self.downcase.gsub(/[\s:',;!#()\.]+/,'_')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module Heresy
|
26
|
+
class TestCase < Test::Unit::TestCase
|
27
|
+
include RR::Adapters::TestUnit
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module HeresyTest
|
32
|
+
class Entry < Heresy::Model
|
33
|
+
fields do |f|
|
34
|
+
f.string :title
|
35
|
+
f.string :body
|
36
|
+
f.integer :comments_count
|
37
|
+
f.float :rating
|
38
|
+
f.datetime :published_at
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
Heresy.db = Sequel.connect("mysql://root@localhost/heresy_test")
|
44
|
+
HeresyTest::Entry.schema.drop rescue nil
|
45
|
+
HeresyTest::Entry.schema.create
|
46
|
+
|
47
|
+
begin
|
48
|
+
require 'ruby-debug'
|
49
|
+
Debugger.start
|
50
|
+
rescue LoadError
|
51
|
+
end
|
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: kabuki-heresy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- kabuki
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-03-29 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: activesupport
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ~>
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 2.2.0
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: sequel
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.10.0
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: json
|
37
|
+
type: :runtime
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 1.1.3
|
44
|
+
version:
|
45
|
+
- !ruby/object:Gem::Dependency
|
46
|
+
name: uuid
|
47
|
+
type: :runtime
|
48
|
+
version_requirement:
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 2.0.1
|
54
|
+
version:
|
55
|
+
description:
|
56
|
+
email: kabukiruby@gmail.com
|
57
|
+
executables: []
|
58
|
+
|
59
|
+
extensions: []
|
60
|
+
|
61
|
+
extra_rdoc_files:
|
62
|
+
- README.rdoc
|
63
|
+
- LICENSE
|
64
|
+
files:
|
65
|
+
- README.rdoc
|
66
|
+
- VERSION.yml
|
67
|
+
- lib/heresy
|
68
|
+
- lib/heresy/formatting
|
69
|
+
- lib/heresy/formatting/json.rb
|
70
|
+
- lib/heresy/index.rb
|
71
|
+
- lib/heresy/model.rb
|
72
|
+
- lib/heresy/schema
|
73
|
+
- lib/heresy/schema/column.rb
|
74
|
+
- lib/heresy/schema/fields.rb
|
75
|
+
- lib/heresy/schema.rb
|
76
|
+
- lib/heresy-with-gems.rb
|
77
|
+
- lib/heresy.rb
|
78
|
+
- test/fields_test.rb
|
79
|
+
- test/formatting
|
80
|
+
- test/formatting/json_test.rb
|
81
|
+
- test/index_test.rb
|
82
|
+
- test/model_test.rb
|
83
|
+
- test/schema_test.rb
|
84
|
+
- test/test_helper.rb
|
85
|
+
- LICENSE
|
86
|
+
has_rdoc: true
|
87
|
+
homepage: http://github.com/kabuki/heresy
|
88
|
+
post_install_message:
|
89
|
+
rdoc_options:
|
90
|
+
- --inline-source
|
91
|
+
- --charset=UTF-8
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: "0"
|
99
|
+
version:
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: "0"
|
105
|
+
version:
|
106
|
+
requirements: []
|
107
|
+
|
108
|
+
rubyforge_project:
|
109
|
+
rubygems_version: 1.2.0
|
110
|
+
signing_key:
|
111
|
+
specification_version: 2
|
112
|
+
summary: TODO
|
113
|
+
test_files: []
|
114
|
+
|