plain_record 0.2 → 0.3

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.
@@ -0,0 +1,86 @@
1
+ =begin
2
+ Extention to convert fields to special type.
3
+
4
+ Copyright (C) 2012 Andrey “A.I.” Sitnik <andrey@sitnik.ru>,
5
+ sponsored by Evil Martians.
6
+
7
+ This program is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU Lesser General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
+
12
+ This program is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU Lesser General Public License for more details.
16
+
17
+ You should have received a copy of the GNU Lesser General Public License
18
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
19
+ =end
20
+
21
+ require 'time'
22
+ require 'date'
23
+
24
+ module PlainRecord
25
+ # Extention to set field type and convert value to it.
26
+ #
27
+ # class Post
28
+ # include PlainRecord::Resource
29
+ #
30
+ # entry_in '*/*/post.md'
31
+ #
32
+ # field :title, type(String)
33
+ # field :visits, type(Integer)
34
+ # field :rating, type(Float)
35
+ # field :created, type(Date)
36
+ # field :updated, type(Time)
37
+ # end
38
+ #
39
+ # You can add support for your classes. Just set parse and stringify
40
+ # code to <tt>PlainRecord::Type</tt>:
41
+ #
42
+ # PlainRecord::Type.parsers[Car] = 'Car.parse(super)'
43
+ # PlainRecord::Type.stringifies[Car] = 'v.to_s'
44
+ module Type
45
+
46
+ private
47
+
48
+ # Filter to convert field values to some type.
49
+ def type(klass)
50
+ proc do |model, field, type|
51
+ model.add_accessors <<-EOS, __FILE__, __LINE__
52
+ def #{field}
53
+ v = super
54
+ #{Type.parsers[klass]}
55
+ end
56
+ def #{field}=(v)
57
+ super(#{Type.stringifies[klass]})
58
+ end
59
+ EOS
60
+ end
61
+ end
62
+
63
+ class << self
64
+ # Hash of class to string of parse code.
65
+ attr_accessor :parsers
66
+
67
+ # Hash of class to string of stringify code.
68
+ attr_accessor :stringifies
69
+ end
70
+
71
+ Type.parsers = {
72
+ String => 'v ? v.to_s : v',
73
+ Integer => 'v ? v.to_i : v',
74
+ Float => 'v ? v.to_f : v',
75
+ Time => 'v.is_a?(String) ? Time.parse(v) : v',
76
+ Date => 'v.is_a?(String) ? Date.parse(v) : v'
77
+ }
78
+ Type.stringifies = {
79
+ String => 'v ? v.to_s : v',
80
+ Integer => 'v ? v.to_i : v',
81
+ Float => 'v ? v.to_f : v',
82
+ Time => 'v ? v.strftime("%Y-%m-%d %H:%M:%S %Z") : v',
83
+ Date => 'v'
84
+ }
85
+ end
86
+ end
@@ -1,3 +1,3 @@
1
1
  module PlainRecord
2
- VERSION = '0.2' unless defined? PlainRecord::VERSION
2
+ VERSION = '0.3' unless defined? PlainRecord::VERSION
3
3
  end
data/lib/plain_record.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  =begin
2
2
  Main file to load all neccessary classes for Plain Record.
3
3
 
4
- Copyright (C) 2009 Andrey “A.I.” Sitnik <andrey@sitnik.ru>
4
+ Copyright (C) 2009 Andrey “A.I.” Sitnik <andrey@sitnik.ru>,
5
+ sponsored by Evil Martians.
5
6
 
6
7
  This program is free software: you can redistribute it and/or modify
7
8
  it under the terms of the GNU Lesser General Public License as published by
@@ -25,21 +26,29 @@ YAML::ENGINE.yamler = 'syck' if defined? YAML::ENGINE
25
26
  dir = Pathname(__FILE__).dirname.expand_path + 'plain_record'
26
27
  require dir + 'version'
27
28
  require dir + 'callbacks'
29
+ require dir + 'default'
28
30
  require dir + 'filepath'
29
31
  require dir + 'association_proxy'
30
32
  require dir + 'associations'
33
+ require dir + 'type'
31
34
  require dir + 'model'
32
35
  require dir + 'resource'
33
36
 
34
37
  module PlainRecord
38
+ module Extra
39
+ autoload :Git, 'plain_record/extra/git'
40
+ autoload :I18n, 'plain_record/extra/i18n'
41
+ end
42
+
35
43
  class << self
36
44
  # Set new root for Model#entry_in or Model#list_in.
37
45
  #
38
46
  # Note, that it add last slash to root path (<tt>/content</tt> will be saved
39
47
  # as <tt>/content/</tt>).
40
48
  def root=(value)
49
+ value = value.to_s
41
50
  value += File::SEPARATOR if File::SEPARATOR != value[-1..-1]
42
- @root = value
51
+ @root = value
43
52
  end
44
53
 
45
54
  # Return root for Model#entry_in or Model#list_in.
@@ -50,3 +59,9 @@ module PlainRecord
50
59
  end
51
60
  end
52
61
  end
62
+
63
+ if defined? Rails
64
+ ActiveSupport.on_load(:after_initialize) do
65
+ PlainRecord.root = Rails.root.join('data')
66
+ end
67
+ end
data/plain_record.gemspec CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
9
9
  'readable plain text files.'
10
10
  s.description = <<-EOF
11
11
  Plain Record is a data persistence, which use human editable and
12
- readable plain text files. Its ideal for static generated sites,
12
+ readable plain text files. It's ideal for static generated sites,
13
13
  like blog or homepage.
14
14
  EOF
15
15
 
@@ -27,4 +27,6 @@ Gem::Specification.new do |s|
27
27
  s.add_development_dependency "rake", [">= 0"]
28
28
  s.add_development_dependency "rspec", [">= 0"]
29
29
  s.add_development_dependency "redcarpet", [">= 0"]
30
+ s.add_development_dependency "r18n-core", [">= 0"]
31
+ s.add_development_dependency "i18n", [">= 0"]
30
32
  end
@@ -10,7 +10,7 @@ describe PlainRecord::Associations do
10
10
  class ::RatedPost
11
11
  include PlainRecord::Resource
12
12
  entry_in 'data/3/post.md'
13
- property :rate, one(::Rate)
13
+ field :rate, one(::Rate)
14
14
  end
15
15
 
16
16
  class ::Comment
@@ -21,9 +21,9 @@ describe PlainRecord::Associations do
21
21
  virtual :commented_post_name, in_filepath(1)
22
22
  virtual :commented_post, one(::FilepathPost)
23
23
 
24
- property :author_name
25
- property :text
26
- property :answers, many(::Comment)
24
+ field :author_name
25
+ field :text
26
+ field :answers, many(::Comment)
27
27
  end
28
28
 
29
29
  class ::CommentedPost
@@ -68,7 +68,7 @@ describe PlainRecord::Associations do
68
68
  end
69
69
 
70
70
  it "should load one-to-many real association" do
71
- root = ::Comment.first()
71
+ root = ::Comment.first(:commented_post_name => '1')
72
72
  root.should have(1).answers
73
73
  root.answers[0].should be_instance_of(::Comment)
74
74
  root.answers[0].path.should == 'data/1/comments.yml'
@@ -78,10 +78,11 @@ describe PlainRecord::Associations do
78
78
  end
79
79
 
80
80
  it "should save one-to-many real association" do
81
+ comment = ::Comment.first(:commented_post_name => '1')
82
+
81
83
  file = StringIO.new
82
84
  File.should_receive(:open).with(anything(), 'w').and_yield(file)
83
-
84
- ::Comment.first().save()
85
+ comment.save()
85
86
 
86
87
  file.should has_yaml([
87
88
  {
@@ -94,7 +95,7 @@ describe PlainRecord::Associations do
94
95
 
95
96
  it "should find map for virtual association" do
96
97
  PlainRecord::Associations.map(
97
- ::Comment, ::CommentedPost, 'commented_post_').should == {
98
+ ::Comment, ::CommentedPost, 'commented_post_').should == {
98
99
  :commented_post_name => :name }
99
100
  end
100
101
 
@@ -102,6 +103,9 @@ describe PlainRecord::Associations do
102
103
  post = ::FilepathPost.first(:name => '1')
103
104
  comment = ::Comment.first(:author_name => 'super1997')
104
105
  comment.commented_post.should == post
106
+
107
+ another = ::Comment.first(:commented_post_name => '2')
108
+ another.commented_post.should_not == post
105
109
  end
106
110
 
107
111
  it "should change one-to-one virtual association" do
@@ -0,0 +1,2 @@
1
+ - author_name: john
2
+ text: Fix
data/spec/data/2/post.md CHANGED
@@ -1,3 +1,3 @@
1
- unknow_property: 1
1
+ unknow_field: 1
2
2
  ---
3
3
  only one
@@ -0,0 +1,17 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe PlainRecord::Default do
4
+
5
+ it "should set default value for field" do
6
+ klass = Class.new do
7
+ include PlainRecord::Resource
8
+ field :category, default('uncategorized')
9
+ end
10
+ post = klass.new
11
+
12
+ post.category.should == 'uncategorized'
13
+ post.category = 'a'
14
+ post.category.should == 'a'
15
+ end
16
+
17
+ end
@@ -2,50 +2,50 @@ require File.join(File.dirname(__FILE__), 'spec_helper')
2
2
 
3
3
  describe PlainRecord::Filepath do
4
4
 
5
- it "shouldn't create non-virtual filepath property" do
5
+ it "shouldn't create non-virtual filepath field" do
6
6
  lambda {
7
7
  Class.new do
8
8
  include PlainRecord::Resource
9
9
  entry_in 'data/*/post.md'
10
- property :category, in_filepath(1)
10
+ field :category, in_filepath(1)
11
11
  end
12
12
  }.should raise_error(ArgumentError, /virtual creator/)
13
13
  end
14
14
 
15
- it "should load filepath property" do
15
+ it "should load filepath field" do
16
16
  best = FilepathPost.first(:title => 'Best')
17
17
  best.category.should == 'best/'
18
- best.name.should == '4'
18
+ best.name.should == '4'
19
19
  end
20
20
 
21
- it "should load filepath property as nil when ** pattern is empty" do
21
+ it "should load filepath field as nil when ** pattern is empty" do
22
22
  FilepathPost.first(:title => 'First').category.should be_empty
23
23
  end
24
24
 
25
- it "should return more accurate path by filepath properties" do
25
+ it "should return more accurate path by filepath fields" do
26
26
  FilepathPost.path(:name => 2).should == 'data/**/2/post.md'
27
27
  end
28
28
 
29
- it "should use filepath properties in search" do
29
+ it "should use filepath fields in search" do
30
30
  FilepathPost.loaded = { }
31
31
  FilepathPost.all(:category => 'best/')
32
32
  FilepathPost.loaded.should have(1).keys
33
33
  end
34
34
 
35
- it "should load properties from model constructor" do
35
+ it "should load fields from model constructor" do
36
36
  post = FilepathPost.new(:name => 5)
37
37
  post.name.should == 5
38
38
  post.category.should be_nil
39
39
  end
40
40
 
41
- it "should get entry path by filepath properties" do
41
+ it "should get entry path by filepath fields" do
42
42
  path = File.join(File.dirname(__FILE__), 'data/5/post.md')
43
43
  post = FilepathPost.new(:name => 5, :category => '')
44
44
  FilepathPost.should_receive(:move_entry).with(post, nil, path)
45
45
  post.save
46
46
  end
47
47
 
48
- it "should raise error, when can't get entry path by filepath properties" do
48
+ it "should raise error, when can't get entry path by filepath fields" do
49
49
  post = FilepathPost.new
50
50
  lambda { post.save }.should raise_error(ArgumentError, /isn't file to save/)
51
51
  end
data/spec/git_spec.rb ADDED
@@ -0,0 +1,55 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe PlainRecord::Extra::Git do
4
+
5
+ before :all do
6
+ class TimedPost
7
+ include PlainRecord::Resource
8
+ include PlainRecord::Extra::Git
9
+
10
+ entry_in 'data/*/post.md'
11
+
12
+ virtual :name, in_filepath(1)
13
+ field :created, git_created_time
14
+ field :updated, git_modified_time
15
+ end
16
+ end
17
+
18
+ before do
19
+ @post = TimedPost.first(:name => '2')
20
+
21
+ @now = Time.at(256)
22
+ Time.stub!(:now).and_return(@now)
23
+ end
24
+
25
+ it "should take file create time from git" do
26
+ @post.created.utc.should == Time.parse('2012-05-17 22:23:47 UTC')
27
+ end
28
+
29
+ it "should take file updated time from git" do
30
+ @post.updated.utc.should == Time.parse('2012-05-18 07:41:52 UTC')
31
+ end
32
+
33
+ it "should overrided git time" do
34
+ post = TimedPost.new
35
+
36
+ post.created = Time.at(0)
37
+ post.created.should == Time.at(0)
38
+
39
+ post.updated = Time.at(1)
40
+ post.updated.should == Time.at(1)
41
+ end
42
+
43
+ it "should return now for new model" do
44
+ post = TimedPost.new
45
+ post.created.should == @now
46
+ post.updated.should == @now
47
+ end
48
+
49
+ it "shoult return now if file has uncommitted changes" do
50
+ @post.stub!(:git_uncommitted?).and_return(true)
51
+ @post.created.utc.should == Time.parse('2012-05-17 22:23:47 UTC')
52
+ @post.updated.should == @now
53
+ end
54
+
55
+ end
data/spec/i18n_spec.rb ADDED
@@ -0,0 +1,167 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe PlainRecord::Extra::I18n do
4
+
5
+ before :all do
6
+ class I18nPost
7
+ include PlainRecord::Resource
8
+ include PlainRecord::Extra::I18n
9
+ field :one, i18n
10
+ end
11
+
12
+ class PlainI18nPost < I18nPost
13
+ attr_accessor :locale
14
+ end
15
+ end
16
+
17
+ it "should raise error on i18n filter on text" do
18
+ lambda {
19
+ klass = Class.new do
20
+ include PlainRecord::Resource
21
+ include PlainRecord::Extra::I18n
22
+ text :one, i18n
23
+ end
24
+ }.should raise_error(ArgumentError, /text/)
25
+ end
26
+
27
+ it "should save translations with locale" do
28
+ post = PlainI18nPost.new
29
+
30
+ post.locale = 'en'
31
+ post.one = 1
32
+ post.data['one'].should == { 'en' => 1 }
33
+
34
+ post.locale = 'ru'
35
+ post.one = 2
36
+ post.data['one'].should == { 'en' => 1, 'ru' => 2 }
37
+ end
38
+
39
+ it "should return untraslated field" do
40
+ post = PlainI18nPost.new
41
+
42
+ post.locale = 'en'
43
+ post.one = 1
44
+ post.untraslated_one.should == { 'en' => 1 }
45
+
46
+ post.untraslated_one = { 'ru' => 2 }
47
+ post.data['one'] == { 'ru' => 2 }
48
+ end
49
+
50
+ it "should return original field if it is not hash" do
51
+ post = PlainI18nPost.new
52
+ post.one.should be_nil
53
+ post.data['one'] = 1
54
+ post.one.should == 1
55
+ end
56
+
57
+ context 'plain' do
58
+
59
+ it "should raise error when locale method is not redefined" do
60
+ klass = Class.new do
61
+ include PlainRecord::Resource
62
+ include PlainRecord::Extra::I18n
63
+ field :one, i18n
64
+ end
65
+ post = klass.new
66
+ lambda { post.locale }.should raise_error(/Redefine/)
67
+ end
68
+
69
+ it "should translate method" do
70
+ post = PlainI18nPost.new
71
+ post.data['one'] = { 'en' => 1, 'ru' => 2 }
72
+
73
+ post.locale = 'en'
74
+ post.one.should == 1
75
+
76
+ post.locale = 'ru'
77
+ post.one.should == 2
78
+ end
79
+ end
80
+
81
+ context 'i18n' do
82
+ before :all do
83
+ require 'i18n'
84
+ end
85
+
86
+ it "should git locale from I18n" do
87
+ post = I18nPost.new
88
+ post.data['one'] = { 'en' => 1, 'ru' => 2 }
89
+
90
+ I18n.locale = :en
91
+ post.one == 1
92
+
93
+ I18n.locale = :ru
94
+ post.one == 2
95
+
96
+ post.one = 3
97
+ post.data['one'].should == { 'en' => 1, 'ru' => 3 }
98
+ end
99
+ end
100
+
101
+ context 'r18n' do
102
+ before :all do
103
+ require 'r18n-core'
104
+ end
105
+
106
+ before do
107
+ @post = I18nPost.new
108
+ end
109
+
110
+ it "should get I18n object from R18n" do
111
+ @post.data['one'] = { 'en' => 1, 'ru' => 2 }
112
+
113
+ R18n.set('en')
114
+ @post.one == 1
115
+
116
+ R18n.set('ru')
117
+ @post.one == 2
118
+
119
+ @post.one = 3
120
+ @post.data['one'].should == { 'en' => 1, 'ru' => 3 }
121
+ end
122
+
123
+ it "should return translated string" do
124
+ @post.data['one'] = { 'en' => '1' }
125
+ R18n.set('en')
126
+
127
+ @post.one.should be_translated
128
+ @post.one.locale.code.should == 'en'
129
+ @post.one.path.should == 'I18nPost#one'
130
+ end
131
+
132
+ it "should find translation in user locales" do
133
+ @post.data['one'] = { 'fr' => '1' }
134
+
135
+ R18n.set(['ru', 'fr'])
136
+ @post.one.should == '1'
137
+ end
138
+
139
+ it "should return untraslated" do
140
+ @post.data['one'] = { 'fr' => '1' }
141
+ R18n.set('ru')
142
+
143
+ @post.one.should_not be_translated
144
+ @post.one.translated_path.should == 'I18nPost#'
145
+ @post.one.untranslated_path.should == 'one'
146
+ end
147
+
148
+ it "should translate non-strings" do
149
+ @post.data['one'] = { 'ru' => { :a => 1 } }
150
+ R18n.set('ru')
151
+
152
+ @post.one.should == { :a => 1 }
153
+ end
154
+
155
+ it "should use filters for custom type" do
156
+ klass = Class.new do
157
+ include PlainRecord::Resource
158
+ include PlainRecord::Extra::I18n
159
+ field :one, i18n('pl')
160
+ end
161
+ post = klass.new
162
+ post.data['one'] = { 'en' => { '1' => '%1 one', 'n' => '%1 ones' } }
163
+ post.one(5).should == '5 ones'
164
+ end
165
+ end
166
+
167
+ end
data/spec/model_spec.rb CHANGED
@@ -7,31 +7,33 @@ describe PlainRecord::Model do
7
7
  Author.loaded = { }
8
8
  end
9
9
 
10
- it "should define virtual property" do
10
+ it "should define virtual field" do
11
11
  klass = Class.new do
12
12
  include PlainRecord::Resource
13
- virtual :one, Definers.none
13
+ virtual :one, proc { }
14
14
  end
15
15
 
16
16
  klass.virtuals.should == [:one]
17
17
  end
18
18
 
19
- it "shouldn't define virtual property without accessor from definers" do
19
+ it "shouldn't define virtual field without accessor from filters" do
20
20
  lambda {
21
21
  Class.new do
22
22
  include PlainRecord::Resource
23
- virtual :one, Definers.reader
23
+ virtual :one
24
24
  end
25
25
  }.should raise_error(ArgumentError, /own accessors/)
26
26
  end
27
27
 
28
- it "should define property" do
28
+ it "should define field" do
29
29
  klass = Class.new do
30
30
  include PlainRecord::Resource
31
- property :one
31
+ field :one
32
32
  end
33
33
 
34
- klass.properties.should == [:one]
34
+ klass.fields.should == [:one]
35
+ klass.accessors_modules[:main].should has_methods(:one, :one=)
36
+
35
37
  object = klass.new(nil, { 'one' => 1 })
36
38
  object.one.should == 1
37
39
  object.one = 2
@@ -45,43 +47,43 @@ describe PlainRecord::Model do
45
47
  end
46
48
 
47
49
  klass.texts.should == [:content]
50
+ klass.accessors_modules[:main].should has_methods(:content, :content=)
51
+
48
52
  object = klass.new(nil, { }, ['text'])
49
53
  object.content.should == 'text'
50
54
  object.content = 'another'
51
55
  object.content.should == 'another'
52
56
  end
53
57
 
54
- it "should call definer" do
55
- klass = Class.new do
58
+ it "should send field name and type type to filter" do
59
+ klass = Class.new
60
+ filter = mock
61
+ filter.stub!(:virtual).with(klass, :one, :virtual)
62
+ filter.stub!(:field).with(klass, :two, :field)
63
+ filter.stub!(:text).with(klass, :three, :text)
64
+ klass.class_eval do
56
65
  include PlainRecord::Resource
57
- property :one, Definers.accessor
58
- property :two, Definers.reader
59
- text :three, Definers.writer
60
- text :four, Definers.none
66
+ virtual :one, filter.method(:virtual)
67
+ field :two, filter.method(:field)
68
+ text :three, filter.method(:text)
61
69
  end
62
- klass.should has_methods(:one, :'one=', :'three=', :two)
63
70
  end
64
71
 
65
- it "should use accessors from definers" do
66
- klass = Class.new do
67
- include PlainRecord::Resource
68
- property :one, Definers.writer, Definers.reader, Definers.accessor
69
- text :two, Definers.reader
72
+ it "should override sustem accessors by filter" do
73
+ filter = proc do |model, name, type|
74
+ model.add_accessors <<-EOS, __FILE__, __LINE__
75
+ def #{name}
76
+ super + 1
77
+ end
78
+ EOS
70
79
  end
71
- klass.should has_methods(:two)
72
- end
73
-
74
- it "should send property name and caller type to definer" do
75
- definer = mock
76
- definer.stub!(:virtual).with(:one, :virtual)
77
- definer.stub!(:property).with(:two, :property)
78
- definer.stub!(:text).with(:three, :text)
79
80
  klass = Class.new do
80
81
  include PlainRecord::Resource
81
- virtual :one, definer.method(:virtual)
82
- property :two, definer.method(:property)
83
- text :three, definer.method(:text)
82
+ field :one, filter
84
83
  end
84
+ a = klass.new
85
+ a.one = 1
86
+ a.one.should == 2
85
87
  end
86
88
 
87
89
  it "should find all enrty files by glob pattern" do
@@ -219,4 +221,43 @@ describe PlainRecord::Model do
219
221
  end
220
222
  end
221
223
 
224
+ it "should add modules for accessors" do
225
+ klass = Class.new do
226
+ include PlainRecord::Resource
227
+ end
228
+
229
+ klass.accessors_modules.should be_empty
230
+
231
+ main = klass.add_accessors(:main)
232
+ klass.add_accessors(:main).should == main
233
+ klass.accessors_modules.should have(1).keys
234
+
235
+ mod = klass.add_accessors
236
+ mod.should_not == main
237
+ klass.add_accessors.should_not == mod
238
+ klass.accessors_modules.should have(1).keys
239
+ end
240
+
241
+ it "should define accessors" do
242
+ klass = Class.new do
243
+ include PlainRecord::Resource
244
+ end
245
+ klass.add_accessors :one, "def one; 1; end"
246
+ klass.add_accessors "def two; 2; end"
247
+ klass.add_accessors <<-EOS, __FILE__, __LINE__
248
+ def three; 3; end
249
+ EOS
250
+
251
+ klass.should has_methods(:one, :two, :three)
252
+ end
253
+
254
+ it "should allow to define filters as Hash" do
255
+ klass = Class.new do
256
+ include PlainRecord::Resource
257
+ field :one, :default => 1
258
+ end
259
+ a = klass.new
260
+ a.one.should == 1
261
+ end
262
+
222
263
  end