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