plain_record 0.1.0 → 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 +29 -41
- data/lib/plain_record.rb +22 -2
- 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.rb +134 -61
- data/lib/plain_record/model/entry.rb +20 -8
- data/lib/plain_record/model/list.rb +30 -8
- data/lib/plain_record/resource.rb +76 -18
- data/lib/plain_record/version.rb +3 -0
- 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 +100 -39
- data/spec/resource_spec.rb +84 -23
- data/spec/spec_helper.rb +33 -14
- metadata +119 -61
- data/README.rdoc +0 -92
- data/VERSION +0 -1
- data/spec/data/3/post.m +0 -1
@@ -30,19 +30,31 @@ module PlainRecord
|
|
30
30
|
end
|
31
31
|
@loaded[file]
|
32
32
|
end
|
33
|
-
|
34
|
-
def each_entry
|
35
|
-
files.each do |file|
|
33
|
+
|
34
|
+
def each_entry(matcher = { })
|
35
|
+
files(matcher).each do |file|
|
36
36
|
yield load_file(file)
|
37
37
|
end
|
38
38
|
end
|
39
|
-
|
39
|
+
|
40
|
+
def delete_entry(file, entry = nil)
|
41
|
+
delete_file(file)
|
42
|
+
end
|
43
|
+
|
44
|
+
def move_entry(entry, from, to)
|
45
|
+
if from
|
46
|
+
@loaded.delete(from)
|
47
|
+
delete_file(from)
|
48
|
+
end
|
49
|
+
@loaded[to] = entry
|
50
|
+
end
|
51
|
+
|
40
52
|
private
|
41
|
-
|
42
|
-
def all_entries
|
43
|
-
files.map { |file| load_file(file) }
|
53
|
+
|
54
|
+
def all_entries(matcher = { })
|
55
|
+
files(matcher).map { |file| load_file(file) }
|
44
56
|
end
|
45
|
-
|
57
|
+
|
46
58
|
def entries_string(entry)
|
47
59
|
entry.to_yaml + entry.texts.map{ |i| "---\n" + i }.join("\n")
|
48
60
|
end
|
@@ -29,21 +29,43 @@ module PlainRecord
|
|
29
29
|
end
|
30
30
|
@loaded[file]
|
31
31
|
end
|
32
|
-
|
33
|
-
def each_entry
|
34
|
-
files.each do |file|
|
32
|
+
|
33
|
+
def each_entry(matcher = { })
|
34
|
+
files(matcher).each do |file|
|
35
35
|
load_file(file).each do |entry|
|
36
36
|
yield entry
|
37
37
|
end
|
38
38
|
end
|
39
39
|
end
|
40
|
-
|
40
|
+
|
41
|
+
def delete_entry(file, entry = nil)
|
42
|
+
if entry.nil? or 1 == @loaded[file].length
|
43
|
+
delete_file(file)
|
44
|
+
else
|
45
|
+
@loaded[file].delete(entry)
|
46
|
+
save_file(file)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def move_entry(entry, from, to)
|
51
|
+
if from
|
52
|
+
@loaded[from].delete(entry)
|
53
|
+
if @loaded[from].empty?
|
54
|
+
delete_file(from)
|
55
|
+
else
|
56
|
+
save_file(from)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
@loaded[to] = [] unless @loaded.has_key? to
|
60
|
+
@loaded[to] << entry
|
61
|
+
end
|
62
|
+
|
41
63
|
private
|
42
|
-
|
43
|
-
def all_entries
|
44
|
-
files.map { |file| load_file(file) }.flatten
|
64
|
+
|
65
|
+
def all_entries(matcher = { })
|
66
|
+
files(matcher).map { |file| load_file(file) }.flatten
|
45
67
|
end
|
46
|
-
|
68
|
+
|
47
69
|
def entries_string(entries)
|
48
70
|
entries.to_yaml
|
49
71
|
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,38 +52,80 @@ 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
|
75
|
+
end
|
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
|
54
88
|
end
|
55
|
-
|
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
|
111
|
+
end
|
112
|
+
|
113
|
+
# Delete current entry and it file if there isn’t has any other entries.
|
114
|
+
def destroy
|
115
|
+
self.class.use_callbacks(:destroy, self) do
|
116
|
+
self.class.delete_entry(@file, self)
|
117
|
+
end
|
60
118
|
end
|
61
|
-
|
119
|
+
|
62
120
|
# Return string of YAML representation of entry +data+.
|
63
|
-
def to_yaml(opts = {})
|
121
|
+
def to_yaml(opts = { })
|
64
122
|
@data.to_yaml(opts)
|
65
123
|
end
|
66
|
-
|
124
|
+
|
67
125
|
# Compare if its properties and texts are equal.
|
68
126
|
def eql?(other)
|
69
127
|
return false unless other.kind_of?(self.class)
|
70
|
-
@data == other.data and @texts == @texts
|
128
|
+
@file == other.file and @data == other.data and @texts == @texts
|
71
129
|
end
|
72
130
|
alias == eql?
|
73
131
|
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
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
describe PlainRecord::Callbacks do
|
4
|
+
before :all do
|
5
|
+
module ::Fullname
|
6
|
+
include PlainRecord::Callbacks
|
7
|
+
|
8
|
+
def fullname(first, second)
|
9
|
+
use_callbacks(:fullname, first, second) do
|
10
|
+
first + ' ' + second
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should use methods without callbacks" do
|
17
|
+
Class.new {
|
18
|
+
extend ::Fullname
|
19
|
+
}.fullname('John', 'Smith').should == 'John Smith'
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should use before callbacks by priority" do
|
23
|
+
checker = mock()
|
24
|
+
checker.should_receive(:check_first).with('John', 'Smith').once.ordered
|
25
|
+
checker.should_receive(:check_last).with('John', 'Smith').once.ordered
|
26
|
+
|
27
|
+
Class.new {
|
28
|
+
extend ::Fullname
|
29
|
+
|
30
|
+
before :fullname, 2, &checker.method(:check_last)
|
31
|
+
before :fullname, 1, &checker.method(:check_first)
|
32
|
+
}.fullname('John', 'Smith').should == 'John Smith'
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should use after callbacks by priority" do
|
36
|
+
adder = Class.new do
|
37
|
+
def self.add_first(full, first, last); full + ' ' + first.downcase end
|
38
|
+
def self.add_last(full, first, last); full + ' ' + last.downcase end
|
39
|
+
end
|
40
|
+
|
41
|
+
Class.new {
|
42
|
+
extend ::Fullname
|
43
|
+
|
44
|
+
after :fullname, 2, &adder.method(:add_last)
|
45
|
+
after :fullname, 1, &adder.method(:add_first)
|
46
|
+
}.fullname('John', 'Smith').should == 'John Smith john smith'
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should set one callback for many events" do
|
50
|
+
klass = Class.new {
|
51
|
+
extend ::Fullname
|
52
|
+
before [:one, :two] do; end
|
53
|
+
before [:one, :two] do; end
|
54
|
+
}
|
55
|
+
klass.callbacks[:before].length.should == 2
|
56
|
+
klass.callbacks[:after].length.should == 2
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|