plain_record 0.1.0 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 '/content/*/post.m'
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
- attr_reader :file
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
- @file = file
52
- @data = data
53
- @texts = texts
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.save_file(@file)
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,3 @@
1
+ module PlainRecord
2
+ VERSION = '0.2' unless defined? PlainRecord::VERSION
3
+ 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