plain_record 0.1 → 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.
- 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
|