plain_record 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ *.DS_Store
2
+ *~
3
+
4
+ .yardoc/
5
+ doc/
6
+ pkg/
7
+
8
+ .bundle
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --format documentation --colour
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - ruby-head
5
+ - jruby-18mode
6
+ - rbx-18mode
data/.yardopts ADDED
@@ -0,0 +1,4 @@
1
+ --charset utf-8
2
+ -
3
+ README.md
4
+ ChangeLog
data/ChangeLog ADDED
@@ -0,0 +1,9 @@
1
+ == 0.2 (Smallpox)
2
+ * Add associations.
3
+ * Add special syntax for virtual properties.
4
+ * Add properties from entry file name.
5
+ * Add before/after callbacks.
6
+ * Add support for models with all entries in one file.
7
+
8
+ == 0.1 (Plague)
9
+ * Initial release
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source :rubygems
2
+
3
+ gem 'rake'
4
+ gem 'yard'
5
+ gem 'rspec'
6
+ gem 'redcarpet'
data/Gemfile.lock ADDED
@@ -0,0 +1,24 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ diff-lcs (1.1.3)
5
+ rake (0.9.2.2)
6
+ redcarpet (2.1.1)
7
+ rspec (2.10.0)
8
+ rspec-core (~> 2.10.0)
9
+ rspec-expectations (~> 2.10.0)
10
+ rspec-mocks (~> 2.10.0)
11
+ rspec-core (2.10.0)
12
+ rspec-expectations (2.10.0)
13
+ diff-lcs (~> 1.1.3)
14
+ rspec-mocks (2.10.1)
15
+ yard (0.8.1)
16
+
17
+ PLATFORMS
18
+ ruby
19
+
20
+ DEPENDENCIES
21
+ rake
22
+ redcarpet
23
+ rspec
24
+ yard
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- GNU LESSER GENERAL PUBLIC LICENSE
1
+ GNU LESSER GENERAL PUBLIC LICENSE
2
2
  Version 3, 29 June 2007
3
3
 
4
4
  Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
@@ -10,7 +10,7 @@
10
10
  the terms and conditions of version 3 of the GNU General Public
11
11
  License, supplemented by the additional permissions listed below.
12
12
 
13
- 0. Additional Definitions.
13
+ 0. Additional Definitions.
14
14
 
15
15
  As used herein, "this License" refers to version 3 of the GNU Lesser
16
16
  General Public License, and the "GNU GPL" refers to version 3 of the GNU
@@ -111,7 +111,7 @@ the following:
111
111
  a copy of the Library already present on the user's computer
112
112
  system, and (b) will operate properly with a modified version
113
113
  of the Library that is interface-compatible with the Linked
114
- Version.
114
+ Version.
115
115
 
116
116
  e) Provide Installation Information, but only if you would otherwise
117
117
  be required to provide such information under section 6 of the
@@ -163,4 +163,3 @@ whether future versions of the GNU Lesser General Public License shall
163
163
  apply, that proxy's public statement of acceptance of any version is
164
164
  permanent authorization for you to choose that version for the
165
165
  Library.
