mudnaes-dbstruct 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/README +56 -0
  2. data/dbstruct.gemspec +21 -0
  3. data/lib/dbstruct.rb +204 -0
  4. data/spec/dbstruct_spec.rb +143 -0
  5. metadata +82 -0
data/README ADDED
@@ -0,0 +1,56 @@
1
+ This is a very simple framework to be able to access database rows as
2
+ objects without forcing you to inherit from a Model class. Instead
3
+ you create the class-hierarchy you want and use mixin to add
4
+ persistance functionality to the object.
5
+
6
+ It a lot simpler than an real Object-Relation-Mapper since it's ignores relations.
7
+
8
+ The benefit of this framework is that you can write code like this:
9
+
10
+ class AnObject
11
+ include DBStruct
12
+ @non_persisted_field
13
+ attr_accessor :non_persisted_field
14
+
15
+ def initialize(*args)
16
+ @non_persisted_field = args[0]
17
+ end
18
+
19
+ def return_value_from_field
20
+ return @non_persisted_field
21
+ end
22
+ end
23
+
24
+ Create a simple migration:
25
+
26
+ class CreateDb < Sequel::Migration
27
+ def up
28
+ create_table :person do
29
+ primary_key :id
30
+ text :name
31
+ float :amount
32
+ integer :age
33
+ end
34
+ end
35
+ end
36
+
37
+ An then start coding. To create new field you only have to add it to the migration.
38
+
39
+ DB = Sequel.sqlite '', :logger => [Logger.new($stdout)]
40
+ CreateDb.apply(DB,:up)
41
+ AnObject.bind_table(DB,:person)
42
+
43
+ r = AnObject.template
44
+ r.name = 'Donald'
45
+ r.amount = 10.1
46
+ r.age = 77
47
+
48
+ r.insert(DB)
49
+
50
+ r.age = 98
51
+ r.update(DB)
52
+ r.delete(DB)
53
+
54
+ This framework relies heavely on the work of others (Sequel and OpenStruct)
55
+
56
+ Morten Udnæs.
@@ -0,0 +1,21 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "dbstruct"
3
+ s.version = "0.1.1"
4
+ s.date = "2008-12-03"
5
+ s.summary = "Easy to use library for mapping rows between objects and database"
6
+ s.email = "mudnaes@gmail.com"
7
+ s.homepage = "http://github.com/mudnaes/dbstruct/tree/master"
8
+ s.description = "This is a very simple framework to be able to access database rows as objects without forcing you to inherit from a Model class. Instead you create the class-hierarchy you want and use mixin to add persistance functionality to the object."
9
+ s.has_rdoc = true
10
+ s.authors = ["Morten Udnæs"]
11
+ s.files = ["README",
12
+ "dbstruct.gemspec",
13
+ "lib/dbstruct.rb"]
14
+ s.test_files = ["spec/dbstruct_spec.rb"]
15
+ s.rdoc_options = ["--main", "README.txt"]
16
+ s.extra_rdoc_files = ["README"]
17
+ s.add_dependency("diff-lcs", ["> 0.0.0"])
18
+ s.add_dependency("mime-types", ["> 0.0.0"])
19
+ s.add_dependency("open4", ["> 0.0.0"])
20
+ end
21
+
@@ -0,0 +1,204 @@
1
+ require 'rubygems'
2
+ require 'ostruct'
3
+
4
+ module DBStruct
5
+ # The purpose of this module is to have a struct inside an ordinary object
6
+ # to contain feilds for persistence. The fields are accessed the same way
7
+ # as the other fields inside the object..
8
+ #
9
+
10
+
11
+ # Define dynamic accessors to struct fields
12
+ def method_missing(mid, *args)
13
+ fieldname = mid.id2name
14
+
15
+ if mid.id2name.match("=")
16
+ fieldname.chop!
17
+ if args.length != 1
18
+ raise ArgumentError, "Setter must have only one argument. Number of arguments found was (#{len} for 1)", caller(1)
19
+ end
20
+
21
+ # Make sure fields aren't added in error (typos)
22
+ if !@_dbdata.marshal_dump.key?(fieldname.to_sym) & @_frozen
23
+ raise ArgumentError, "Struct fields are frozen, reopen before using <field>= . Field was (#{fieldname} for 1)", caller(1)
24
+ end
25
+ end
26
+
27
+ if @_dbdata == nil
28
+ super(mid, *args)
29
+
30
+ elsif @_dbdata.marshal_dump.key?(fieldname.to_sym)
31
+ @_dbdata.send(mid.id2name,args[0])
32
+
33
+ # Route method missing to struct object
34
+ elsif !@_frozen & mid.id2name.match("=")
35
+ @_dbdata.method_missing(mid, *args)
36
+
37
+ # Forward methods missing not related to struct
38
+ else
39
+ super(mid, *args)
40
+ end
41
+ end
42
+
43
+ # Used for creating an object based on an hash (primarily for loading records directly from DB
44
+ def load(response)
45
+ if response != nil
46
+ @_dbdata = OpenStruct.new(response)
47
+ @_frozen = true
48
+ end
49
+ end
50
+
51
+ # Export hash from struct
52
+ def save(*args)
53
+ @_dbdata.marshal_dump(*args)
54
+ end
55
+
56
+ def get(db, pkid)
57
+ @_dbdata.pkid = db[self.class.get_table].filter(:pkid =>pkid)
58
+ end
59
+
60
+
61
+ def insert(db)
62
+ @_dbdata.pkid = db[self.class.get_table].insert(save)
63
+ end
64
+
65
+ def delete(db,search_criteria)
66
+ db[self.class.get_table].filter({:pkid =>pkid}).delete
67
+ end
68
+
69
+
70
+ def update(db)
71
+ db[self.class.get_table].filter({:pkid => pkid}).update(save)
72
+ end
73
+
74
+
75
+ # Define which fields are needed. Sent in as an array of symbols or strings
76
+ def setup_fields(fields)
77
+ h = {}
78
+ fields.each do |field|
79
+ if field.kind_of? Symbol
80
+ h[field.to_s] = nil
81
+ else
82
+ h[field] = nil
83
+ end
84
+ end
85
+ @_dbdata = OpenStruct.new(fields)
86
+ @_frozen = true
87
+ end
88
+
89
+ # Avoid anyone referencing @_dbdata directly
90
+ def empty?
91
+ @_dbdata == nil
92
+ end
93
+
94
+ # When overiding method_missing this one should also be...
95
+ def respond_to(*args)
96
+ super(*args)
97
+ end
98
+
99
+ # Freeze struct so new fields aren't added by mistake
100
+ def freeze_fields
101
+ @_frozen = true
102
+ end
103
+
104
+ # Unfreeze struct so new fields can be added manually. Remember to freeze afterwards
105
+ def unfreeze_fields
106
+ @_frozen = false
107
+ end
108
+
109
+ #Class methods for included dbstructs
110
+ def self.included receiver
111
+ receiver.extend DBStructClassMethods
112
+ end
113
+
114
+ module DBStructClassMethods
115
+ # Used to create objects based on database rows
116
+
117
+
118
+
119
+ def create(rows)
120
+ list = []
121
+ if rows == nil
122
+ return list
123
+ end
124
+ rows.each do |row|
125
+ the_instance = self.new
126
+ the_instance.load(row)
127
+ list << the_instance
128
+ end
129
+ list
130
+ end
131
+
132
+ #Hack enable referencing modularized class-variable from instance
133
+ def get_table
134
+ return @@_table
135
+ end
136
+
137
+ #Binds class to given table in the database, Sequel specific
138
+ def bind_table(db,table)
139
+ tablefields = []
140
+ @@_table = table
141
+
142
+ if db.schema(table).empty?
143
+ raise ArgumentError, "Table [#{table}] was not found. Unable to bind object", caller(1)
144
+ end
145
+
146
+ db.schema(table).each do |fields|
147
+ tablefields << fields[0]
148
+ end
149
+ bind(tablefields)
150
+ end
151
+
152
+ # Use this to bind a class to a table, so new objects created with template will match fields in database
153
+ def bind(fields)
154
+
155
+ if fields.empty?
156
+ raise ArgumentError, "Trying to bind object to empty list of fields..."
157
+ end
158
+
159
+ if (@@_template rescue nil) != nil
160
+ raise ArgumentError, "Class already bound to table: #{@@_template.to_s}. Use method rebind if new binding is needed", caller(1)
161
+ end
162
+ @@_template = {}
163
+ fields.each { |field| @@_template[field] = nil}
164
+ return
165
+ end
166
+
167
+ # Method can rebind fields if database changes dynamicly.... I'm not sure you should need this one....
168
+ def rebind!(fields)
169
+ @@_template = {}
170
+ fields.each { |field| @@_template[field] = nil}
171
+ return
172
+ end
173
+
174
+
175
+ def find(db,search_criteria)
176
+ db[@@_table].filter(search_criteria)
177
+ end
178
+
179
+
180
+ def delete!(db,search_criteria)
181
+ db[@@_table].filter(search_criteria).delete
182
+ end
183
+
184
+
185
+ def update!(db,search_criteria, update_criteria)
186
+ db[@@_table ].filter(search_criteria).update(update_criteria)
187
+ end
188
+
189
+
190
+
191
+ def template(*args)
192
+ the_instance = self.new(*args)
193
+ the_instance.load(@@_template)
194
+ the_instance
195
+ end
196
+
197
+ def find(db,search_criteria)
198
+ create(db[@@_table].where(search_criteria))
199
+
200
+ end
201
+
202
+ end
203
+
204
+ end
@@ -0,0 +1,143 @@
1
+ require 'rubygems'
2
+ require 'sequel'
3
+ require '../lib/dbstruct'
4
+ require 'logger'
5
+
6
+ class AnObject
7
+ include DBStruct
8
+ @non_persisted_field
9
+ attr_accessor :non_persisted_field
10
+
11
+ def initialize(*args)
12
+ @non_persisted_field = args[0]
13
+
14
+ end
15
+
16
+ def return_value_from_field
17
+ return @non_persisted_field
18
+ end
19
+ end
20
+
21
+ class CreateDb < Sequel::Migration
22
+ def up
23
+ create_table :person do
24
+ primary_key :pkid
25
+ text :name
26
+ float :amount
27
+ integer :age
28
+ end
29
+ end
30
+ end
31
+
32
+
33
+ class DBStruct_spec
34
+ DB = Sequel.sqlite '', :logger => [Logger.new($stdout)]
35
+ CreateDb.apply(DB,:up)
36
+ AnObject.bind_table(DB,:person)
37
+
38
+ def self.create_test_object
39
+ r = AnObject.template
40
+ r.name = 'Donald'
41
+ r.amount = 10.1
42
+ r.age = 77
43
+ return r
44
+ end
45
+
46
+
47
+ context "Create empty object and access fields" do
48
+ setup do
49
+
50
+ end
51
+
52
+ specify "All fields should have accesors based on table names" do
53
+ r = DBStruct_spec.create_test_object
54
+
55
+ r.name.should == 'Donald'
56
+ r.amount.should == 10.1
57
+ r.age.should == 77
58
+ end
59
+
60
+ specify "should raise error when accessing field that doesn't exist in database or object" do
61
+ r = AnObject.template
62
+ lambda {r.field_does_nt_exist}.should raise_error
63
+ end
64
+
65
+ specify "should be able to accesses transient (non persisted) values" do
66
+ r = AnObject.template
67
+ r.non_persisted_field = 'BOO'
68
+ r.non_persisted_field.should == 'BOO'
69
+ end
70
+
71
+ specify "should be able to pass variables to object constructor (initialize)" do
72
+ r = AnObject.template('BOO')
73
+ r.non_persisted_field.should == 'BOO'
74
+ end
75
+
76
+ specify "should be able to call ordinary method on persisted object" do
77
+ r = AnObject.template("BOO")
78
+ r.return_value_from_field.should == "BOO"
79
+ end
80
+ end
81
+
82
+
83
+ context "Use instance to perform insert, read, update and delete" do
84
+ setup do
85
+ end
86
+
87
+ specify "should be able to insert and read object" do
88
+ i = DBStruct_spec.create_test_object
89
+
90
+ lambda {i.insert(DB)}.should_not raise_error
91
+ from_db = AnObject.find(DB,{:age => 77}).first
92
+ from_db.name == i.name.should
93
+ from_db.amount == i.amount.should
94
+ from_db.age == i.age.should
95
+
96
+ i.amount = 12.1
97
+ i.update(DB)
98
+ from_db = AnObject.find(DB,{:age => 77}).first
99
+ from_db.name == i.name.should
100
+ from_db.amount == i.amount.should
101
+ from_db.age == i.age.should
102
+
103
+ lambda {AnObject.delete!(DB,{:age => 77})}.should_not raise_error
104
+ #i.delete(DB)
105
+ deleted = AnObject.find(DB,{:age => 77})
106
+ deleted.should == []
107
+ end
108
+ end
109
+
110
+ context "Using class to perform statements that changes multiple rows" do
111
+ specify "should update multiple rows" do
112
+ i1 = DBStruct_spec.create_test_object
113
+ i1.insert(DB)
114
+ i2 = DBStruct_spec.create_test_object
115
+ i2.insert(DB)
116
+ lambda {AnObject.update!(DB, {:name => 'Donald'}, :name => 'Dolly')}.should_not raise_error
117
+
118
+ from_db = AnObject.find(DB,{:name => 'Dolly'}).first
119
+ from_db.name.should == 'Dolly'
120
+ end
121
+
122
+ specify "should delete multiple rows" do
123
+
124
+ end
125
+ end
126
+
127
+ context "Use indirect approach to save and store objects" do
128
+ setup do
129
+ end
130
+
131
+ specify "should be able to insert and read object indirectly" do
132
+ r = DBStruct_spec.create_test_object
133
+ r.age = 88
134
+ DB[:person].insert(r.save)
135
+
136
+ row = DB[:person].filter({:age => 88})
137
+ from_db = AnObject.create(row).first
138
+ from_db.name.should == r.name
139
+ from_db.age.should == r.age
140
+ from_db.amount == r.amount
141
+ end
142
+ end
143
+ end
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mudnaes-dbstruct
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - "Morten Udn\xC3\xA6s"
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-12-03 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: diff-lcs
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">"
21
+ - !ruby/object:Gem::Version
22
+ version: 0.0.0
23
+ version:
24
+ - !ruby/object:Gem::Dependency
25
+ name: mime-types
26
+ version_requirement:
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - - ">"
30
+ - !ruby/object:Gem::Version
31
+ version: 0.0.0
32
+ version:
33
+ - !ruby/object:Gem::Dependency
34
+ name: open4
35
+ version_requirement:
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.0.0
41
+ version:
42
+ description: This is a very simple framework to be able to access database rows as objects without forcing you to inherit from a Model class. Instead you create the class-hierarchy you want and use mixin to add persistance functionality to the object.
43
+ email: mudnaes@gmail.com
44
+ executables: []
45
+
46
+ extensions: []
47
+
48
+ extra_rdoc_files:
49
+ - README
50
+ files:
51
+ - README
52
+ - dbstruct.gemspec
53
+ - lib/dbstruct.rb
54
+ has_rdoc: true
55
+ homepage: http://github.com/mudnaes/dbstruct/tree/master
56
+ post_install_message:
57
+ rdoc_options:
58
+ - --main
59
+ - README.txt
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: "0"
67
+ version:
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: "0"
73
+ version:
74
+ requirements: []
75
+
76
+ rubyforge_project:
77
+ rubygems_version: 1.2.0
78
+ signing_key:
79
+ specification_version: 2
80
+ summary: Easy to use library for mapping rows between objects and database
81
+ test_files:
82
+ - spec/dbstruct_spec.rb