plain_record 0.1 → 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 +27 -50
- 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/entry.rb +17 -9
- data/lib/plain_record/model/list.rb +22 -9
- data/lib/plain_record/model.rb +119 -64
- data/lib/plain_record/resource.rb +72 -19
- data/lib/plain_record/version.rb +1 -1
- data/lib/plain_record.rb +21 -2
- 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 +90 -42
- data/spec/resource_spec.rb +70 -27
- data/spec/spec_helper.rb +33 -14
- metadata +122 -70
- data/README.rdoc +0 -96
- data/spec/data/3/post.m +0 -1
    
        data/lib/plain_record/model.rb
    CHANGED
    
    | @@ -26,36 +26,55 @@ module PlainRecord | |
| 26 26 | 
             
                dir = Pathname(__FILE__).dirname.expand_path + 'model'
         | 
| 27 27 | 
             
                autoload :Entry, (dir + 'entry').to_s
         | 
| 28 28 | 
             
                autoload :List,  (dir + 'list').to_s
         | 
| 29 | 
            -
             | 
| 30 | 
            -
                 | 
| 31 | 
            -
                 | 
| 32 | 
            -
                
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                include PlainRecord::Callbacks
         | 
| 31 | 
            +
                include PlainRecord::Filepath
         | 
| 32 | 
            +
                include PlainRecord::Associations
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                # YAML properties names.
         | 
| 35 | 
            +
                attr_accessor :properties
         | 
| 36 | 
            +
             | 
| 33 37 | 
             
                # Name of special properties with big text.
         | 
| 34 | 
            -
                 | 
| 35 | 
            -
             | 
| 38 | 
            +
                attr_accessor :texts
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                # Properties names with dynamic value.
         | 
| 41 | 
            +
                attr_accessor :virtuals
         | 
| 42 | 
            +
             | 
| 36 43 | 
             
                # Storage type: +:entry+ or +:list+.
         | 
| 37 44 | 
             
                attr_reader :storage
         | 
| 38 | 
            -
             | 
| 45 | 
            +
             | 
| 39 46 | 
             
                # Content of already loaded files.
         | 
| 40 | 
            -
                 | 
| 41 | 
            -
             | 
| 47 | 
            +
                attr_accessor :loaded
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                def self.extended(base) #:nodoc:
         | 
| 50 | 
            +
                  base.properties = []
         | 
| 51 | 
            +
                  base.virtuals   = []
         | 
| 52 | 
            +
                  base.texts      = []
         | 
| 53 | 
            +
                  base.loaded     = { }
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 42 56 | 
             
                # Load and return all entries in +file+.
         | 
| 43 57 | 
             
                #
         | 
| 44 58 | 
             
                # See method code in <tt>Model::Entry</tt> or <tt>Model::List</tt>.
         | 
| 45 59 | 
             
                def load_file(file); end
         | 
| 46 | 
            -
             | 
| 47 | 
            -
                # Call block on all entry | 
| 48 | 
            -
                # loading, so it is useful if you planing | 
| 49 | 
            -
                # the middle (for example, like +first+).
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                # Call block on all entry, which is may be match for +matchers+. Unlike
         | 
| 62 | 
            +
                # <tt>all.each</tt> it use lazy file loading, so it is useful if you planing
         | 
| 63 | 
            +
                # to break this loop somewhere in the middle (for example, like +first+).
         | 
| 50 64 | 
             
                #
         | 
| 51 65 | 
             
                # See method code in <tt>Model::Entry</tt> or <tt>Model::List</tt>.
         | 
| 52 | 
            -
                def each_entry; end
         | 
| 53 | 
            -
             | 
| 66 | 
            +
                def each_entry(matchers = { }); end
         | 
| 67 | 
            +
             | 
| 54 68 | 
             
                # Delete +entry+ from +file+.
         | 
| 55 69 | 
             
                #
         | 
| 56 70 | 
             
                # See method code in <tt>Model::Entry</tt> or <tt>Model::List</tt>.
         | 
| 57 71 | 
             
                def delete_entry(file, entry = nil); end
         | 
| 58 | 
            -
             | 
| 72 | 
            +
             | 
| 73 | 
            +
                # Move +entry+ from one file to another.
         | 