166
-
data/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # Plain Record
2
+
3
+ Plaint Record is a data persistence, which use human editable and readable plain
4
+ text files. It’s ideal for static generated sites, like blog or homepage.
5
+
6
+ If you want to write another static website generator, you don’t need to write
7
+ another file parser – you can use Plain Record.
8
+
9
+ ## How To
10
+
11
+ For example we will create simple blog storage with posts and comments.
12
+
13
+ 1. Add Plain Record to your application Gemfile:
14
+
15
+ ```ruby
16
+ gem "plain_record"
17
+ ```
18
+
19
+ 2. Set storage root – dir, which will contain all data files:
20
+
21
+ ```ruby
22
+ PlainRecord.root = 'data/'
23
+ ```
24
+
25
+ 3. Create Post class, include `Plain::Resource` module, set glob pattern
26
+ to posts files and define properties:
27
+
28
+ ```ruby
29
+ class Post
30
+ include Plain::Resource
31
+
32
+ entry_in '*/post.md'
33
+
34
+ virtual :name, in_filepath(1)
35
+ virtual :comments, many(Comment)
36
+ property :title
37
+ property :tags
38
+ text :summary
39
+ text :content
40
+ end
41
+ ```
42
+
43
+ 4. Create new post file `data/first/post.md`. Properties will be saved as
44
+ YAML and text will be placed as plain text, which is separated by 3 dashes:
45
+
46
+ ```
47
+ title: My first post
48
+ tags: test, first
49
+ ---
50
+ It is short post summary.
51
+ ---
52
+ And this is big big post text.
53
+ In several lines.
54
+ ```
55
+
56
+ 5. Also you can use files with list of entries. For example, comments:
57
+
58
+ ```ruby
59
+ class Comment
60
+ include Plain::Resource
61
+
62
+ list_in '*/comments.yml'
63
+
64
+ virtual :post_name, in_filepath(1)
65
+ virtual :post, one(Post)
66
+ property :author
67
+ property :comment
68
+ end
69
+ ```
70
+ You can’t use text fields in list files.
71
+ 6. List files is a just YAML array. For example, `data/first/comments.yml`:
72
+
73
+ <pre><code>\- author: Anonymous
74
+ comment: I like it!
75
+ \- author: Friend
76
+ comment: You first post it shit.</pre></code>
77
+
78
+ 7. Get all post:
79
+
80
+ ```ruby
81
+ Post.all # will return array with our first post
82
+ ```
83
+
84
+ 8. Get specify enrties:
85
+
86
+ ```ruby
87
+ Comment.all(author: 'Anonymous')
88
+ Post.all(title: /first/)
89
+ Post.all { |i| i.tags.length == 2 }
90
+ ```
91
+
92
+ 9. To get one entry use `first` method, which also can take matchers. You can
93
+ access for properties and text by methods with same name:
94
+
95
+ ```ruby
96
+ post = Post.first(title: /first/)
97
+ post.file #=> "data/first/post.md"
98
+ post.name #=> "first"
99
+ post.title #=> "My first post"
100
+ post.tags #=> ["test", "first"]
101
+ post.summary #=> "It is short post summary."
102
+ ```
103
+
104
+ 10. You can also change and save entries:
105
+
106
+ ```ruby
107
+ post.title = 'First post'
108
+ post.save
109
+ ```
110
+
111
+ 11. And delete it (with empty dirs in it file path):
112
+
113
+ ```ruby
114
+ post.destroy
115
+ ```
116
+
117
+ ## License
118
+
119
+ Plain Record is licensed under the GNU Lesser General Public License version 3.
120
+ See the LICENSE file or http://www.gnu.org/licenses/lgpl.html.
121
+
122
+ ## Author
123
+
124
+ Andrey “A.I.” Sitnik <andrey@sitnik.ru>
data/Rakefile CHANGED
@@ -1,57 +1,34 @@
1
1
  # encoding: utf-8
2
- gem 'rspec'
3
- require 'spec/rake/spectask'
4
- require 'rake/rdoctask'
5
- require 'rake/gempackagetask'
6
-
7
- Spec::Rake::SpecTask.new('spec') do |t|
8
- t.spec_opts = ['--format', 'specdoc', '--colour']
9
- t.spec_files = Dir['spec/**/*_spec.rb'].sort
10
- end
11
2
 
12
- Rake::RDocTask.new do |rdoc|
13
- rdoc.main = 'README.rdoc'
14
- rdoc.rdoc_files.include('*.rdoc', 'lib/**/*.rb')
15
- rdoc.title = 'Plain Record'
16
- rdoc.rdoc_dir = 'doc'
17
- rdoc.options << '--charset=utf-8'
18
- rdoc.options << '--all'
19
- rdoc.options << '--inline-source'
3
+ require 'rubygems'
4
+
5
+ begin
6
+ require 'bundler/setup'
7
+ Bundler::GemHelper.install_tasks
8
+ rescue LoadError
9
+ puts "Bundler not available. Install it with: gem install bundler"
20
10
  end
21
11
 
