plain_record 0.1.0 → 0.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.
@@ -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