| 74 | 
            +
                #
         | 
| 75 | 
            +
                # See method code in <tt>Model::Entry</tt> or <tt>Model::List</tt>.
         | 
| 76 | 
            +
                def move_entry(entry, from, to); end
         | 
| 77 | 
            +
             | 
| 59 78 | 
             
                # Write all loaded entries to +file+.
         | 
| 60 79 | 
             
                def save_file(file)
         | 
| 61 80 | 
             
                  if @loaded.has_key? file
         | 
| @@ -64,7 +83,7 @@ module PlainRecord | |
| 64 83 | 
             
                    end
         | 
| 65 84 | 
             
                  end
         | 
| 66 85 | 
             
                end
         | 
| 67 | 
            -
             | 
| 86 | 
            +
             | 
| 68 87 | 
             
                # Return all entries, which is match for +matchers+ and return true on
         | 
| 69 88 | 
             
                # +block+.
         | 
| 70 89 | 
             
                #
         | 
| @@ -74,13 +93,13 @@ module PlainRecord | |
| 74 93 | 
             
                #   Post.all(title: 'Post title')
         | 
| 75 94 | 
             
                #   Post.all(title: /^Post/, summary: /cool/)
         | 
| 76 95 | 
             
                #   Post.all { |post| 20 < Post.content.length }
         | 
| 77 | 
            -
                def all(matchers = {}, &block)
         | 
| 78 | 
            -
                  entries = all_entries
         | 
| 96 | 
            +
                def all(matchers = { }, &block)
         | 
| 97 | 
            +
                  entries = all_entries(matchers)
         | 
| 79 98 | 
             
                  entries.delete_if { |i| not match(i, matchers) } if matchers
         | 
| 80 | 
            -
                  entries.delete_if { |i| not block.call(i) } | 
| 99 | 
            +
                  entries.delete_if { |i| not block.call(i) }      if block_given?
         | 
| 81 100 | 
             
                  entries
         | 
| 82 101 | 
             
                end
         | 
| 83 | 
            -
             | 
| 102 | 
            +
             | 
| 84 103 | 
             
                # Return first entry, which is match for +matchers+ and return true on
         | 
| 85 104 | 
             
                # +block+.
         | 
| 86 105 | 
             
                #
         | 
| @@ -90,11 +109,13 @@ module PlainRecord | |
| 90 109 | 
             
                #   Post.first(title: 'Post title')
         | 
| 91 110 | 
             
                #   Post.first(title: /^Post/, summary: /cool/)
         | 
| 92 111 | 
             
                #   Post.first { |post| 2 < Post.title.length }
         | 
| 93 | 
            -
                def first(matchers = {}, &block)
         | 
| 112 | 
            +
                def first(matchers = { }, &block)
         | 
| 94 113 | 
             
                  if matchers and block_given?
         | 
| 95 | 
            -
                    each_entry | 
| 114 | 
            +
                    each_entry(matchers) do |i|
         | 
| 115 | 
            +
                      return i if match(i, matchers) and block.call(i)
         | 
| 116 | 
            +
                    end
         | 
| 96 117 | 
             
                  elsif matchers
         | 
| 97 | 
            -
                    each_entry { |i| return i if match(i, matchers) }
         | 
| 118 | 
            +
                    each_entry(matchers) { |i| return i if match(i, matchers) }
         | 
| 98 119 | 
             
                  elsif block_given?
         | 
| 99 120 | 
             
                    each_entry { |i| return i if block.call(i) }
         | 
| 100 121 | 
             
                  else
         | 
| @@ -102,29 +123,37 @@ module PlainRecord | |
| 102 123 | 
             
                  end
         | 
| 103 124 | 
             
                  nil
         | 
| 104 125 | 
             
                end
         | 
| 105 | 
            -
             | 
| 106 | 
            -
                # Return all  | 
| 107 | 
            -
                def files
         | 