22
- require File.join(File.dirname(__FILE__), 'lib', 'plain_record', 'version')
23
-
24
- spec = Gem::Specification.new do |s|
25
- s.platform = Gem::Platform::RUBY
26
- s.name = 'plain_record'
27
- s.version = PlainRecord::VERSION
28
- s.summary = 'Data persistence, which use human editable and ' +
29
- 'readable plain text files.'
30
- s.description = <<-DESC
31
- Plain Record is a data persistence, which use human editable and readable
32
- plain text files. It’s ideal for static generated sites, like blog or
33
- homepage.
34
- DESC
35
-
36
- s.files = FileList[
37
- 'lib/**/*',
38
- 'spec/**/*',
39
- 'Rakefile',
40
- 'LICENSE',
41
- 'README.rdoc']
42
- s.test_files = FileList['spec/**/*']
43
- s.extra_rdoc_files = ['README.rdoc', 'LICENSE']
44
- s.require_path = 'lib'
45
- s.has_rdoc = true
46
- s.rdoc_options << '--title "Plain Record"' << '--main README.rdoc' <<
47
- '--charset=utf-8' << '--all' << '--inline-source'
48
-
49
- s.author = 'Andrey "A.I." Sitnik'
50
- s.email = 'andrey@sitnik.ru'
51
- s.homepage = 'http://github.com/ai/plain_record'
52
- s.rubyforge_project = 'plainrecord'
12
+ require File.join(File.dirname(__FILE__), 'lib/plain_record/version')
13
+
14
+ require 'rspec/core/rake_task'
15
+
16
+ RSpec::Core::RakeTask.new
17
+
18
+ require 'yard'
19
+ YARD::Rake::YardocTask.new do |yard|
20
+ yard.options << "--title='Plain Record #{PlainRecord::VERSION}'"
53
21
  end
54
22
 
55
- Rake::GemPackageTask.new(spec) do |pkg|
56
- pkg.gem_spec = spec
23
+ task :clobber_doc do
24
+ rm_r 'doc' rescue nil
25
+ rm_r '.yardoc' rescue nil
26
+ end
27
+ task :clobber_package do
28
+ rm_r 'pkg' rescue nil
57
29
  end
