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 +8 -0
- data/.rspec +1 -0
- data/.travis.yml +6 -0
- data/.yardopts +4 -0
- data/ChangeLog +9 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +24 -0
- data/LICENSE +3 -4
- data/README.md +124 -0
- data/Rakefile +27 -50
- data/lib/plain_record/association_proxy.rb +59 -0
- data/lib/plain_record/associations.rb +271 -0
- data/lib/plain_record/callbacks.rb +146 -0
- data/lib/plain_record/filepath.rb +141 -0
- data/lib/plain_record/model/entry.rb +17 -9
- data/lib/plain_record/model/list.rb +22 -9
- data/lib/plain_record/model.rb +119 -64
- data/lib/plain_record/resource.rb +72 -19
- data/lib/plain_record/version.rb +1 -1
- data/lib/plain_record.rb +21 -2
- data/plain_record.gemspec +30 -0
- data/spec/associations_spec.rb +142 -0
- data/spec/callbacks_spec.rb +59 -0
- data/spec/data/1/comments.yml +5 -0
- data/spec/data/1/{post.m → post.md} +0 -0
- data/spec/data/2/{post.m → post.md} +1 -1
- data/spec/data/3/post.md +4 -0
- data/spec/data/authors/extern.yml +2 -2
- data/spec/data/authors/intern.yml +4 -4
- data/spec/data/best/4/post.md +1 -0
- data/spec/filepath_spec.rb +53 -0
- data/spec/model_spec.rb +90 -42
- data/spec/resource_spec.rb +70 -27
- data/spec/spec_helper.rb +33 -14
- metadata +122 -70
- data/README.rdoc +0 -96
- data/spec/data/3/post.m +0 -1
data/lib/plain_record/model.rb
CHANGED
@@ -26,36 +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
|
-
|
31
|
-
|
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
|
-
|
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
|
-
|
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
|
48
|
-
# loading, so it is useful if you planing
|
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+).
|
50
64
|
#
|
51
65
|
# See method code in <tt>Model::Entry</tt> or <tt>Model::List</tt>.
|
52
|
-
def each_entry; end
|
53
|
-
|
66
|
+
def each_entry(matchers = { }); end
|
67
|
+
|
54
68
|
# Delete +entry+ from +file+.
|
55
69
|
#
|
56
70
|
# See method code in <tt>Model::Entry</tt> or <tt>Model::List</tt>.
|
57
71
|
def delete_entry(file, entry = nil); end
|
58
|
-
|
72
|
+
|
73
|
+
# Move +entry+ from one file to another.
|
74
|
+
#
|
75
|
+
# See method code in <tt>Model::Entry</tt> or <tt>Model::List</tt>.
|
76
|
+
def move_entry(entry, from, to); end
|
77
|
+
|
59
78
|
# Write all loaded entries to +file+.
|
60
79
|
def save_file(file)
|
61
80
|
if @loaded.has_key? file
|
@@ -64,7 +83,7 @@ module PlainRecord
|
|
64
83
|
end
|
65
84
|
end
|
66
85
|
end
|
67
|
-
|
86
|
+
|
68
87
|
# Return all entries, which is match for +matchers+ and return true on
|
69
88
|
# +block+.
|
70
89
|
#
|
@@ -74,13 +93,13 @@ module PlainRecord
|
|
74
93
|
# Post.all(title: 'Post title')
|
75
94
|
# Post.all(title: /^Post/, summary: /cool/)
|
76
95
|
# Post.all { |post| 20 < Post.content.length }
|
77
|
-
def all(matchers = {}, &block)
|
78
|
-
entries = all_entries
|
96
|
+
def all(matchers = { }, &block)
|
97
|
+
entries = all_entries(matchers)
|
79
98
|
entries.delete_if { |i| not match(i, matchers) } if matchers
|
80
|
-
entries.delete_if { |i| not block.call(i) }
|
99
|
+
entries.delete_if { |i| not block.call(i) } if block_given?
|
81
100
|
entries
|
82
101
|
end
|
83
|
-
|
102
|
+
|
84
103
|
# Return first entry, which is match for +matchers+ and return true on
|
85
104
|
# +block+.
|
86
105
|
#
|
@@ -90,11 +109,13 @@ module PlainRecord
|
|
90
109
|
# Post.first(title: 'Post title')
|
91
110
|
# Post.first(title: /^Post/, summary: /cool/)
|
92
111
|
# Post.first { |post| 2 < Post.title.length }
|
93
|
-
def first(matchers = {}, &block)
|
112
|
+
def first(matchers = { }, &block)
|
94
113
|
if matchers and block_given?
|
95
|
-
each_entry
|
114
|
+
each_entry(matchers) do |i|
|
115
|
+
return i if match(i, matchers) and block.call(i)
|
116
|
+
end
|
96
117
|
elsif matchers
|
97
|
-
each_entry { |i| return i if match(i, matchers) }
|
118
|
+
each_entry(matchers) { |i| return i if match(i, matchers) }
|
98
119
|
elsif block_given?
|
99
120
|
each_entry { |i| return i if block.call(i) }
|
100
121
|
else
|
@@ -102,29 +123,37 @@ module PlainRecord
|
|
102
123
|
end
|
103
124
|
nil
|
104
125
|
end
|
105
|
-
|
106
|
-
# Return all
|
107
|
-
def files
|
108
|
-
Dir.glob(
|
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
|
109
138
|
end
|
110
|
-
|
139
|
+
|
111
140
|
private
|
112
|
-
|
113
|
-
# Return all model entries
|
141
|
+
|
142
|
+
# Return all model entries, which is may be match for +matchers+.
|
114
143
|
#
|
115
144
|
# See method code in <tt>Model::Entry</tt> or <tt>Model::List</tt>.
|
116
|
-
def all_entries; end
|
117
|
-
|
145
|
+
def all_entries(matchers); end
|
146
|
+
|
118
147
|
# Return string representation of +entries+ to write it to file.
|
119
148
|
#
|
120
149
|
# See method code in <tt>Model::Entry</tt> or <tt>Model::List</tt>.
|
121
150
|
def entries_string(entries); end
|
122
|
-
|
151
|
+
|
123
152
|
# Delete file, cache and empty dirs in path.
|
124
153
|
def delete_file(file)
|
125
154
|
File.delete(file)
|
126
155
|
@loaded.delete(file)
|
127
|
-
|
156
|
+
|
128
157
|
path = Pathname(file).dirname
|
129
158
|
root = Pathname(PlainRecord.root)
|
130
159
|
until 2 != path.entries.length or path == root
|
@@ -132,7 +161,7 @@ module PlainRecord
|
|
132
161
|
path = path.parent
|
133
162
|
end
|
134
163
|
end
|
135
|
-
|
164
|
+
|
136
165
|
# Match +object+ by +matchers+ to use in +all+ and +first+ methods.
|
137
166
|
def match(object, matchers)
|
138
167
|
matchers.each_pair do |key, matcher|
|
@@ -145,20 +174,19 @@ module PlainRecord
|
|
145
174
|
end
|
146
175
|
true
|
147
176
|
end
|
148
|
-
|
177
|
+
|
149
178
|
# Set glob +pattern+ for files with entry. Each file must contain one entry.
|
150
179
|
# To set root for this path use +PlainRecord.root+.
|
151
180
|
#
|
152
181
|
# Also add methods from <tt>Model::Entry</tt>.
|
153
182
|
#
|
154
|
-
# entry_in 'content/*/post.
|
183
|
+
# entry_in 'content/*/post.md'
|
155
184
|
def entry_in(path)
|
156
185
|
@storage = :entry
|
157
186
|
@path = path
|
158
187
|
self.extend PlainRecord::Model::Entry
|
159
|
-
@loaded = {}
|
160
188
|
end
|
161
|
-
|
189
|
+
|
162
190
|
# Set glob +pattern+ for files with list of entries. Each file may contain
|
163
191
|
# several entries, but you may have several files. All data will storage
|
164
192
|
# in YAML, so you can’t define +text+.
|
@@ -170,29 +198,55 @@ module PlainRecord
|
|
170
198
|
@storage = :list
|
171
199
|
@path = path
|
172
200
|
self.extend PlainRecord::Model::List
|
173
|
-
@loaded = {}
|
174
201
|
end
|
175
|
-
|
176
|
-
# Add property
|
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.
|
177
231
|
#
|
178
232
|
# You can provide your own define logic by +definers+. Definer Proc
|
179
233
|
# will be call with property name in first argument and may return
|
180
234
|
# +:accessor+, +:writer+ or +:reader+ this method create standard methods
|
181
235
|
# to access to property.
|
182
|
-
#
|
236
|
+
#
|
183
237
|
# class Post
|
184
238
|
# include PlainRecord::Resource
|
185
239
|
#
|
186
|
-
# entry_in 'posts/*/post.
|
240
|
+
# entry_in 'posts/*/post.md'
|
187
241
|
#
|
188
242
|
# property :title
|
189
243
|
# end
|
190
244
|
def property(name, *definers)
|
191
245
|
@properties ||= []
|
192
246
|
@properties << name
|
193
|
-
|
194
|
-
accessors = call_definers(definers, name)
|
195
|
-
|
247
|
+
|
248
|
+
accessors = call_definers(definers, name, :property)
|
249
|
+
|
196
250
|
if accessors[:reader]
|
197
251
|
class_eval <<-EOS, __FILE__, __LINE__
|
198
252
|
def #{name}
|
@@ -208,7 +262,7 @@ module PlainRecord
|
|
208
262
|
EOS
|
209
263
|
end
|
210
264
|
end
|
211
|
-
|
265
|
+
|
212
266
|
# Add special property with big text (for example, blog entry content). It
|
213
267
|
# will stored after 3 dashes (<tt>---</tt>).
|
214
268
|
#
|
@@ -223,19 +277,19 @@ module PlainRecord
|
|
223
277
|
# == Example
|
224
278
|
#
|
225
279
|
# Model:
|
226
|
-
#
|
280
|
+
#
|
227
281
|
# class Post
|
228
282
|
# include PlainRecord::Resource
|
229
283
|
#
|
230
|
-
# entry_in 'posts/*/post.
|
284
|
+
# entry_in 'posts/*/post.md'
|
231
285
|
#
|
232
286
|
# property :title
|
233
287
|
# text :summary
|
234
288
|
# text :content
|
235
289
|
# end
|
236
|
-
#
|
290
|
+
#
|
237
291
|
# File:
|
238
|
-
#
|
292
|
+
#
|
239
293
|
# title: Post title
|
240
294
|
# ---
|
241
295
|
# Post summary
|
@@ -245,13 +299,13 @@ module PlainRecord
|
|
245
299
|
if :list == @storage
|
246
300
|
raise ArgumentError, 'Text is supported by only entry_in models'
|
247
301
|
end
|
248
|
-
|
302
|
+
|
249
303
|
@texts ||= []
|
250
304
|
@texts << name
|
251
305
|
number = @texts.length - 1
|
252
|
-
|
253
|
-
accessors = call_definers(definers, name)
|
254
|
-
|
306
|
+
|
307
|
+
accessors = call_definers(definers, name, :text)
|
308
|
+
|
255
309
|
if accessors[:reader]
|
256
310
|
class_eval <<-EOS, __FILE__, __LINE__
|
257
311
|
def #{name}
|
@@ -267,14 +321,15 @@ module PlainRecord
|
|
267
321
|
EOS
|
268
322
|
end
|
269
323
|
end
|
270
|
-
|
271
|
-
# Call +definers+
|
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
|
272
327
|
# be created as standart by +property+ or +text+ method.
|
273
|
-
def call_definers(definers, name)
|
274
|
-
accessors = {:reader => true, :writer => true}
|
275
|
-
|
328
|
+
def call_definers(definers, name, caller)
|
329
|
+
accessors = { :reader => true, :writer => true }
|
330
|
+
|
276
331
|
definers.each do |definer|
|
277
|
-
access = definer.call(name)
|
332
|
+
access = definer.call(name, caller)
|
278
333
|
if :writer == access or access.nil?
|
279
334
|
accessors[:reader] = false
|
280
335
|
end
|
@@ -282,7 +337,7 @@ module PlainRecord
|
|
282
337
|
accessors[:writer] = false
|
283
338
|
end
|
284
339
|
end
|
285
|
-
|
340
|
+
|
286
341
|
accessors
|
287
342
|
end
|
288
343
|
end
|
@@ -21,11 +21,27 @@ module PlainRecord
|
|
21
21
|
# Module to be included into model class. Contain instance methods. See
|
22
22
|
# Model for class methods.
|
23
23
|
#
|
24
|
+
# You can set your callbacks before and after some methods/events:
|
25
|
+
# * <tt>path(matchers)</tt> – return file names for model which is match for
|
26
|
+
# matchers;
|
27
|
+
# * <tt>load(enrty)</tt> – load or create new entry;
|
28
|
+
# * <tt>destroy(entry)</tt> – delete entry;
|
29
|
+
# * <tt>save(entry)</tt> – write entry to file.
|
30
|
+
# See PlainRecord::Callbacks for details.
|
31
|
+
#
|
32
|
+
# You can define properties from entry file path, by +in_filepath+ definer.
|
33
|
+
# See PlainRecord::Filepath for details.
|
34
|
+
#
|
24
35
|
# class Post
|
25
36
|
# include PlainRecord::Resource
|
26
|
-
#
|
27
|
-
# entry_in '
|
28
|
-
#
|
37
|
+
#
|
38
|
+
# entry_in 'content/*/post.md'
|
39
|
+
#
|
40
|
+
# before :save do |enrty|
|
41
|
+
# entry.title = Time.now.to.s unless entry.title
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# virtual :name, in_filepath(1)
|
29
45
|
# property :title
|
30
46
|
# text :summary
|
31
47
|
# text :content
|
@@ -36,39 +52,76 @@ module PlainRecord
|
|
36
52
|
base.send :extend, Model
|
37
53
|
end
|
38
54
|
end
|
39
|
-
|
55
|
+
|
40
56
|
# Properties values.
|
41
57
|
attr_reader :data
|
42
|
-
|
58
|
+
|
43
59
|
# Texts values.
|
44
60
|
attr_reader :texts
|
45
|
-
|
61
|
+
|
46
62
|
# File, where this object is stored.
|
47
|
-
|
48
|
-
|
63
|
+
attr_accessor :file
|
64
|
+
|
49
65
|
# Create new model instance with YAML +data+ and +texts+ from +file+.
|
50
|
-
def initialize(file, data, texts = [])
|
51
|
-
|
52
|
-
|
53
|
-
|
66
|
+
def initialize(file = nil, data = { }, texts = [])
|
67
|
+
self.class.use_callbacks(:load, self) do
|
68
|
+
texts, data = data, nil if data.is_a? Array
|
69
|
+
data, file = file, nil if file.is_a? Hash
|
70
|
+
|
71
|
+
@file = file
|
72
|
+
@data = data
|
73
|
+
@texts = texts
|
74
|
+
end
|
54
75
|
end
|
55
|
-
|
76
|
+
|
77
|
+
# Set path to entry storage. File should be in <tt>PlainRecord.root</tt> and
|
78
|
+
# can be relative.
|
79
|
+
def file=(value)
|
80
|
+
if PlainRecord.root != value.slice(0...PlainRecord.root.length)
|
81
|
+
value = PlainRecord.root(value)
|
82
|
+
end
|
83
|
+
|
84
|
+
if @file != value
|
85
|
+
self.class.move_entry(self, @file, value)
|
86
|
+
@file = value
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Return relative path to +file+ from <tt>PlainRecord.root</tt>.
|
91
|
+
def path
|
92
|
+
return nil unless @file
|
93
|
+
@file.slice(PlainRecord.root.length..-1)
|
94
|
+
end
|
95
|
+
|
56
96
|
# Save entry to file. Note, that for in_list models it also save all other
|
57
97
|
# entries in file.
|
58
98
|
def save
|
59
|
-
self.class.
|
99
|
+
self.class.use_callbacks(:save, self) do
|
100
|
+
unless @file
|
101
|
+
unless self.class.path =~ /[\*\[\?\{]/
|
102
|
+
self.file = self.class.path
|
103
|
+
else
|
104
|
+
raise ArgumentError, "There isn't file to save entry. " +
|
105
|
+
"Set filepath properties or file."
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
self.class.save_file(@file)
|
110
|
+
end
|
60
111
|
end
|
61
|
-
|
112
|
+
|
62
113
|
# Delete current entry and it file if there isn’t has any other entries.
|
63
114
|
def destroy
|
64
|
-
self.class.
|
115
|
+
self.class.use_callbacks(:destroy, self) do
|
116
|
+
self.class.delete_entry(@file, self)
|
117
|
+
end
|
65
118
|
end
|
66
|
-
|
119
|
+
|
67
120
|
# Return string of YAML representation of entry +data+.
|
68
|
-
def to_yaml(opts = {})
|
121
|
+
def to_yaml(opts = { })
|
69
122
|
@data.to_yaml(opts)
|
70
123
|
end
|
71
|
-
|
124
|
+
|
72
125
|
# Compare if its properties and texts are equal.
|
73
126
|
def eql?(other)
|
74
127
|
return false unless other.kind_of?(self.class)
|
data/lib/plain_record/version.rb
CHANGED
data/lib/plain_record.rb
CHANGED
@@ -20,14 +20,33 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
20
20
|
require 'pathname'
|
21
21
|
require 'yaml'
|
22
22
|
|
23
|
+
YAML::ENGINE.yamler = 'syck' if defined? YAML::ENGINE
|
24
|
+
|
23
25
|
dir = Pathname(__FILE__).dirname.expand_path + 'plain_record'
|
24
26
|
require dir + 'version'
|
27
|
+
require dir + 'callbacks'
|
28
|
+
require dir + 'filepath'
|
29
|
+
require dir + 'association_proxy'
|
30
|
+
require dir + 'associations'
|
25
31
|
require dir + 'model'
|
26
32
|
require dir + 'resource'
|
27
33
|
|
28
34
|
module PlainRecord
|
29
35
|
class << self
|
30
|
-
#
|
31
|
-
|
36
|
+
# Set new root for Model#entry_in or Model#list_in.
|
37
|
+
#
|
38
|
+
# Note, that it add last slash to root path (<tt>/content</tt> will be saved
|
39
|
+
# as <tt>/content/</tt>).
|
40
|
+
def root=(value)
|
41
|
+
value += File::SEPARATOR if File::SEPARATOR != value[-1..-1]
|
42
|
+
@root = value
|
43
|
+
end
|
44
|
+
|
45
|
+
# Return root for Model#entry_in or Model#list_in.
|
46
|
+
#
|
47
|
+
# If you set +path+ it will be added to root path.
|
48
|
+
def root(path = '')
|
49
|
+
File.join(@root, path)
|
50
|
+
end
|
32
51
|
end
|
33
52
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require './lib/plain_record/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.platform = Gem::Platform::RUBY
|
5
|
+
s.name = 'plain_record'
|
6
|
+
s.version = PlainRecord::VERSION
|
7
|
+
s.date = Time.now.strftime('%Y-%m-%d')
|
8
|
+
s.summary = 'Data persistence, which use human editable and ' +
|
9
|
+
'readable plain text files.'
|
10
|
+
s.description = <<-EOF
|
11
|
+
Plain Record is a data persistence, which use human editable and
|
12
|
+
readable plain text files. It’s ideal for static generated sites,
|
13
|
+
like blog or homepage.
|
14
|
+
EOF
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {spec}/*`.split("\n")
|
18
|
+
s.extra_rdoc_files = ['README.md', 'LICENSE', 'ChangeLog']
|
19
|
+
s.require_path = 'lib'
|
20
|
+
|
21
|
+
s.author = 'Andrey "A.I." Sitnik'
|
22
|
+
s.email = 'andrey@sitnik.ru'
|
23
|
+
s.homepage = 'https://github.com/ai/plain_record'
|
24
|
+
|
25
|
+
s.add_development_dependency "bundler", [">= 1.0.10"]
|
26
|
+
s.add_development_dependency "yard", [">= 0"]
|
27
|
+
s.add_development_dependency "rake", [">= 0"]
|
28
|
+
s.add_development_dependency "rspec", [">= 0"]
|
29
|
+
s.add_development_dependency "redcarpet", [">= 0"]
|
30
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
describe PlainRecord::Associations do
|
4
|
+
|
5
|
+
before :all do
|
6
|
+
class ::Rate
|
7
|
+
include PlainRecord::Resource
|
8
|
+
end
|
9
|
+
|
10
|
+
class ::RatedPost
|
11
|
+
include PlainRecord::Resource
|
12
|
+
entry_in 'data/3/post.md'
|
13
|
+
property :rate, one(::Rate)
|
14
|
+
end
|
15
|
+
|
16
|
+
class ::Comment
|
17
|
+
include PlainRecord::Resource
|
18
|
+
|
19
|
+
list_in 'data/*/comments.yml'
|
20
|
+
|
21
|
+
virtual :commented_post_name, in_filepath(1)
|
22
|
+
virtual :commented_post, one(::FilepathPost)
|
23
|
+
|
24
|
+
property :author_name
|
25
|
+
property :text
|
26
|
+
property :answers, many(::Comment)
|
27
|
+
end
|
28
|
+
|
29
|
+
class ::CommentedPost
|
30
|
+
include PlainRecord::Resource
|
31
|
+
entry_in 'data/*/post.md'
|
32
|
+
virtual :name, in_filepath(1)
|
33
|
+
virtual :comments, many(::Comment)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
it "shouldn't create association for text" do
|
38
|
+
lambda {
|
39
|
+
Class.new do
|
40
|
+
include PlainRecord::Resource
|
41
|
+
text :one, one(Post)
|
42
|
+
end
|
43
|
+
}.should raise_error(ArgumentError, /text creator/)
|
44
|
+
|
45
|
+
lambda {
|
46
|
+
Class.new do
|
47
|
+
include PlainRecord::Resource
|
48
|
+
text :many, many(Post)
|
49
|
+
end
|
50
|
+
}.should raise_error(ArgumentError, /text creator/)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should load one-to-one real association" do
|
54
|
+
rate = ::RatedPost.first().rate
|
55
|
+
rate.should be_instance_of(::Rate)
|
56
|
+
rate.path.should == 'data/3/post.md'
|
57
|
+
rate.data.should == { 'subject' => 5, 'text' => 2 }
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should save one-to-one real association" do
|
61
|
+
file = StringIO.new
|
62
|
+
File.should_receive(:open).with(anything(), 'w').and_yield(file)
|
63
|
+
|
64
|
+
::RatedPost.first().save()
|
65
|
+
|
66
|
+
file.should has_yaml({ 'title' => 'Third',
|
67
|
+
'rate' => { 'text' => 2, 'subject' => 5 } })
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should load one-to-many real association" do
|
71
|
+
root = ::Comment.first()
|
72
|
+
root.should have(1).answers
|
73
|
+
root.answers[0].should be_instance_of(::Comment)
|
74
|
+
root.answers[0].path.should == 'data/1/comments.yml'
|
75
|
+
root.answers[0].data.should == { 'author_name' => 'john',
|
76
|
+
'text' => 'Thanks',
|
77
|
+
'answers' => [] }
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should save one-to-many real association" do
|
81
|
+
file = StringIO.new
|
82
|
+
File.should_receive(:open).with(anything(), 'w').and_yield(file)
|
83
|
+
|
84
|
+
::Comment.first().save()
|
85
|
+
|
86
|
+
file.should has_yaml([
|
87
|
+
{
|
88
|
+
'author_name' => 'super1997',
|
89
|
+
'text' => 'Cool!',
|
90
|
+
'answers' => [{ 'author_name' => 'john', 'text' => 'Thanks' }]
|
91
|
+
}
|
92
|
+
])
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should find map for virtual association" do
|
96
|
+
PlainRecord::Associations.map(
|
97
|
+
::Comment, ::CommentedPost, 'commented_post_').should == {
|
98
|
+
:commented_post_name => :name }
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should load one-to-one virtual association" do
|
102
|
+
post = ::FilepathPost.first(:name => '1')
|
103
|
+
comment = ::Comment.first(:author_name => 'super1997')
|
104
|
+
comment.commented_post.should == post
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should change one-to-one virtual association" do
|
108
|
+
post = ::FilepathPost.first(:name => '2')
|
109
|
+
comment = ::Comment.first(:author_name => 'super1997')
|
110
|
+
comment.commented_post = post
|
111
|
+
|
112
|
+
post.name.should == '1'
|
113
|
+
comment.commented_post.should == post
|
114
|
+
end
|
115
|
+
|
116
|
+
it "should load one-to-many virtual association" do
|
117
|
+
post = ::CommentedPost.first(:name => '1')
|
118
|
+
post.should have(1).comments
|
119
|
+
post.comments.first.should == ::Comment.first(:author_name => 'super1997')
|
120
|
+
end
|
121
|
+
|
122
|
+
it "should add new item to one-to-many virtual association" do
|
123
|
+
post = ::CommentedPost.first(:name => '1')
|
124
|
+
comment = ::Comment.new
|
125
|
+
post.comments << comment
|
126
|
+
|
127
|
+
post.should have(2).comments
|
128
|
+
post.comments[1].should == comment
|
129
|
+
comment.commented_post_name.should == post.name
|
130
|
+
end
|
131
|
+
|
132
|
+
it "should create new one-to-many association" do
|
133
|
+
post = ::CommentedPost.new(:name => 'new')
|
134
|
+
comment = ::Comment.new
|
135
|
+
post.comments = [comment]
|
136
|
+
|
137
|
+
post.should have(1).comments
|
138
|
+
post.comments.first.should == comment
|
139
|
+
comment.commented_post_name.should == post.name
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|