git-ds 0.9.2

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.
Files changed (51) hide show
  1. data/README.rdoc +795 -0
  2. data/doc/Examples.rdoc +36 -0
  3. data/doc/examples/key_value/kv_get.rb +29 -0
  4. data/doc/examples/key_value/kv_init.rb +20 -0
  5. data/doc/examples/key_value/kv_list.rb +28 -0
  6. data/doc/examples/key_value/kv_remove.rb +29 -0
  7. data/doc/examples/key_value/kv_set.rb +39 -0
  8. data/doc/examples/key_value/model.rb +156 -0
  9. data/doc/examples/key_value/test.rb +50 -0
  10. data/doc/examples/test_suite/model.rb +503 -0
  11. data/doc/examples/test_suite/test.rb +173 -0
  12. data/doc/examples/test_suite/ts_add_bug.rb +65 -0
  13. data/doc/examples/test_suite/ts_add_module.rb +74 -0
  14. data/doc/examples/test_suite/ts_add_module_to_test.rb +78 -0
  15. data/doc/examples/test_suite/ts_add_test.rb +77 -0
  16. data/doc/examples/test_suite/ts_add_test_suite.rb +65 -0
  17. data/doc/examples/test_suite/ts_add_test_to_bug.rb +76 -0
  18. data/doc/examples/test_suite/ts_init.rb +20 -0
  19. data/doc/examples/test_suite/ts_list.rb +118 -0
  20. data/doc/examples/test_suite/ts_perform_test.rb +104 -0
  21. data/doc/examples/test_suite/ts_update_bugs.rb +58 -0
  22. data/doc/examples/user_group/model.rb +265 -0
  23. data/doc/examples/user_group/test.rb +64 -0
  24. data/doc/examples/user_group/ug_add_group.rb +39 -0
  25. data/doc/examples/user_group/ug_add_group_user.rb +36 -0
  26. data/doc/examples/user_group/ug_add_user.rb +39 -0
  27. data/doc/examples/user_group/ug_init.rb +20 -0
  28. data/doc/examples/user_group/ug_list.rb +32 -0
  29. data/lib/git-ds.rb +14 -0
  30. data/lib/git-ds/config.rb +53 -0
  31. data/lib/git-ds/database.rb +289 -0
  32. data/lib/git-ds/exec_cmd.rb +107 -0
  33. data/lib/git-ds/index.rb +205 -0
  34. data/lib/git-ds/model.rb +136 -0
  35. data/lib/git-ds/model/db_item.rb +42 -0
  36. data/lib/git-ds/model/fs_item.rb +51 -0
  37. data/lib/git-ds/model/item.rb +428 -0
  38. data/lib/git-ds/model/item_list.rb +97 -0
  39. data/lib/git-ds/model/item_proxy.rb +128 -0
  40. data/lib/git-ds/model/property.rb +144 -0
  41. data/lib/git-ds/model/root.rb +46 -0
  42. data/lib/git-ds/repo.rb +455 -0
  43. data/lib/git-ds/shared.rb +17 -0
  44. data/lib/git-ds/transaction.rb +77 -0
  45. data/tests/ut_database.rb +304 -0
  46. data/tests/ut_git_grit_equiv.rb +195 -0
  47. data/tests/ut_index.rb +203 -0
  48. data/tests/ut_model.rb +360 -0
  49. data/tests/ut_repo.rb +260 -0
  50. data/tests/ut_user_group_model.rb +316 -0
  51. metadata +142 -0
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env ruby
2
+ # :title: Git-DS::Index
3
+ =begin rdoc
4
+ Wrapper for Grit::Index
5
+
6
+ Copyright 2010 Thoughtgang <http://www.thoughtgang.org>
7
+ =end
8
+
9
+ require 'rubygems'
10
+ require 'grit'
11
+ require 'fileutils'
12
+
13
+ require 'git-ds/shared'
14
+
15
+ module GitDS
16
+
17
+ # =============================================================================
18
+ =begin rdoc
19
+ A Git Index.
20
+ =end
21
+ class Index < Grit::Index
22
+
23
+ =begin rdoc
24
+ Write index to the object db, then read the object DB into the GIT staging
25
+ index. Returns SHA of new tree.
26
+ =end
27
+ def write
28
+ sha = local_write_tree(self.tree, self.current_tree)
29
+ sha
30
+ end
31
+
32
+ =begin rdoc
33
+ Re-implemented from Grit. Grit adds a trailing / to directory names, which
34
+ makes it impossible to delete Tree objects!
35
+ =end
36
+ def local_write_tree(tree, now_tree=nil)
37
+ tree_contents = {}
38
+ now_tree.contents.each do |obj|
39
+ sha = [obj.id].pack("H*")
40
+ k = obj.name
41
+ tree_contents[k] = "%s %s\0%s" % [obj.mode.to_s, obj.name, sha]
42
+ end if now_tree
43
+
44
+ tree.each do |k, v|
45
+ case v
46
+ when String
47
+ sha = write_blob(v)
48
+ sha = [sha].pack("H*")
49
+ str = "%s %s\0%s" % ['100644', k, sha]
50
+ tree_contents[k] = str
51
+ when Hash
52
+ ctree = now_tree/k if now_tree
53
+ sha = local_write_tree(v, ctree)
54
+ sha = [sha].pack("H*")
55
+ str = "%s %s\0%s" % ['40000', k, sha]
56
+ tree_contents[k] = str
57
+ when false
58
+ tree_contents.delete(k)
59
+ end
60
+ end
61
+
62
+ tr = tree_contents.sort.map { |k, v| v }.join('')
63
+ self.repo.git.put_raw_object(tr, 'tree')
64
+ end
65
+
66
+ =begin rdoc
67
+ Add a DB entry at the virtual path 'path' with contents 'contents'
68
+ =end
69
+ alias :add_db :add
70
+
71
+ def add(path, data, on_fs=false)
72
+ super(path, data)
73
+ add_fs_item(path, data) if on_fs
74
+ end
75
+
76
+ =begin rdoc
77
+ Convenience function to add an on-filesystem object.
78
+ =end
79
+ def add_fs(path, data)
80
+ add(path, data, true)
81
+ end
82
+
83
+ =begin rdoc
84
+ Wrapper for Grit::Index#delete that removes the file if it exists on the
85
+ filesystem.
86
+ =end
87
+ def delete(path)
88
+ super
89
+ @repo.exec_in_git_dir {
90
+ ::FileUtils.remove_entry(path) if ::File.exist?(path)
91
+ }
92
+ end
93
+
94
+ private
95
+
96
+ =begin rdoc
97
+ Add a DB entry at the filesystem path 'path' with contents 'contents'
98
+ =end
99
+ def add_fs_item( path, data )
100
+ fs_path = @repo.top_level + ::File::SEPARATOR + path
101
+ make_parent_dirs(fs_path)
102
+
103
+ # Create file in filesystem
104
+ @repo.exec_in_git_dir { ::File.open(fs_path, 'w') {|f| f.write(data)} }
105
+ end
106
+
107
+ =begin rdoc
108
+ Add parent directories as-needed to create 'path' on the filesystem.
109
+ =end
110
+ def make_parent_dirs(path)
111
+ tmp_path = ''
112
+
113
+ ::File.dirname(path).split(::File::SEPARATOR).each do |dir|
114
+ next if dir.empty?
115
+ tmp_path << ::File::SEPARATOR << dir
116
+ Dir.mkdir(tmp_path) if not ::File.exist?(tmp_path)
117
+ end
118
+ end
119
+
120
+ end
121
+
122
+ =begin rdoc
123
+ Index object for the Git staging index.
124
+ =end
125
+ class StageIndex < Index
126
+
127
+ attr_reader :sha
128
+ attr_reader :parent_commit
129
+
130
+ =begin rdoc
131
+ =end
132
+ def initialize(repo,treeish=nil)
133
+ super(repo)
134
+ @parent_commit = repo.commits(repo.current_branch, 1).first
135
+ treeish = (@parent_commit ? @parent_commit.tree.id : 'master') if \
136
+ not treeish
137
+ read_tree(treeish)
138
+ @sha = self.current_tree.id
139
+ end
140
+
141
+ =begin rdoc
142
+ =end
143
+ def commit(msg, author=nil)
144
+ last_tree = @parent_commit ? @parent_commit.tree.id : nil
145
+ parents = @parent_commit ? [@parent_commit] : []
146
+ # TODO : why does last_tree cause some commits to fail?
147
+ # test_transaction(TC_GitDatabaseTest)
148
+ # transaction commit has wrong message.
149
+ # <"SUCCESS"> expected but was <"auto-commit on transaction">
150
+ # Possible bug in Grit::last_tree?
151
+ #sha = super(msg, parents, author, last_tree, @repo.current_branch)
152
+ sha = super(msg, parents, author, nil, @repo.current_branch)
153
+ if sha
154
+ @parent_commit = @repo.commit(sha)
155
+ read_tree(@parent_commit.tree.id)
156
+ end
157
+ sha
158
+ end
159
+
160
+ =begin rdoc
161
+ Write tree object for index to object database.
162
+ =end
163
+ def write
164
+ @sha = super
165
+ end
166
+
167
+ =begin rdoc
168
+ Write, read tree. Done when a tree is requested.
169
+ =end
170
+ def build
171
+ return @sha if self.tree.empty?
172
+ self.read_tree(self.write)
173
+ end
174
+
175
+ def read_tree(sha)
176
+ super
177
+ end
178
+
179
+ =begin rdoc
180
+ Sync with staging index. This causes the Git index (used by command-line tools)
181
+ to be filled with the contents of this index.
182
+
183
+ This can be instead of a commit to ensure that command-line tools can access
184
+ the index contents.
185
+ =end
186
+ def sync
187
+ self.build
188
+ @repo.exec_in_git_dir { `git read-tree #{@sha}` }
189
+ end
190
+
191
+ =begin rdoc
192
+ Read staging index from disk and create a StagingIndex object for it.
193
+
194
+ This can be used to access index contents created by command-line tools.
195
+ =end
196
+ def self.read(repo)
197
+ sha = repo.exec_in_git_dir{`git write-tree`}.chomp
198
+ new(repo, sha)
199
+ end
200
+
201
+ end
202
+
203
+
204
+ end
205
+
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env ruby
2
+ # :title: Git-DS::Model
3
+ =begin rdoc
4
+
5
+ Copyright 2010 Thoughtgang <http://www.thoughtgang.org>
6
+ =end
7
+
8
+ require 'git-ds/shared'
9
+ require 'git-ds/config'
10
+ require 'git-ds/model/property'
11
+ require 'git-ds/model/item'
12
+ require 'git-ds/model/db_item'
13
+ require 'git-ds/model/fs_item'
14
+ require 'git-ds/model/item_list'
15
+ require 'git-ds/model/item_proxy'
16
+ require 'git-ds/model/root'
17
+
18
+ # TODO: REFACTOR to act as delegate for database
19
+
20
+ # TODO: register ModelItemClass name, e.g. 'user', with model.
21
+ # then to instantiate, get item name from path, e.g.
22
+ # system/1/user/1 would be 'user'
23
+ # ...and use that to determine class to instantiate.
24
+ # TODO: query/find/grep (search of object contents or paths)
25
+
26
+ module GitDS
27
+
28
+ =begin rdoc
29
+ A data model.
30
+ =end
31
+ class Model
32
+
33
+ =begin rdoc
34
+ The Root item for the Model.
35
+ =end
36
+ attr_reader :root
37
+
38
+ =begin rdoc
39
+ The database connection for the model. This is expected to be a GitDS::Database
40
+ object.
41
+ =end
42
+ attr_reader :db
43
+
44
+ =begin rdoc
45
+ The name of the model. This is only used for storing configuration variables.
46
+ =end
47
+ attr_reader :name
48
+
49
+ def initialize(db, name='generic', root=nil)
50
+ @db = db
51
+ @root = root ? root : RootItem.new(self)
52
+ @name = name
53
+ end
54
+
55
+ =begin rdoc
56
+ Provides access to the Hash of Model-specific config variables.
57
+ =end
58
+ def config
59
+ @git_config ||= RepoConfig.new(@db, 'model-' + @name)
60
+ end
61
+
62
+ =begin rdoc
63
+ Returns true if Model contains path.
64
+ =end
65
+ def include?(path)
66
+ @db.include? path
67
+ end
68
+
69
+ alias :exist? :include?
70
+
71
+ =begin rdoc
72
+ List children (filenames) of path. Returns [] if path is not a directory.
73
+ =end
74
+ def list_children(path=root.path)
75
+ @db.list(path).keys.sort
76
+ end
77
+
78
+ =begin rdoc
79
+ Add an item to the object DB.
80
+ =end
81
+ # Might be better as add child?
82
+ def add_item(path, data)
83
+ # note: @db.add uses exec {} so there is no need to here.
84
+ @db.add(path, data)
85
+ end
86
+
87
+ =begin rdoc
88
+ Add an item to the object DB and the filesystem.
89
+ =end
90
+ def add_fs_item(path, data)
91
+ # note: @db.add uses exec {} so there is no need to here.
92
+ @db.add(path, data, true)
93
+ end
94
+
95
+ =begin rdoc
96
+ Return the contents of the BLOB at path.
97
+ =end
98
+ def get_item(path)
99
+ @db.object_data(path)
100
+ end
101
+
102
+ =begin rdoc
103
+ Delete an item from the object DB (and the filesystem, if it exists).
104
+ =end
105
+ def delete_item(path)
106
+ # note: @db.delete uses exec {} so there is no need to here.
107
+ @db.delete(path)
108
+ end
109
+
110
+ =begin rdoc
111
+ Execute block as a database ExecCmd.
112
+ =end
113
+ def exec(&block)
114
+ @db.exec(&block)
115
+ end
116
+
117
+ =begin rdoc
118
+ Execute block as a database transaction.
119
+ =end
120
+ def transaction(&block)
121
+ @db.transaction(&block)
122
+ end
123
+
124
+ =begin rdoc
125
+ Execute a transaction in a branch, then merge if it was successful.
126
+
127
+ See Database#branch_and_merge.
128
+ =end
129
+ def branched_transaction(name=@db.next_branch_tag(), &block)
130
+ raise 'Branched transactions cannot be nested' if @db.staging?
131
+ @db.branch_and_merge(name, &block)
132
+ end
133
+
134
+ end
135
+
136
+ end
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env ruby
2
+ # :title: Git-DS::DbModelItem
3
+ =begin rdoc
4
+
5
+ Copyright 2011 Thoughtgang <http://www.thoughtgang.org>
6
+ =end
7
+
8
+ require 'git-ds/shared'
9
+ require 'git-ds/model/item'
10
+
11
+ module GitDS
12
+
13
+ # ----------------------------------------------------------------------
14
+ =begin rdoc
15
+ An in-DB ModelItem mixin. DbModelItems exist only in the database.
16
+
17
+ Note: this is an instance-method module. It should be included, not extended.
18
+ =end
19
+ module DbModelItemObject
20
+ include ModelItemObject
21
+ end
22
+
23
+ # ----------------------------------------------------------------------
24
+ =begin rdoc
25
+ Note: this is a class-method module. It should be extended in a class, not
26
+ included.
27
+ =end
28
+ module DbModelItemClass
29
+ include ModelItemClass
30
+
31
+ end
32
+
33
+ # ----------------------------------------------------------------------
34
+ =begin rdoc
35
+ Base class for DB-only ModelItem objects. These do not appear in the filesystem.
36
+ =end
37
+ class ModelItem
38
+ extend DbModelItemClass
39
+ include DbModelItemObject
40
+ end
41
+
42
+ end
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env ruby
2
+ # :title: Git-DS::FsModelItem
3
+ =begin rdoc
4
+
5
+ Copyright 2011 Thoughtgang <http://www.thoughtgang.org>
6
+ =end
7
+
8
+ require 'git-ds/shared'
9
+ require 'git-ds/model/item'
10
+
11
+ module GitDS
12
+
13
+ # ----------------------------------------------------------------------
14
+ =begin rdoc
15
+ A filesystem ModelItem mixin. FsModelItems exist both on the filesystem and
16
+ in the database.
17
+
18
+ Note: this is an instance-method module. It should be included, not extended.
19
+ =end
20
+ module FsModelItemObject
21
+ include ModelItemObject
22
+ end
23
+
24
+ # ----------------------------------------------------------------------
25
+ =begin rdoc
26
+ Note: this is a class-method module. It should be extended in a class, not
27
+ included.
28
+ =end
29
+ module FsModelItemClass
30
+ include ModelItemClass
31
+
32
+ =begin rdoc
33
+ Define a property for this ModelItem class. The property will exist in the
34
+ DB and on the filesystem.
35
+ =end
36
+ def property(name, default=0, &block)
37
+ define_fs_property(name, default, &block)
38
+ end
39
+
40
+ end
41
+
42
+ # ----------------------------------------------------------------------
43
+ =begin rdoc
44
+ Base class for filesystem ModelItem objects.
45
+ =end
46
+ class FsModelItem
47
+ extend FsModelItemClass
48
+ include FsModelItemObject
49
+ end
50
+
51
+ end
@@ -0,0 +1,428 @@
1
+ #!/usr/bin/env ruby
2
+ # :title: Git-DS::ModelItem
3
+ =begin rdoc
4
+
5
+ Copyright 2011 Thoughtgang <http://www.thoughtgang.org>
6
+
7
+ Notes:
8
+ The children of an item will be one of the following:
9
+ * A Property (a named BLOB containing a value)
10
+ * A ModelItem (a subdirectory that defines a ModelItem instance)
11
+ * A ModelItem link (a named BLOB containing a path to a ModelItem instance)
12
+ Note that ModelItems and ModelItemLinks will be in a subdirectory named for
13
+ the ModelItem, even if there is only a single entry.
14
+ =end
15
+
16
+ require 'time' # for timestamp proprties
17
+ require 'git-ds/shared'
18
+ require 'git-ds/model/property'
19
+
20
+ module GitDS
21
+
22
+ class InvalidModelItemError < RuntimeError
23
+ end
24
+
25
+ # ----------------------------------------------------------------------
26
+ =begin rdoc
27
+ ModelItem class methods.
28
+
29
+ Note: this is a class-method module. It should be extended in a class, not
30
+ included.
31
+ =end
32
+ module ModelItemClass
33
+
34
+ =begin rdoc
35
+ The name of the ModelItemClass in the database.
36
+
37
+ To be overridden by a modelitem class.
38
+ =end
39
+ def name(name=nil)
40
+ @name ||= nil
41
+ raise 'ModelItemClass has no name defined' if (not name) && (not @name)
42
+ name ? @name = name : @name
43
+ end
44
+
45
+ =begin rdoc
46
+ Return the path to the ModelItem class owned by the specified parent.
47
+
48
+ For example, given the following database:
49
+
50
+ ClassA/a1/ClassB/b1
51
+ ClassA/a1/ClassB/b2
52
+ ClassA/a2/ClassB/b3
53
+
54
+ ClassA.path(@model.root) will return 'ClassA', ClassB.path(a1) will return
55
+ 'ClassA/a1/ClassB', and ClassB.path(a2) will return 'ClassA/a2/ClassB'.
56
+ =end
57
+ def path(parent)
58
+ return name if not parent
59
+ build_path(parent.path)
60
+ end
61
+
62
+ =begin rdoc
63
+ Return the path to the ModelItem class inside the specified directory.
64
+ =end
65
+ def build_path(parent_path)
66
+ return name if (not parent_path) || parent_path.empty?
67
+ parent_path + ::File::SEPARATOR + name
68
+ end
69
+
70
+ =begin rdoc
71
+ Return the path to an instance of the object under parent_path.
72
+ =end
73
+ def instance_path(parent_path, ident)
74
+ path = build_path(parent_path)
75
+ path += ::File::SEPARATOR if not path.empty?
76
+ path += ident.to_s
77
+ end
78
+
79
+ =begin rdoc
80
+ List all children of this ModelItem class.
81
+
82
+ This will list all instances of the class owned by the specified parent.
83
+ For example, given the following database:
84
+
85
+ ClassA/a1/ClassB/b1
86
+ ClassA/a1/ClassB/b2
87
+ ClassA/a2/ClassB/b3
88
+
89
+ ClassA.list(@model.root) will return [a1, a2], ClassB.list(a1) will return
90
+ [b1, b2], and ClassB.list(a2) will return [b3].
91
+ =end
92
+ def list(parent)
93
+ list_in_path parent.model, parent.path
94
+ end
95
+
96
+ =begin rdoc
97
+ List all children of the ModelItem class inside parent_path.
98
+ =end
99
+ def list_in_path(model, parent_path)
100
+ model.list_children build_path(parent_path)
101
+ end
102
+
103
+ =begin rdoc
104
+ Generate an ident from a Hash of arguments.
105
+
106
+ To be overridden by a modelitem class.
107
+ =end
108
+ def ident(args)
109
+ args[ident_key()].to_s
110
+ end
111
+
112
+ =begin rdoc
113
+ The key containing the object ident in the args Hash passed to create.
114
+
115
+ This can be used to change the name of the ident key in the hash without
116
+ having to override the ident method.
117
+ =end
118
+ def ident_key
119
+ :ident
120
+ end
121
+
122
+ =begin rdoc
123
+ Create a new instance of the ModelItemClass owned by the specified parent.
124
+
125
+ This will create a subdirectory under ModelItemClass.path(parent); the name
126
+ of the directory is determined by ModelItemClass.ident.
127
+
128
+ For example, given the following database:
129
+
130
+ ClassA/a1/ClassB/b1
131
+ ClassA/a1/ClassB/b2
132
+ ClassA/a2/ClassB/b3
133
+
134
+ ModelItemClass.create(a2, { :ident => 'b4' } ) will create the directory
135
+ 'ClassA/a2/ClassB/b4'.
136
+
137
+ The directory will then be filled by calling ModelItemClass.fill.
138
+
139
+ Note that this returns the path to the created item, not an instance.
140
+ =end
141
+ def create(parent, args={})
142
+ raise "Use Database.root instead of nil for parent" if not parent
143
+ raise "parent is not a ModelItem" if not parent.respond_to? :model
144
+
145
+ model = parent.model
146
+ create_in_path(parent.model, parent.path, args)
147
+ end
148
+
149
+ =begin rdoc
150
+ Create a new instance of the ModelItemClass in the specified directory.
151
+
152
+ This will create a subdirectory under parent_path; the name of the directory
153
+ is determined by ModelItemClass.ident.
154
+
155
+ For example, given the following database:
156
+
157
+ ClassA/a1/ClassB/b1
158
+ ClassA/a1/ClassB/b2
159
+ ClassA/a2/ClassB/b3
160
+
161
+ ModelItemClass.create(a2, { :ident => 'b4' } ) will create the directory
162
+ 'ClassA/a2/ClassB/b4'.
163
+
164
+ The directory will then be filled by calling ModelItemClass.fill.
165
+
166
+ The creation of the objects in the model takes place within a DB transaction.
167
+ If this is called from within a transaction, it will use the existing staging
168
+ index; otherwise, it will create a new index and auto-commit on success.
169
+
170
+ Note that this returns the ident of the created item, not an instance.
171
+ =end
172
+ def create_in_path(model, parent_path, args)
173
+ id = ident(args)
174
+ item_path = build_path(parent_path) + ::File::SEPARATOR + id
175
+
176
+ # Ensure that nested calls (e.g. to create children) share index#write
177
+ cls = self
178
+ model.transaction {
179
+ propagate
180
+ cls.fill(model, item_path, args)
181
+ }
182
+
183
+ item_path
184
+ end
185
+
186
+ =begin rdoc
187
+ Create all subdirectories and files needed to represent a ModelItemClass
188
+ instance in the object repository.
189
+
190
+ item_path is the full path to the item in the model.
191
+ args is a hash of arguments used to construct the item.
192
+
193
+ Can be overridden by ModelItem classes and invoked via super.
194
+ =end
195
+ def fill(model, item_path, args)
196
+ fill_properties(model, item_path, args)
197
+ end
198
+
199
+ =begin rdoc
200
+ Fill all properties either with their value in 'args' or their default value.
201
+
202
+ Foreach key in properties,
203
+ if args.include?(key) && property.valid?(key, args[key])
204
+ set property to key
205
+ elsif properties[key].default
206
+ set property to default
207
+ else ignore
208
+ =end
209
+ def fill_properties(model, item_path, args)
210
+ hash = properties
211
+ hash.keys.each do |key|
212
+ prop = hash[key]
213
+ if args.include?(key)
214
+ prop.set(model, item_path, args[key])
215
+ elsif hash[key].default
216
+ prop.set(model, item_path, prop.default)
217
+ end
218
+ end
219
+ end
220
+
221
+ =begin rdoc
222
+ Define a property for this ModelItem class.
223
+ =end
224
+ def define_db_property(name, default=0, &block)
225
+ add_property PropertyDefinition.new(name, default, false, &block)
226
+ end
227
+
228
+ =begin rdoc
229
+ Define an on-filesystem property for this ModelItem class.
230
+ =end
231
+ def define_fs_property(name, default=0, &block)
232
+ add_property PropertyDefinition.new(name, default, true, &block)
233
+ end
234
+
235
+ =begin rdoc
236
+ Define a Property for this ModelItem class. The property will be DB-only.
237
+
238
+ This can be overridden to change how properties are stored.
239
+ =end
240
+ def property(name, default=0, &block)
241
+ define_db_property(name, default, &block)
242
+ end
243
+
244
+ =begin rdoc
245
+ Define a property that is a link to a ModelItem object.
246
+
247
+ Note: this is a link to a single ModelItem class. For a list of links to
248
+ ModelItems, use a ModelItem List of ProxyModelItemClass objects.
249
+ =end
250
+ def link_property(name, cls, &block)
251
+ add_property ProxyProperty.new(name, cls, false, &block)
252
+ end
253
+
254
+
255
+ =begin rdoc
256
+ Hash of properties associated with this MOdelItem class.
257
+ =end
258
+ def properties
259
+ @properties ||= {}
260
+ end
261
+
262
+ private
263
+
264
+ =begin rdoc
265
+ Add a property to the properties Hash. This throws GitDS::DuplicatePropertyError
266
+ if a property with the same name already exists in the class.
267
+ =end
268
+ def add_property(p)
269
+ hash = properties
270
+ raise DuplicatePropertyError.new(p.name) if hash.include? p.name
271
+ hash[p.name] = p
272
+ end
273
+ end
274
+
275
+ # ----------------------------------------------------------------------
276
+ =begin rdoc
277
+ Instance methods used by repo-backed objects.
278
+
279
+ Note: this is an instance-method module. It should be included, not extended.
280
+ =end
281
+ module ModelItemObject
282
+
283
+ =begin rdoc
284
+ The GitDS::Model that contains the object.
285
+ =end
286
+ attr_reader :model
287
+
288
+ def initialize(model, path)
289
+ @model = model
290
+ @path = path
291
+ @ident = ::File.basename(path)
292
+ end
293
+
294
+ =begin rdoc
295
+ Full path to this item in the repo.
296
+ =end
297
+ def path
298
+ ensure_valid
299
+ @path
300
+ end
301
+
302
+ =begin rdoc
303
+ Primary key (ident) for instance.
304
+ =end
305
+ def ident
306
+ ensure_valid
307
+ @ident
308
+ end
309
+
310
+ =begin rdoc
311
+ Return list of property names.
312
+ =end
313
+ def properties
314
+ self.class.properties.keys.sort
315
+ end
316
+
317
+ =begin rdoc
318
+ Return Hash of cached property values.
319
+ =end
320
+ def property_cache
321
+ ensure_valid
322
+ @property_cache ||= {}
323
+ end
324
+
325
+ =begin rdoc
326
+ Return the value of a specific property. If the proprty has not been set,
327
+ nil is returned.
328
+
329
+ ModelItem classes will generally write property accessors that wrap the
330
+ call to this method.
331
+ =end
332
+ def property(name)
333
+ ensure_valid
334
+ return property_cache[name] if property_cache.include? name
335
+ prop = self.class.properties[name]
336
+ raise "No such property #{name}" if not prop
337
+ property_cache[name] = prop.get(@model, @path)
338
+ end
339
+
340
+ =begin rdoc
341
+ Convenience method for reading Integer properties.
342
+ =end
343
+ def integer_property(name)
344
+ val = property(name)
345
+ val ? property_cache[name] = val.to_i : nil
346
+ end
347
+
348
+ =begin rdoc
349
+ Convenience method for reading Float properties.
350
+ =end
351
+ def float_property(name)
352
+ val = property(name)
353
+ val ? property_cache[name] = val.to_f : nil
354
+ end
355
+
356
+ =begin rdoc
357
+ Convenience method for reading Time (aka timestamp) properties.
358
+ =end
359
+ def ts_property(name)
360
+ val = property(name)
361
+ val && (! val.empty?) ? property_cache[name] = Time.parse(val) : nil
362
+ end
363
+
364
+ =begin rdoc
365
+ Convenience method for reading Boolean properties.
366
+ =end
367
+ def bool_property(name)
368
+ val = property(name)
369
+ (val && val == 'true')
370
+ end
371
+
372
+ =begin rdoc
373
+ Convenience method for reading Array properties.
374
+
375
+ Note that this returns an Array of Strings.
376
+
377
+ Note: the default delimiter is what Property uses to encode Array objects.
378
+ Classes which perform their own encoding can choose a different delimiter.
379
+ =end
380
+ def array_property(name, delim="\n")
381
+ val = property(name)
382
+ val ? (property_cache[name] = val.split(delim)) : nil
383
+ end
384
+
385
+ =begin rdoc
386
+ Set the value of a specific property.
387
+
388
+ ModelItem classes will generally write property accessors that wrap the
389
+ call to this method.
390
+ =end
391
+ def set_property(name, data)
392
+ ensure_valid
393
+ prop = self.class.properties[name]
394
+ raise "No such property #{name}" if not prop
395
+ property_cache[name] = prop.set(@model, @path, data)
396
+ end
397
+
398
+ =begin rdoc
399
+ =end
400
+ def delete
401
+ ensure_valid
402
+ @model.delete_item(@path)
403
+ # invalidate object
404
+ @path = nil
405
+ end
406
+
407
+ =begin rdoc
408
+ Return true if item is valid, false otherwise.
409
+ =end
410
+ def valid?
411
+ @path # an invalid item has a nil path
412
+ end
413
+
414
+ protected
415
+
416
+ =begin rdoc
417
+ Raises an InvalidModelItemError if item is not valid.
418
+
419
+ Note: accessors for non-property children should invoke this before
420
+ touching the object.
421
+ =end
422
+ def ensure_valid
423
+ raise InvalidModelItemError if not valid?
424
+ end
425
+
426
+ end
427
+
428
+ end