30
+
31
+ desc 'Delete all generated files'
32
+ task :clobber => [:clobber_package, :clobber_doc]
33
+
34
+ task :default => :spec
@@ -0,0 +1,59 @@
1
+ =begin
2
+ Storage for one-to-many virtual associations.
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
+ # Storage for one-to-many virtual associations. When new object will pushed,
22
+ # proxy change it mapped properties.
23
+ class AssociationProxy < Array
24
+ # Model with association.
25
+ attr_accessor :owner
26
+
27
+ # Associations property name.
28
+ attr_accessor :property
29
+
30
+ # Create proxy for one-to-many virtual associations +property+ in +owner+
31
+ # and put +array+ into it.
32
+ def self.link(array, owner, property)
33
+ proxy = new(array, owner, property)
34
+ proxy.each { |i| proxy.link(i) }
35
+ proxy
36
+ end
37
+
38
+ # Create proxy for one-to-many virtual associations +property+ in +owner+
39
+ # with +array+ in value.
40
+ def initialize(array, owner, property)
41
+ @owner = owner
42
+ @property = property
43
+ super(array)
44
+ end
45
+
46
+ # Push new item in association and change it property by association map.
47
+ def <<(obj)
48
+ link(obj)
49
+ super(obj)
50
+ end
51
+
52
+ # Change properties in +obj+ by association map.
53
+ def link(obj)
54
+ @owner.class.association_maps[@property].each do |from, to|
55
+ obj.send(from.to_s + '=', @owner.send(to))
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,271 @@
1
+ =begin
2
+ Extention to store or have link to another model.
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 for model to store or have link to another model. There is two
22
+ # type of association.
23
+ #
24
+ # == Virtual property
25
+ # In +virtual+ method this definer create only _link_ to another model. When
26
+ # you try to use this virtual property, model will find association object by
27
+ # rules in +map+.
28
+ #
29
+ # Rules in +map+ is only Hash with model properties in key and association
30
+ # properties in value. For example, if model contain +name+ property and
31
+ # association must have +post_name+ with same value, +map+ will be
32
+ # <tt>{ :name => :post_name }</tt>.
33
+ #
34
+ # If you didn’t set +map+ definer will try to find it automatically:
35
+ # it will find in model and association class all property pairs, what have
36
+ # name like +property+ → <tt>model</tt>_<tt>property</tt>. For example,
37
+ # if model +Post+ have property +name+ and +Comment+ have +post_name+, you
38
+ # may not set +map+ – definer will find it automatically.
39
+ #
40
+ # class Review
41
+ # include PlainRecord::Resource
42
+ #
43
+ # entry_in 'reviews/*.md'
44
+ #
45
+ # virtual :author, one(Author)
46
+ # property :author_login
47
+ # text :review
48
+ # end
49
+ #
50
+ # class Author
51
+ # include PlainRecord::Resource
52
+ #
53
+ # list_in 'authors.yml'
54
+ #
55
+ # virtual :reviews, many(Review)
56
+ # property :login
57
+ # property :name
58
+ # end
59
+ #
60
+ # == Real property
61
+ # If you will use this definer in +property+ method, association object data
62
+ # will store in you model file. For example model:
63
+ #
64
+ # class Movie
65
+ # include PlainRecord::Resource
66
+ #
67
+ # property :title
68
+ # property :genre
69
+ # property :release_year
70
+ # end
71
+ #
72
+ # class Tag
73
+ # include PlainRecord::Resource
74
+ # property :name
75
+ # end
76
+ #
77
+ # class Review
78
+ # include PlainRecord::Resource
79
+ #
80
+ # entry_in 'reviews/*.md'
81
+ #
82
+ # property :author
83
+ # property :movie, one(Movie)
84
+ # property :tags, many(Tag)
85
+ # text :review
86
+ # end
87
+ #
88
+ # will be store as:
89
+ #
90
+ # author: John Smith
91
+ # movie:
92
+ # title: Watchmen
93
+ # genre: action
94
+ # release_year: 2009
95
+ # tags:
96
+ # - name: Great movies
97
+ # - name: Comics
98
+ # ---
99
+ # Movie is great!
100
+ module Associations
101
+ # Hash with map for virtual associations.
102
+ attr_accessor :association_maps
103
+
104
+ # Hash with cached values for virtual associations.
105
+ attr_accessor :association_cache
106
+
107
+ private
108
+
109
+ # Return definer for one-to-one association with +klass+. Have different
110
+ # logic in +property+ and +virtual+ methods.
111
+ def one(klass, map = { })
112
+ proc do |property, caller|
113
+ if :property == caller
114
+ Associations.define_real_one(self, property, klass)
115
+ :accessor
116
+ elsif :virtual == caller
117
+ map = Associations.map(self, klass, "#{property}_") if map.empty?
118
+ Associations.define_link_one(self, klass, property, map)
119
+ nil
120
+ else
121
+ raise ArgumentError, "You couldn't create association property" +
122
+ " #{property} by text creator"
123
+ end
124
+ end
125
+ end
126
+
127
+ # Return definer for one-to-many or many-to-many association with +klass+.
128
+ # Have different login in +property+ and +virtual+ methods.
129
+ def many(klass, prefix = nil, map = { })
130
+ proc do |property, caller|
131
+ if :property == caller
132
+ Associations.define_real_many(self, property, klass)
133
+ :accessor
134
+ elsif :virtual == caller
135
+ unless prefix
136
+ prefix = self.to_s.gsub!(/[A-Z]/, '_\0')[1..-1].downcase + '_'
137
+ end
138
+ map = Associations.map(klass, self, prefix) if map.empty?
139
+ Associations.define_link_many(self, klass, property, map)
140
+ nil
141
+ else
142
+ raise ArgumentError, "You couldn't create association property" +
143
+ " #{property} by text creator"
144
+ end
145
+ end
146
+ end
147
+
148
+ class << self
149
+ # Define, that +property+ in +klass+ contain in file data from +model+.
150
+ def define_real_one(klass, property, model)
151
+ name = property.to_s
152
+ klass.after :load do |result, entry|
153
+ entry.data[name] = model.new(entry.file, entry.data[name])
154
+ result
155
+ end
156
+ klass.before :save do |entry|
157
+ model.call_before_callbacks(:save, [entry.data[name]])
158
+ entry.data[name] = entry.data[name]
159
+ end
160
+ klass.after :save do |result, entry|
161
+ entry.data[name] = model.new(entry.file, entry.data[name])
162
+ model.call_after_callbacks(:save, nil, [entry.data[name]])
163
+ result
164
+ end
165
+ end
166
+
167
+ # Define, that +property+ in +klass+ contain in file array of data from
168
+ # +model+ objects.
169
+ def define_real_many(klass, property, model)
170
+ name = property.to_s
171
+ klass.after :load do |result, entry|
172
+ if entry.data[name].is_a? Enumerable
173
+ entry.data[name].map! { |i| model.new(entry.file, i) }
174
+ else
175
+ entry.data[name] = []
176
+ end
177
+ result
178
+ end
179
+ klass.before :save do |entry|
180
+ if entry.data[name].empty?
181
+ entry.data.delete(name)
182
+ else
183
+ entry.data[name].map! do |obj|
184
+ model.call_before_callbacks(:save, [obj])
185
+ obj.data
186
+ end
187
+ end
188
+ end
189
+ klass.after :save do |result, entry|
190
+ entry.data[name].map! { |i| model.new(entry.file, i) }
191
+ entry.data[name].each do |i|
192
+ model.call_after_callbacks(:save, nil, [i])
193
+ end
194
+ result
195
+ end
196
+ end
197
+
198
+ # Find properties pairs in +from+ and +to+ models, witch is like
199
+ # <tt>prefix</tt>_<tt>from</tt> → +to+.
200
+ #
201
+ # For example, if Comment contain +post_name+ property and Post contain
202
+ # +name+:
203
+ #
204
+ # Associations.map(Comment, Post, :post) #=> { :post_name => :name }
205
+ def map(from, to, prefix)
206
+ from_fields = (from.properties + from.virtuals).map { |i| i.to_s }
207
+ mapped = { }
208
+ (to.properties + to.virtuals).each do |to_field|
209
+ from_field = prefix + to_field.to_s
210
+ if from_fields.include? from_field
211
+ mapped[from_field.to_sym] = to_field
212
+ end
213
+ end
214
+ mapped
215
+ end
216
+
217
+ # Define that virtual property +name+ in +klass+ contain link to +model+
218
+ # witch is finded by +map+.
219
+ def define_link_one(klass, model, name, map)
220
+ klass.association_cache ||= { }
221
+ klass.association_maps ||= { }
222
+ klass.association_maps[name] = map
223
+
224
+ klass.class_eval <<-EOS, __FILE__, __LINE__
225
+ def #{name}
226
+ unless self.class.association_cache[:#{name}]
227
+ search = Hash[
228
+ self.class.association_maps[:#{name}].map do |from, to|
229
+ [to, send(from)]
230
+ end]
231
+ self.class.association_cache[:#{name}] = #{model}.first(search)
232
+ end
233
+ self.class.association_cache[:#{name}]
234
+ end
235
+ def #{name}=(value)
236
+ self.class.association_maps[:#{name}].each do |from, to|
237
+ value.send(to.to_s + '=', send(from))
238
+ end
239
+ self.class.association_cache[:#{name}] = value
240
+ end
241
+ EOS
242
+ end
243
+
244
+ # Define that virtual property +name+ in +klass+ contain links to +model+
245
+ # witch are finded by +map+.
246
+ def define_link_many(klass, model, name, map)
247
+ klass.association_cache ||= { }
248
+ klass.association_maps ||= { }
249
+ klass.association_maps[name] = map
250
+
251
+ klass.class_eval <<-EOS, __FILE__, __LINE__
252
+ def #{name}
253
+ unless self.class.association_cache[:#{name}]
254
+ search = Hash[
255
+ self.class.association_maps[:#{name}].map do |from, to|
256
+ [from, send(to)]
257
+ end]
258
+ self.class.association_cache[:#{name}] = AssociationProxy.new(
259
+ #{model}.all(search), self, :#{name})
260
+ end
261
+ self.class.association_cache[:#{name}]
262
+ end
263
+ def #{name}=(values)
264
+ self.class.association_cache[:#{name}] = AssociationProxy.link(
265
+ values, self, :#{name})
266
+ end
267
+ EOS
268
+ end
269
+ end
270
+ end
271
+ end