plain_record 0.1.0 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,146 @@
1
+ =begin
2
+ Module to add before/after hooks.
3
+
4
+ Copyright (C) 2009 Andrey “A.I.” Sitnik <andrey@sitnik.ru>
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU Lesser General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU Lesser General Public License for more details.
15
+
16
+ You should have received a copy of the GNU Lesser General Public License
17
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ =end
19
+
20
+ module PlainRecord
21
+ # Callbacks are hooks that allow you to define methods to run before and
22
+ # after some method, to change it logic.
23
+ module Callbacks
24
+ # Hash of class callbacks with property.
25
+ attr_accessor :callbacks
26
+
27
+ # Set block as callback before +events+. Callback with less +priority+ will
28
+ # start earlier.
29
+ #
30
+ # class File
31
+ # include PlainRecord::Callbacks
32
+ #
33
+ # attr_accessor :name
34
+ # attr_accessor :content
35
+ #
36
+ # def save
37
+ # use_callbacks(:save, self) do
38
+ # File.open(@name, 'w') { |io| io.puts @content }
39
+ # end
40
+ # end
41
+ # end
42
+ #
43
+ # class NewFile < File
44
+ # before :save do |file|
45
+ # while File.exists? file.name
46
+ # file.name = 'another ' + file.name
47
+ # end
48
+ # end
49
+ #
50
+ # before, :save do
51
+ # raise ArgumentError if 255 < @name.length
52
+ # end
53
+ # end
54
+ def before(events, priority = 1, &block)
55
+ Array(events).each do |event|
56
+ add_callback(:before, event, priority, block)
57
+ end
58
+ end
59
+
60
+ # Set block as callback after +events+. Callback with less +priority+ will
61
+ # start earlier.
62
+ #
63
+ # After callbacks may change method return, which will be pass as first
64
+ # argument for first callback. It return will be pass for next callback and
65
+ # so on.
66
+ #
67
+ # class Person
68
+ # include PlainRecord::Callbacks
69
+ #
70
+ # def name
71
+ # use_callbacks(:name) do
72
+ # 'John'
73
+ # end
74
+ # end
75
+ # end
76
+ #
77
+ # class GreatPerson < Person
78
+ # after :name, 2 do |name|
79
+ # 'Great ' + name
80
+ # end
81
+ #
82
+ # after :name do |name|
83
+ # 'The ' + name
84
+ # end
85
+ # end
86
+ #
87
+ # GreatPerson.new.name #=> "The Great John"
88
+ def after(events, priority = 1, &block)
89
+ Array(events).each do |event|
90
+ add_callback(:after, event, priority, block)
91
+ end
92
+ end
93
+
94
+ # Call +before+ callbacks for +event+ with +params+. In your
95
+ # code use more pretty +use_callbacks+ method.
96
+ def call_before_callbacks(event, params)
97
+ init_callbacks(event)
98
+ @callbacks[:before][event].each do |before, priority|
99
+ before.call(*params)
100
+ end
101
+ end
102
+
103
+ # Call +before+ callbacks for +event+ with +params+. Callbacks can change
104
+ # +result+. In your code use more pretty +use_callbacks+ method.
105
+ def call_after_callbacks(event, result, params)
106
+ init_callbacks(event)
107
+ @callbacks[:after][event].each do |after, priority|
108
+ result = after.call(result, *params)
109
+ end
110
+ result
111
+ end
112
+
113
+ # Call before callback for +event+, run block and give it result to
114
+ # after callbacks.
115
+ #
116
+ # def my_save_method(entry)
117
+ # use_callbacks(:save, enrty) do
118
+ # entry.file.write
119
+ # end
120
+ # end
121
+ def use_callbacks(event, *params, &block)
122
+ call_before_callbacks(event, params)
123
+ result = yield
124
+ call_after_callbacks(event, result, params)
125
+ end
126
+
127
+ private
128
+
129
+ # Backend for +before+ and +after+ method to add callback.
130
+ def add_callback(type, event, priority, block)
131
+ init_callbacks(event)
132
+
133
+ @callbacks[type][event] << [block, priority]
134
+ @callbacks[type][event].sort! { |a, b| a[1] <=> b[1] }
135
+ end
136
+
137
+ # Check and create Hash into +callbacks+ for +event+ if necessary.
138
+ def init_callbacks(event)
139
+ unless @callbacks
140
+ @callbacks = { :before => { }, :after => { } }
141
+ end
142
+ @callbacks[:before][event] = [] unless @callbacks[:before][event]
143
+ @callbacks[:after][event] = [] unless @callbacks[:after][event]
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,141 @@
1
+ =begin
2
+ Extention to get property from entry file path.
3
+
4
+ Copyright (C) 2009 Andrey “A.I.” Sitnik <andrey@sitnik.ru>
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU Lesser General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU Lesser General Public License for more details.
15
+
16
+ You should have received a copy of the GNU Lesser General Public License
17
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+ =end
19
+
20
+ module PlainRecord
21
+ # Extention to get properties from enrty file path. For example, your blog
22
+ # post may stored in <tt>_name_/post.md</tt>, and post model will have +name+
23
+ # property. Also if you set name property to Model#first or Model#all method,
24
+ # they will load entry directly only by it file.
25
+ #
26
+ # To define filepath property:
27
+ # 1. Use <tt>*</tt> or <tt>**</tt> pattern in model path in +enrty_in+ or
28
+ # +list_in+.
29
+ # 2. In +virtual+ method use <tt>in_filepath(i)</tt> definer after name with
30
+ # <tt>*</tt> or <tt>**</tt> number (start from 1).
31
+ #
32
+ # Define filepath property only after +entry_in+ or +list_in+ call.
33
+ #
34
+ # class Post
35
+ # include PlainRecord::Resource
36
+ #
37
+ # entry_in '*/*/post.md'
38
+ #
39
+ # virtual :category, in_filepath(1)
40
+ # virtual :name, in_filepath(1)
41
+ # …
42
+ # end
43
+ #
44
+ # superpost = Post.new
45
+ # superpost.name = 'superpost'
46
+ # superpost.category = 'best/'
47
+ # superpost.save # Save to best/superpost/post.md
48
+ #
49
+ # bests = Post.all(category: 'best') # Look up only in best/ dir
50
+ module Filepath
51
+ attr_accessor :filepath_properties
52
+ attr_accessor :filepath_regexp
53
+
54
+ private
55
+
56
+ # Return definer for filepath property for +number+ <tt>*</tt> or
57
+ # <tt>**</tt> pattern in path.
58
+ def in_filepath(number)
59
+ proc do |property, caller|
60
+ if :virtual != caller
61
+ raise ArgumentError, "You must create filepath property #{property}" +
62
+ ' virtual creator'
63
+ end
64
+ Filepath.define_property(self, property, number)
65
+ nil
66
+ end
67
+ end
68
+
69
+ class << self
70
+ # Define class variables and events in +klass+. It should be call once on
71
+ # same class after +entry_in+ or +list_in+ call.
72
+ def install(klass)
73
+ klass.filepath_properties = { }
74
+
75
+ path = Regexp.escape(klass.path).gsub(/\\\*\\\*(\/|$)/, '(.*)').
76
+ gsub('\\*', '([^/]+)')
77
+ klass.filepath_regexp = Regexp.new(path)
78
+
79
+ klass.class_eval do
80
+ attr_accessor :filepath_data
81
+ end
82
+
83
+ klass.after :load do |result, entry|
84
+ if entry.path
85
+ data = klass.filepath_regexp.match(entry.path)
86
+ entry.filepath_data = { }
87
+ klass.filepath_properties.each_pair do |number, name|
88
+ entry.filepath_data[name] = data[number]
89
+ end
90
+ else
91
+ entry.filepath_data = { }
92
+ klass.filepath_properties.each_value do |name|
93
+ entry.filepath_data[name] = entry.data[name]
94
+ entry.data.delete(name)
95
+ end
96
+ end
97
+ result
98
+ end
99
+
100
+ klass.after :path do |path, matchers|
101
+ i = 0
102
+ path.gsub /(\*\*(\/|$)|\*)/ do |pattern|
103
+ i += 1
104
+ property = klass.filepath_properties[i]
105
+ unless matchers[property].is_a? Regexp or matchers[property].nil?
106
+ matchers[property]
107
+ else
108
+ pattern
109
+ end
110
+ end
111
+ end
112
+
113
+ klass.before :save do |entry|
114
+ unless entry.file
115
+ path = klass.path(entry.filepath_data)
116
+ entry.file = path unless path =~ /[\*\[\?\{]/
117
+ end
118
+ end
119
+ end
120
+
121
+ # Define in +klass+ filepath property with +name+ for +number+ <tt>*</tt>
122
+ # or <tt>**</tt> pattern in path.
123
+ def define_property(klass, name, number)
124
+ unless klass.filepath_properties
125
+ install(klass)
126
+ end
127
+
128
+ klass.filepath_properties[number] = name
129
+
130
+ klass.class_eval <<-EOS, __FILE__, __LINE__
131
+ def #{name}
132
+ @filepath_data[:#{name}]
133
+ end
134
+ def #{name}=(value)
135
+ @filepath_data[:#{name}] = value
136
+ end
137
+ EOS
138
+ end
139
+ end
140
+ end
141
+ end
@@ -26,31 +26,55 @@ module PlainRecord
26
26
  dir = Pathname(__FILE__).dirname.expand_path + 'model'
27
27
  autoload :Entry, (dir + 'entry').to_s
28
28
  autoload :List, (dir + 'list').to_s
29
-
30
- # Properties names.
31
- attr_reader :properties
32
-
29
+
30
+ include PlainRecord::Callbacks
31
+ include PlainRecord::Filepath
32
+ include PlainRecord::Associations
33
+
34
+ # YAML properties names.
35
+ attr_accessor :properties
36
+
33
37
  # Name of special properties with big text.
34
- attr_reader :texts
35
-
38
+ attr_accessor :texts
39
+
40
+ # Properties names with dynamic value.
41
+ attr_accessor :virtuals
42
+
36
43
  # Storage type: +:entry+ or +:list+.
37
44
  attr_reader :storage
38
-
45
+
39
46
  # Content of already loaded files.
40
- attr_reader :loaded
41
-
47
+ attr_accessor :loaded
48
+
49
+ def self.extended(base) #:nodoc:
50
+ base.properties = []
51
+ base.virtuals = []
52
+ base.texts = []
53
+ base.loaded = { }
54
+ end
55
+
42
56
  # Load and return all entries in +file+.
43
57
  #
44
58
  # See method code in <tt>Model::Entry</tt> or <tt>Model::List</tt>.
45
59
  def load_file(file); end
46
-
47
- # Call block on all entry. Unlike <tt>all.each</tt> it use lazy file
48
- # loading, so it is useful if you planing to break this loop somewhere in
49
- # the middle (for example, like +first+).
60
+
61
+ # Call block on all entry, which is may be match for +matchers+. Unlike
62
+ # <tt>all.each</tt> it use lazy file loading, so it is useful if you planing
63
+ # to break this loop somewhere in the middle (for example, like +first+).
64
+ #
65
+ # See method code in <tt>Model::Entry</tt> or <tt>Model::List</tt>.
66
+ def each_entry(matchers = { }); end
67
+
68
+ # Delete +entry+ from +file+.
69
+ #
70
+ # See method code in <tt>Model::Entry</tt> or <tt>Model::List</tt>.
71
+ def delete_entry(file, entry = nil); end
72
+
73
+ # Move +entry+ from one file to another.
50
74
  #
51
75
  # See method code in <tt>Model::Entry</tt> or <tt>Model::List</tt>.
52
- def each_entry; end
53
-
76
+ def move_entry(entry, from, to); end
77
+
54
78
  # Write all loaded entries to +file+.
55
79
  def save_file(file)
56
80
  if @loaded.has_key? file
@@ -59,7 +83,7 @@ module PlainRecord
59
83
  end
60
84
  end
61
85
  end
62
-
86
+
63
87
  # Return all entries, which is match for +matchers+ and return true on
64
88
  # +block+.
65
89
  #
@@ -69,13 +93,13 @@ module PlainRecord
69
93
  # Post.all(title: 'Post title')
70
94
  # Post.all(title: /^Post/, summary: /cool/)
71
95
  # Post.all { |post| 20 < Post.content.length }
72
- def all(matchers = {}, &block)
73
- entries = all_entries
96
+ def all(matchers = { }, &block)
97
+ entries = all_entries(matchers)
74
98
  entries.delete_if { |i| not match(i, matchers) } if matchers
75
- entries.delete_if { |i| not block.call(i) } if block_given?
99
+ entries.delete_if { |i| not block.call(i) } if block_given?
76
100
  entries
77
101
  end
78
-
102
+
79
103
  # Return first entry, which is match for +matchers+ and return true on
80
104
  # +block+.
81
105
  #
@@ -85,11 +109,13 @@ module PlainRecord
85
109
  # Post.first(title: 'Post title')
86
110
  # Post.first(title: /^Post/, summary: /cool/)
87
111
  # Post.first { |post| 2 < Post.title.length }
88
- def first(matchers = {}, &block)
112
+ def first(matchers = { }, &block)
89
113
  if matchers and block_given?
90
- each_entry { |i| return i if match(i, matchers) and block.call(i) }
114
+ each_entry(matchers) do |i|
115
+ return i if match(i, matchers) and block.call(i)
116
+ end
91
117
  elsif matchers
92
- each_entry { |i| return i if match(i, matchers) }
118
+ each_entry(matchers) { |i| return i if match(i, matchers) }
93
119
  elsif block_given?
94
120
  each_entry { |i| return i if block.call(i) }
95
121
  else
@@ -97,24 +123,45 @@ module PlainRecord
97
123
  end
98
124
  nil
99
125
  end
100
-
101
- # Return all model files.
102
- def files
103
- Dir.glob(File.join(PlainRecord.root, @path))
126
+
127
+ # Return all file list for models, which match for +matchers+.
128
+ def files(matchers = { })
129
+ Dir.glob(PlainRecord.root(path(matchers)))
130
+ end
131
+
132
+ # Return glob pattern to for files with entris, which is may be match for
133
+ # +matchers+.
134
+ def path(matchers = { })
135
+ use_callbacks(:path, matchers) do
136
+ @path
137
+ end
104
138
  end
105
-
139
+
106
140
  private
107
-
108
- # Return all model entries.
141
+
142
+ # Return all model entries, which is may be match for +matchers+.
109
143
  #
110
144
  # See method code in <tt>Model::Entry</tt> or <tt>Model::List</tt>.
111
- def all_entries; end
112
-
145
+ def all_entries(matchers); end
146
+
113
147
  # Return string representation of +entries+ to write it to file.
114
148
  #
115
149
  # See method code in <tt>Model::Entry</tt> or <tt>Model::List</tt>.
116
150
  def entries_string(entries); end
117
-
151
+
152
+ # Delete file, cache and empty dirs in path.
153
+ def delete_file(file)
154
+ File.delete(file)
155
+ @loaded.delete(file)
156
+
157
+ path = Pathname(file).dirname
158
+ root = Pathname(PlainRecord.root)
159
+ until 2 != path.entries.length or path == root
160
+ path.rmdir
161
+ path = path.parent
162
+ end
163
+ end
164
+
118
165
  # Match +object+ by +matchers+ to use in +all+ and +first+ methods.
119
166
  def match(object, matchers)
120
167
  matchers.each_pair do |key, matcher|
@@ -127,20 +174,19 @@ module PlainRecord
127
174
  end
128
175
  true
129
176
  end
130
-
177
+
131
178
  # Set glob +pattern+ for files with entry. Each file must contain one entry.
132
179
  # To set root for this path use +PlainRecord.root+.
133
180
  #
134
181
  # Also add methods from <tt>Model::Entry</tt>.
135
182
  #
136
- # entry_in 'content/*/post.m'
183
+ # entry_in 'content/*/post.md'
137
184
  def entry_in(path)
138
185
  @storage = :entry
139
186
  @path = path
140
187
  self.extend PlainRecord::Model::Entry
141
- @loaded = {}
142
188
  end
143
-
189
+
144
190
  # Set glob +pattern+ for files with list of entries. Each file may contain
145
191
  # several entries, but you may have several files. All data will storage
146
192
  # in YAML, so you can’t define +text+.
@@ -152,29 +198,55 @@ module PlainRecord
152
198
  @storage = :list
153
199
  @path = path
154
200
  self.extend PlainRecord::Model::List
155
- @loaded = {}
156
201
  end
157
-
158
- # Add property to model with some +name+. It will be stored as YAML.
202
+
203
+ # Add virtual property with some +name+ to model. It value willn’t be in
204
+ # file and will be calculated dynamically.
205
+ #
206
+ # You _must_ provide your own define logic by +definers+. Definer Proc
207
+ # will be call with property name in first argument and may return
208
+ # +:accessor+, +:writer+ or +:reader+ this method create standard methods
209
+ # to access to property.
210
+ #
211
+ # class Post
212
+ # include PlainRecord::Resource
213
+ #
214
+ # entry_in 'posts/*/post.md'
215
+ #
216
+ # virtual :name, in_filepath(1)
217
+ # end
218
+ def virtual(name, *definers)
219
+ @virtuals ||= []
220
+ @virtuals << name
221
+
222
+ accessors = call_definers(definers, name, :virtual)
223
+
224
+ if accessors[:reader] or accessors[:writer]
225
+ raise ArgumentError, 'You must provide you own accessors for virtual ' +
226
+ "property #{name}"
227
+ end
228
+ end
229
+
230
+ # Add property with some +name+ to model. It will be stored as YAML.
159
231
  #
160
232
  # You can provide your own define logic by +definers+. Definer Proc
161
233
  # will be call with property name in first argument and may return
162
234
  # +:accessor+, +:writer+ or +:reader+ this method create standard methods
163
235
  # to access to property.
164
- #
236
+ #
165
237
  # class Post
166
238
  # include PlainRecord::Resource
167
239
  #
168
- # entry_in 'posts/*/post.m'
240
+ # entry_in 'posts/*/post.md'
169
241
  #
170
242
  # property :title
171
243
  # end
172
244
  def property(name, *definers)
173
245
  @properties ||= []
174
246
  @properties << name
175
-
176
- accessors = call_definers(definers, name)
177
-
247
+
248
+ accessors = call_definers(definers, name, :property)
249
+
178
250
  if accessors[:reader]
179
251
  class_eval <<-EOS, __FILE__, __LINE__
180
252
  def #{name}
@@ -190,7 +262,7 @@ module PlainRecord
190
262
  EOS
191
263
  end
192
264
  end
193
-
265
+
194
266
  # Add special property with big text (for example, blog entry content). It
195
267
  # will stored after 3 dashes (<tt>---</tt>).
196
268
  #
@@ -205,19 +277,19 @@ module PlainRecord
205
277
  # == Example
206
278
  #
207
279
  # Model:
208
- #
280
+ #
209
281
  # class Post
210
282
  # include PlainRecord::Resource
211
283
  #
212
- # entry_in 'posts/*/post.m'
284
+ # entry_in 'posts/*/post.md'
213
285
  #
214
286
  # property :title
215
287
  # text :summary
216
288
  # text :content
217
289
  # end
218
- #
290
+ #
219
291
  # File:
220
- #
292
+ #
221
293
  # title: Post title
222
294
  # ---
223
295
  # Post summary
@@ -227,13 +299,13 @@ module PlainRecord
227
299
  if :list == @storage
228
300
  raise ArgumentError, 'Text is supported by only entry_in models'
229
301
  end
230
-
302
+
231
303
  @texts ||= []
232
304
  @texts << name
233
305
  number = @texts.length - 1
234
-
235
- accessors = call_definers(definers, name)
236
-
306
+
307
+ accessors = call_definers(definers, name, :text)
308
+
237
309
  if accessors[:reader]
238
310
  class_eval <<-EOS, __FILE__, __LINE__
239
311
  def #{name}
@@ -249,14 +321,15 @@ module PlainRecord
249
321
  EOS
250
322
  end
251
323
  end
252
-
253
- # Call +definers+ for property with +name+ and return accessors, which will
324
+
325
+ # Call +definers+ from +caller+ (<tt>:virtual</tt>, <tt>:property</tt> or
326
+ # <tt>:text</tt>) for property with +name+ and return accessors, which will
254
327
  # be created as standart by +property+ or +text+ method.
255
- def call_definers(definers, name)
256
- accessors = {:reader => true, :writer => true}
257
-
328
+ def call_definers(definers, name, caller)
329
+ accessors = { :reader => true, :writer => true }
330
+
258
331
  definers.each do |definer|
259
- access = definer.call(name)
332
+ access = definer.call(name, caller)
260
333
  if :writer == access or access.nil?
261
334
  accessors[:reader] = false
262
335
  end
@@ -264,7 +337,7 @@ module PlainRecord
264
337
  accessors[:writer] = false
265
338
  end
266
339
  end
267
-
340
+
268
341
  accessors
269
342
  end
270
343
  end