plain_record 0.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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