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.
- 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
|