| 108 | 
            -
                  Dir.glob( | 
| 126 | 
            +
             | 
| 127 | 
            +
                # Return all file list for models, which match for +matchers+.
         | 
| 128 | 
            +
                def files(matchers = { })
         | 
| 129 | 
            +
                  Dir.glob(PlainRecord.root(path(matchers)))
         | 
| 130 | 
            +
                end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                # Return glob pattern to for files with entris, which is may be match for
         | 
| 133 | 
            +
                # +matchers+.
         | 
| 134 | 
            +
                def path(matchers = { })
         | 
| 135 | 
            +
                  use_callbacks(:path, matchers) do
         | 
| 136 | 
            +
                    @path
         | 
| 137 | 
            +
                  end
         | 
| 109 138 | 
             
                end
         | 
| 110 | 
            -
             | 
| 139 | 
            +
             | 
| 111 140 | 
             
                private
         | 
| 112 | 
            -
             | 
| 113 | 
            -
                # Return all model entries | 
| 141 | 
            +
             | 
| 142 | 
            +
                # Return all model entries, which is may be match for +matchers+.
         | 
| 114 143 | 
             
                #
         | 
| 115 144 | 
             
                # See method code in <tt>Model::Entry</tt> or <tt>Model::List</tt>.
         | 
| 116 | 
            -
                def all_entries; end
         | 
| 117 | 
            -
             | 
| 145 | 
            +
                def all_entries(matchers); end
         | 
| 146 | 
            +
             | 
| 118 147 | 
             
                # Return string representation of +entries+ to write it to file.
         | 
| 119 148 | 
             
                #
         | 
| 120 149 | 
             
                # See method code in <tt>Model::Entry</tt> or <tt>Model::List</tt>.
         | 
| 121 150 | 
             
                def entries_string(entries); end
         | 
| 122 | 
            -
             | 
| 151 | 
            +
             | 
| 123 152 | 
             
                # Delete file, cache and empty dirs in path.
         | 
| 124 153 | 
             
                def delete_file(file)
         | 
| 125 154 | 
             
                  File.delete(file)
         | 
| 126 155 | 
             
                  @loaded.delete(file)
         | 
| 127 | 
            -
             | 
| 156 | 
            +
             | 
| 128 157 | 
             
                  path = Pathname(file).dirname
         | 
| 129 158 | 
             
                  root = Pathname(PlainRecord.root)
         | 
| 130 159 | 
             
                  until 2 != path.entries.length or path == root
         | 
| @@ -132,7 +161,7 @@ module PlainRecord | |
| 132 161 | 
             
                    path = path.parent
         | 
| 133 162 | 
             
                  end
         | 
| 134 163 | 
             
                end
         | 
| 135 | 
            -
             | 
| 164 | 
            +
             | 
| 136 165 | 
             
                # Match +object+ by +matchers+ to use in +all+ and +first+ methods.
         | 
| 137 166 | 
             
                def match(object, matchers)
         | 
| 138 167 | 
             
                  matchers.each_pair do |key, matcher|
         | 
| @@ -145,20 +174,19 @@ module PlainRecord | |
| 145 174 | 
             
                  end
         | 
| 146 175 | 
             
                  true
         | 
| 147 176 | 
             
                end
         | 
| 148 | 
            -
             | 
| 177 | 
            +
             | 
| 149 178 | 
             
                # Set glob +pattern+ for files with entry. Each file must contain one entry.
         | 
| 150 179 | 
             
                # To set root for this path use +PlainRecord.root+.
         | 
| 151 180 | 
             
                #
         | 
| 152 181 | 
             
                # Also add methods from <tt>Model::Entry</tt>.
         | 
| 153 182 | 
             
                #
         | 
| 154 | 
            -
                #   entry_in 'content/*/post. | 
| 183 | 
            +
                #   entry_in 'content/*/post.md'
         | 
| 155 184 | 
             
                def entry_in(path)
         | 
| 156 185 | 
             
                  @storage = :entry
         | 
| 157 186 | 
             
                  @path = path
         | 
| 158 187 | 
             
                  self.extend PlainRecord::Model::Entry
         | 
| 159 | 
            -
                  @loaded = {}
         | 
| 160 188 | 
             
                end
         | 
| 161 | 
            -
             | 
| 189 | 
            +
             | 
| 162 190 | 
             
                # Set glob +pattern+ for files with list of entries. Each file may contain
         | 
| 163 191 | 
             
                # several entries, but you may have several files. All data will storage
         | 
| 164 192 | 
             
                # in YAML, so you can’t define +text+.
         | 
| @@ -170,29 +198,55 @@ module PlainRecord | |
| 170 198 | 
             
                  @storage = :list
         | 
| 171 199 | 
             
                  @path = path
         | 
| 172 200 | 
             
                  self.extend PlainRecord::Model::List
         | 
| 173 | 
            -
                  @loaded = {}
         | 
| 174 201 | 
             
                end
         | 
| 175 | 
            -
             | 
| 176 | 
            -
                # Add property  | 
| 202 | 
            +
             | 
| 203 | 
            +
                # Add virtual property with some +name+ to model. It value willn’t be in
         | 
| 204 | 
            +
                # file and will be calculated dynamically.
         | 
| 205 | 
            +
                #
         | 
| 206 | 
            +
                # You _must_ provide your own define logic by +definers+. Definer Proc
         | 
| 207 | 
            +
                # will be call with property name in first argument and may return
         | 
| 208 | 
            +
                # +:accessor+, +:writer+ or +:reader+ this method create standard methods
         | 
| 209 | 
            +
                # to access to property.
         | 
| 210 | 
            +
                #
         | 
| 211 | 
            +
                #   class Post
         | 
| 212 | 
            +
                #     include PlainRecord::Resource
         | 
| 213 | 
            +
                #
         | 
| 214 | 
            +
                #     entry_in 'posts/*/post.md'
         | 
| 215 | 
            +
                #
         | 
| 216 | 
            +
                #     virtual :name, in_filepath(1)
         | 
| 217 | 
            +
                #   end
         | 
| 218 | 
            +
                def virtual(name, *definers)
         | 
| 219 | 
            +
                  @virtuals ||= []
         | 
| 220 | 
            +
                  @virtuals << name
         | 
| 221 | 
            +
             | 
| 222 | 
            +
                  accessors = call_definers(definers, name, :virtual)
         | 
| 223 | 
            +
             | 
| 224 | 
            +
                  if accessors[:reader] or accessors[:writer]
         | 
| 225 | 
            +
                    raise ArgumentError, 'You must provide you own accessors for virtual ' +
         | 
| 226 | 
            +
                                         "property #{name}"
         | 
| 227 | 
            +
                  end
         | 
| 228 | 
            +
                end
         | 
| 229 | 
            +
             | 
| 230 | 
            +
                # Add property with some +name+ to model. It will be stored as YAML.
         | 
| 177 231 | 
             
                #
         | 
| 178 232 | 
             
                # You can provide your own define logic by +definers+. Definer Proc
         | 
| 179 233 | 
             
                # will be call with property name in first argument and may return
         | 
| 180 234 | 
             
                # +:accessor+, +:writer+ or +:reader+ this method create standard methods
         | 
| 181 235 | 
             
                # to access to property.
         | 
| 182 | 
            -
                # | 
| 236 | 
            +
                #
         | 
| 183 237 | 
             
                #   class Post
         | 
| 184 238 | 
             
                #     include PlainRecord::Resource
         | 
| 185 239 | 
             
                #
         | 
| 186 | 
            -
                #     entry_in 'posts/*/post. | 
| 240 | 
            +
                #     entry_in 'posts/*/post.md'
         | 
| 187 241 | 
             
                #
         | 
| 188 242 | 
             
                #     property :title
         | 
| 189 243 | 
             
                #   end
         | 
| 190 244 | 
             
                def property(name, *definers)
         | 
| 191 245 | 
             
                  @properties ||= []
         | 
| 192 246 | 
             
                  @properties << name
         | 
| 193 | 
            -
             | 
| 194 | 
            -
                  accessors = call_definers(definers, name)
         | 
| 195 | 
            -
             | 
| 247 | 
            +
             | 
| 248 | 
            +
                  accessors = call_definers(definers, name, :property)
         | 
| 249 | 
            +
             | 
| 196 250 | 
             
                  if accessors[:reader]
         | 
| 197 251 | 
             
                    class_eval <<-EOS, __FILE__, __LINE__
         | 
| 198 252 | 
             
                      def #{name}
         | 
| @@ -208,7 +262,7 @@ module PlainRecord | |
| 208 262 | 
             
                    EOS
         | 
| 209 263 | 
             
                  end
         | 
| 210 264 | 
             
                end
         | 
| 211 | 
            -
             | 
| 265 | 
            +
             | 
| 212 266 | 
             
                # Add special property with big text (for example, blog entry content). It
         | 
| 213 267 | 
             
                # will stored after 3 dashes (<tt>---</tt>).
         | 
| 214 268 | 
             
                #
         | 
| @@ -223,19 +277,19 @@ module PlainRecord | |
| 223 277 | 
             
                # == Example
         | 
| 224 278 | 
             
                #
         | 
| 225 279 | 
             
                # Model:
         | 
| 226 | 
            -
                # | 
| 280 | 
            +
                #
         | 
| 227 281 | 
             
                #   class Post
         | 
| 228 282 | 
             
                #     include PlainRecord::Resource
         | 
| 229 283 | 
             
                #
         | 
| 230 | 
            -
                #     entry_in 'posts/*/post. | 
| 284 | 
            +
                #     entry_in 'posts/*/post.md'
         | 
| 231 285 | 
             
                #
         | 
| 232 286 | 
             
                #     property :title
         | 
| 233 287 | 
             
                #     text :summary
         | 
| 234 288 | 
             
                #     text :content
         | 
| 235 289 | 
             
                #   end
         | 
| 236 | 
            -
                # | 
| 290 | 
            +
                #
         | 
| 237 291 | 
             
                # File:
         | 
| 238 | 
            -
                # | 
| 292 | 
            +
                #
         | 
| 239 293 | 
             
                #   title: Post title
         | 
| 240 294 | 
             
                #   ---
         | 
| 241 295 | 
             
                #   Post summary
         | 
| @@ -245,13 +299,13 @@ module PlainRecord | |
| 245 299 | 
             
                  if :list == @storage
         | 
| 246 300 | 
             
                    raise ArgumentError, 'Text is supported by only entry_in models'
         | 
| 247 301 | 
             
                  end
         | 
| 248 | 
            -
             | 
| 302 | 
            +
             | 
| 249 303 | 
             
                  @texts ||= []
         | 
| 250 304 | 
             
                  @texts << name
         | 
| 251 305 | 
             
                  number = @texts.length - 1
         | 
| 252 | 
            -
             | 
| 253 | 
            -
                  accessors = call_definers(definers, name)
         | 
| 254 | 
            -
             | 
| 306 | 
            +
             | 
| 307 | 
            +
                  accessors = call_definers(definers, name, :text)
         | 
| 308 | 
            +
             | 
| 255 309 | 
             
                  if accessors[:reader]
         | 
| 256 310 | 
             
                    class_eval <<-EOS, __FILE__, __LINE__
         | 
| 257 311 | 
             
                      def #{name}
         | 
| @@ -267,14 +321,15 @@ module PlainRecord | |
| 267 321 | 
             
                    EOS
         | 
| 268 322 | 
             
                  end
         | 
| 269 323 | 
             
                end
         | 
| 270 | 
            -
             | 
| 271 | 
            -
                # Call +definers+  | 
| 324 | 
            +
             | 
| 325 | 
            +
                # Call +definers+ from +caller+ (<tt>:virtual</tt>, <tt>:property</tt> or
         | 
| 326 | 
            +
                # <tt>:text</tt>) for property with +name+ and return accessors, which will
         | 
| 272 327 | 
             
                # be created as standart by +property+ or +text+ method.
         | 
| 273 | 
            -
                def call_definers(definers, name)
         | 
| 274 | 
            -
                  accessors = {:reader => true, :writer => true}
         | 
| 275 | 
            -
             | 
| 328 | 
            +
                def call_definers(definers, name, caller)
         | 
| 329 | 
            +
                  accessors = { :reader => true, :writer => true }
         | 
| 330 | 
            +
             | 
| 276 331 | 
             
                  definers.each do |definer|
         | 
| 277 | 
            -
                    access = definer.call(name)
         | 
| 332 | 
            +
                    access = definer.call(name, caller)
         | 
| 278 333 | 
             
                    if :writer == access or access.nil?
         | 
| 279 334 | 
             
                      accessors[:reader] = false
         | 
| 280 335 | 
             
                    end
         | 
| @@ -282,7 +337,7 @@ module PlainRecord | |
| 282 337 | 
             
                      accessors[:writer] = false
         | 
| 283 338 | 
             
                    end
         | 
| 284 339 | 
             
                  end
         | 
| 285 | 
            -
             | 
| 340 | 
            +
             | 
| 286 341 | 
             
                  accessors
         | 
| 287 342 | 
             
                end
         | 
| 288 343 | 
             
              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,39 +52,76 @@ 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
         | 
| 54 75 | 
             
                end
         | 
| 55 | 
            -
             | 
| 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
         | 
| 88 | 
            +
                end
         | 
| 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
         | 
| 60 111 | 
             
                end
         | 
| 61 | 
            -
             | 
| 112 | 
            +
             | 
| 62 113 | 
             
                # Delete current entry and it file if there isn’t has any other entries.
         | 
| 63 114 | 
             
                def destroy
         | 
| 64 | 
            -
                  self.class. | 
| 115 | 
            +
                  self.class.use_callbacks(:destroy, self) do
         | 
| 116 | 
            +
                    self.class.delete_entry(@file, self)
         | 
| 117 | 
            +
                  end
         | 
| 65 118 | 
             
                end
         | 
| 66 | 
            -
             | 
| 119 | 
            +
             | 
| 67 120 | 
             
                # Return string of YAML representation of entry +data+.
         | 
| 68 | 
            -
                def to_yaml(opts = {})
         | 
| 121 | 
            +
                def to_yaml(opts = { })
         | 
| 69 122 | 
             
                  @data.to_yaml(opts)
         | 
| 70 123 | 
             
                end
         | 
| 71 | 
            -
             | 
| 124 | 
            +
             | 
| 72 125 | 
             
                # Compare if its properties and texts are equal.
         | 
| 73 126 | 
             
                def eql?(other)
         | 
| 74 127 | 
             
                  return false unless other.kind_of?(self.class)
         | 
    
        data/lib/plain_record/version.rb
    CHANGED
    
    
    
        data/lib/plain_record.rb
    CHANGED
    
    | @@ -20,14 +20,33 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>. | |
| 20 20 | 
             
            require 'pathname'
         | 
| 21 21 | 
             
            require 'yaml'
         | 
| 22 22 |  | 
| 23 | 
            +
            YAML::ENGINE.yamler = 'syck' if defined? YAML::ENGINE
         | 
| 24 | 
            +
             | 
| 23 25 | 
             
            dir = Pathname(__FILE__).dirname.expand_path + 'plain_record'
         | 
| 24 26 | 
             
            require dir + 'version'
         | 
| 27 | 
            +
            require dir + 'callbacks'
         | 
| 28 | 
            +
            require dir + 'filepath'
         | 
| 29 | 
            +
            require dir + 'association_proxy'
         | 
| 30 | 
            +
            require dir + 'associations'
         | 
| 25 31 | 
             
            require dir + 'model'
         | 
| 26 32 | 
             
            require dir + 'resource'
         | 
| 27 33 |  | 
| 28 34 | 
             
            module PlainRecord
         | 
| 29 35 | 
             
              class << self
         | 
| 30 | 
            -
                #  | 
| 31 | 
            -
                 | 
| 36 | 
            +
                # Set new root for Model#entry_in or Model#list_in.
         | 
| 37 | 
            +
                #
         | 
| 38 | 
            +
                # Note, that it add last slash to root path (<tt>/content</tt> will be saved
         | 
| 39 | 
            +
                # as <tt>/content/</tt>).
         | 
| 40 | 
            +
                def root=(value)
         | 
| 41 | 
            +
                  value += File::SEPARATOR if File::SEPARATOR != value[-1..-1]
         | 
| 42 | 
            +
                  @root = value
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                # Return root for Model#entry_in or Model#list_in.
         | 
| 46 | 
            +
                #
         | 
| 47 | 
            +
                # If you set +path+ it will be added to root path.
         | 
| 48 | 
            +
                def root(path = '')
         | 
| 49 | 
            +
                  File.join(@root, path)
         | 
| 50 | 
            +
                end
         | 
| 32 51 | 
             
              end
         | 
| 33 52 | 
             
            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
         |