crnixon-active_files 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ == 0.2.0 / 2008-06-06
2
+ * 1 major enhancement
3
+ * Moved from inheritance and ostruct limitation to inclusion
4
+
5
+ == 0.1.0 / 2004-02-01
6
+
7
+ * 1 major enhancement
8
+ * Created
9
+
@@ -0,0 +1,19 @@
1
+ .gitignore
2
+ History.txt
3
+ Manifest.txt
4
+ README.rdoc
5
+ Rakefile
6
+ lib/active_files.rb
7
+ lib/active_files/record.rb
8
+ lib/lib_helper.rb
9
+ tasks/bones.rake
10
+ tasks/gem.rake
11
+ tasks/manifest.rake
12
+ tasks/notes.rake
13
+ tasks/post_load.rake
14
+ tasks/rdoc.rake
15
+ tasks/rubyforge.rake
16
+ tasks/setup.rb
17
+ tasks/test.rake
18
+ test/test_active_files.rb
19
+ test/test_helper.rb
@@ -0,0 +1,81 @@
1
+ = ActiveFiles
2
+
3
+ by Clinton R. Nixon
4
+
5
+ URL: http://github.com/crnixon/active_files
6
+
7
+ == Description
8
+
9
+ This library attempts to implement an ActiveRecord-like interface to
10
+ a directory structure of flat files containing serialized objects,
11
+ probably in YAML.
12
+
13
+ == Features/problems
14
+
15
+ * Serializes any object
16
+ * No validations yet
17
+
18
+ == Requirements
19
+
20
+ To run tests, you need the spect and Shoulda gems.
21
+
22
+ == Synopsis
23
+
24
+ ActiveFiles.base_dir = '/tmp/af'
25
+
26
+ class Person < Hash
27
+ include ActiveFiles::Record
28
+ add_file_id_to_initialize
29
+ end
30
+
31
+ class Wingdom
32
+ include ActiveFiles::Record
33
+
34
+ attr_reader :feathers, :color
35
+
36
+ def initialize(id, feathers, color)
37
+ self.file_id = id
38
+ @feathers = feathers
39
+ @color = color
40
+ end
41
+
42
+ def to_activefile
43
+ { :plumoj => @feathers.to_s.split(//).reverse,
44
+ :koloro => [@color, @color, @color].join('\/') }.to_yaml.reverse
45
+ end
46
+
47
+ def self.from_activefile(yaml, file_id)
48
+ hash = YAML::load(yaml.reverse)
49
+ feathers = hash[:plumoj].reverse.inject("") { |s, c| s << c }.to_i
50
+ color = hash[:koloro].split('\/')[1]
51
+ Wingdom.new(file_id, feathers, color)
52
+ end
53
+ end
54
+
55
+ herbert = Person.new('herbert')
56
+ herbert['address'] = '103 Choctaw Dr'
57
+ herbert['birthday'] = Date.parse('2/4/1979')
58
+ herbert.save
59
+
60
+ also_herbert = Person.find("herbert")
61
+ also_herbert = Person.find(:first, "herb*")
62
+ herbs = Person.find("herb*")
63
+ herbs = Person.find(:all, "herb*")
64
+
65
+ == License
66
+
67
+ (ISC License)
68
+
69
+ Copyright (c) 2008 Clinton R. Nixon of Viget Labs
70
+
71
+ Permission to use, copy, modify, and/or distribute this software for any
72
+ purpose with or without fee is hereby granted, provided that the above
73
+ copyright notice and this permission notice appear in all copies.
74
+
75
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
76
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
77
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
78
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
79
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
80
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
81
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 0
@@ -0,0 +1,56 @@
1
+ # Equivalent to a header guard in C/C++
2
+ # Used to prevent the class/module from being loaded more than once
3
+ unless defined? ActiveFiles
4
+
5
+ require File.dirname(__FILE__) + '/lib_helper'
6
+ require 'fileutils'
7
+ require 'yaml'
8
+
9
+ module ActiveFiles
10
+
11
+ # :stopdoc:
12
+ VERSION = '0.1.0'
13
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
14
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
15
+ # :startdoc:
16
+
17
+ def self.version
18
+ VERSION
19
+ end
20
+
21
+ @@base_dir = nil
22
+ @@ext = '.yaml'
23
+
24
+ # This should definitely be used right off the bat, like so:
25
+ #
26
+ # <tt>ActiveFiles.base_dir = '/dir/where/I/store/files'</tt>
27
+ #
28
+ # This is where all your ActiveFiles files will be loaded from
29
+ # and saved to.
30
+ def self.base_dir=(dir)
31
+ FileUtils.mkdir_p dir
32
+ @@base_dir = dir
33
+ end
34
+
35
+ # Accessor! Find out your ActiveFiles directory.
36
+ def self.base_dir
37
+ @@base_dir
38
+ end
39
+
40
+ # Probably never used, but you can change the default
41
+ # ActiveFiles extension, which is ".yaml".
42
+ def self.ext=(ext)
43
+ @@ext = ext
44
+ end
45
+
46
+ # Accessor! Get your ActiveFiles file extension.
47
+ def self.ext
48
+ @@ext
49
+ end
50
+
51
+ end # module ActiveFiles
52
+
53
+ LibHelper.require_all_libs_relative_to __FILE__
54
+
55
+ end # unless defined?
56
+
@@ -0,0 +1,159 @@
1
+ class ActiveFiles::FileNotFound < Exception; end
2
+ class ActiveFiles::NoFileId < Exception; end
3
+
4
+ module ActiveFiles::Record
5
+ def self.included(base)
6
+ base.module_eval do
7
+ extend ActiveFiles::Record::ClassMethods
8
+ end
9
+ end
10
+
11
+ module ClassMethods
12
+ # Adds a parameter to the beginning of initialize to accept a file_id.
13
+ # If you run this, you must run it after your own initialize (or not have
14
+ # an initialize at all.)
15
+ #
16
+ # Feel free to take care of file_id yourself somehow.
17
+ def add_file_id_to_initialize
18
+ self.module_eval do
19
+ def initialize_with_file_id(file_id, *args)
20
+ self.file_id = file_id
21
+ initialize_without_file_id(*args)
22
+ end
23
+ alias :initialize_without_file_id :initialize
24
+ alias :initialize :initialize_with_file_id
25
+ end
26
+ end
27
+
28
+ # Find. Tries way too hard to replicate ActiveRecord.
29
+ #
30
+ # Find operates with three different retrieval mechanisms.
31
+ #
32
+ # * Find by name: Enter a name, or a glob. If one record
33
+ # can be found matching that name, then only one is
34
+ # returned. If more than one can be found, an array
35
+ # is returned. If no record can be found,
36
+ # ActiveFiles::FileNotFound is thrown.
37
+ #
38
+ # * Find first (<tt>:first</tt>): This will return the
39
+ # first record matched by the options used. If no
40
+ # record can matched, nil is returned.
41
+ #
42
+ # * Find all (<tt>:all</tt>: This will return all the
43
+ # records matched by the options used. If no records
44
+ # are found, an empty array is returned.
45
+ #
46
+ # The last two approached accept an option hash as their
47
+ # last parameter. The options are:
48
+ # * <tt>:name => name</tt>:: Glob-based record name.
49
+ def find(*args)
50
+ name = '*'
51
+ order = nil
52
+
53
+ if (args.length == 1 and args[0].kind_of?(String)) then
54
+ name = args[0]
55
+ mode = :name
56
+ elsif (args.first == :first or args.first == :all)
57
+ mode = args.first
58
+ options = extract_find_options!(args)
59
+ name = options[:name] if options.has_key?(:name)
60
+ order = options[:order] if options.has_key?(:order)
61
+ else
62
+ raise ArgumentError, "Unknown mode: #{args.first}"
63
+ end
64
+
65
+ files = Dir[File.join(self.file_store, name + ActiveFiles.ext)]
66
+
67
+ if files.empty? then
68
+ case mode
69
+ when :name
70
+ raise ActiveFiles::FileNotFound
71
+ when :first
72
+ return nil
73
+ when :all
74
+ return Array.new()
75
+ end
76
+ elsif (mode == :first or (mode == :name and files.length == 1)) then
77
+ return self.from_activefile(File.read(files.first), self.parse_file_id(files.first))
78
+ else
79
+ objs = Array.new()
80
+ files.each do |file|
81
+ objs.push(self.from_activefile(File.read(file), self.parse_file_id(file)))
82
+ end
83
+
84
+ return case order
85
+ when 'asc' then objs.sort { |a,b| a.file_id <=> b.file_id }
86
+ when 'desc' then objs.sort { |a,b| b.file_id <=> a.file_id }
87
+ else objs
88
+ end
89
+ end
90
+ rescue Errno::ENOENT
91
+ self.create_file_store
92
+ self.find(*args)
93
+ end
94
+
95
+ # Directory in which this class's ActiveFiles are stored.
96
+ def file_store
97
+ File.join(ActiveFiles.base_dir, self.to_s)
98
+ end
99
+
100
+ def create_file_store
101
+ FileUtils.mkdir_p(file_store) unless File.exists?(file_store)
102
+ end
103
+
104
+ protected
105
+
106
+ def extract_find_options!(args)
107
+ options = args.last.is_a?(Hash) ? args.pop : {}
108
+ unknown_options = options.keys - [:name, :order].flatten
109
+ raise(ArgumentError, "Unknown key(s): #{unknown_options.join(", ")}") unless unknown_options.empty?
110
+ options
111
+ end
112
+
113
+ def parse_file_id(file)
114
+ File.basename(file, ActiveFiles.ext)
115
+ end
116
+
117
+ def from_activefile(yaml, file_id)
118
+ obj = YAML::load(yaml)
119
+ if obj.respond_to?(:file_id=)
120
+ obj.send(:file_id=, file_id)
121
+ end
122
+ obj
123
+ end
124
+ end
125
+
126
+ def save
127
+ raise ActiveFiles::NoFileId if self.file_id.nil?
128
+ File.open(filename, 'w') do |file|
129
+ file.puts self.to_activefile
130
+ end
131
+ true
132
+ rescue Errno::ENOENT
133
+ self.class.create_file_store
134
+ self.save
135
+ end
136
+
137
+ def delete
138
+ File.delete(filename)
139
+ end
140
+
141
+ def file_id
142
+ @file_id
143
+ end
144
+
145
+ protected
146
+
147
+ def to_activefile
148
+ self.to_yaml
149
+ end
150
+
151
+ def filename
152
+ File.join(self.class.file_store, self.file_id + ActiveFiles.ext)
153
+ end
154
+
155
+ def file_id=(id)
156
+ @file_id = id
157
+ end
158
+
159
+ end
@@ -0,0 +1,14 @@
1
+ module LibHelper
2
+ # Utility method used to rquire all files ending in .rb that lie in the
3
+ # directory below this file that has the same name as the filename passed
4
+ # in. Optionally, a specific _directory_ name can be passed in such that
5
+ # the _filename_ does not have to be equivalent to the directory.
6
+ #
7
+ def self.require_all_libs_relative_to( fname, dir = nil )
8
+ dir ||= ::File.basename(fname, '.*')
9
+ search_me = ::File.expand_path(
10
+ ::File.join(::File.dirname(fname), dir, '**', '*.rb'))
11
+
12
+ Dir.glob(search_me).sort.each {|rb| require rb}
13
+ end
14
+ end
@@ -0,0 +1,120 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class Person < Hash
4
+ include ActiveFiles::Record
5
+ add_file_id_to_initialize
6
+ end
7
+
8
+ class Wingdom
9
+ include ActiveFiles::Record
10
+
11
+ attr_reader :feathers, :color
12
+
13
+ def initialize(id, feathers, color)
14
+ self.file_id = id
15
+ @feathers = feathers
16
+ @color = color
17
+ end
18
+
19
+ def to_activefile
20
+ { :plumoj => @feathers.to_s.split(//).reverse,
21
+ :koloro => [@color, @color, @color].join('\/') }.to_yaml.reverse
22
+ end
23
+
24
+ def self.from_activefile(yaml, file_id)
25
+ hash = YAML::load(yaml.reverse)
26
+ feathers = hash[:plumoj].reverse.inject("") { |s, c| s << c }.to_i
27
+ color = hash[:koloro].split('\/')[1]
28
+ Wingdom.new(file_id, feathers, color)
29
+ end
30
+ end
31
+
32
+ class TestActiveFiles < Test::Unit::TestCase
33
+
34
+ context "with a base directory" do
35
+ setup do
36
+ dirname = File.join(File.dirname(__FILE__), 'sample_data')
37
+ ActiveFiles.base_dir = dirname
38
+ end
39
+
40
+ teardown do
41
+ FileUtils.rm_rf(File.join(File.dirname(__FILE__), 'sample_data'))
42
+ end
43
+
44
+ should "be able to save a new object" do
45
+ p = Person.new('test')
46
+ expect(p.save).to.be true
47
+ expect(File.exist?(p.send(:filename))).to.be true
48
+ end
49
+
50
+ should "be able to save and reload an insane object" do
51
+ w = Wingdom.new('cardinal', 1372, 'red')
52
+ w.save
53
+
54
+ w = Wingdom.find('cardinal')
55
+ expect(w.feathers).to.be 1372
56
+ expect(w.color).to.be 'red'
57
+ end
58
+
59
+ context "with an existing object" do
60
+ setup do
61
+ @herbert = Person.new('Herbert')
62
+ @herbert[:birthday] = Date.parse('February 4, 1979')
63
+ @herbert.save
64
+ end
65
+
66
+ should "be able to find object" do
67
+ p = Person.find('Herbert')
68
+ expect(p).not.to.be.nil
69
+ expect(p).is_a Person
70
+ expect(p[:birthday]).to.equal Date.parse('2/4/1979')
71
+ end
72
+
73
+ should "be able to delete object" do
74
+ @herbert.delete
75
+ expect(File.exist?(@herbert.send(:filename))).not.to.be true
76
+ end
77
+ end
78
+
79
+ context "with more than one existing object" do
80
+ setup do
81
+ 3.times do |i|
82
+ Person.new("person_#{i}").save
83
+ end
84
+ end
85
+
86
+ should "be able to find many by globbing" do
87
+ expect(Person.find('person*').size).to.be 3
88
+ end
89
+
90
+ should "be able to find first by globbing" do
91
+ person = Person.find(:first, 'person*')
92
+ expect(person).is.kind_of Person
93
+ expect(person.file_id).to.be 'person_1'
94
+ end
95
+ end
96
+
97
+ context "with no matching elements" do
98
+ setup do
99
+ Person.find(:all, "b*").each do |person|
100
+ person.delete
101
+ end
102
+ end
103
+
104
+ should "raise an error when simple finding" do
105
+ expect(ActiveFiles::FileNotFound).to.be.raised_by do
106
+ Person.find('bill')
107
+ end
108
+ end
109
+
110
+ should "return nil when explicitly finding first" do
111
+ expect(Person.find(:first, 'bill')).to.be.nil
112
+ end
113
+
114
+ should "return an empty array when explicitly finding all" do
115
+ expect(Person.find(:all, 'b*')).to.be.empty
116
+ end
117
+ end
118
+ end
119
+
120
+ end
@@ -0,0 +1,7 @@
1
+ require 'test/unit'
2
+ require 'rubygems'
3
+ require 'shoulda'
4
+ require 'spect'
5
+ require 'mocha'
6
+
7
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'active_files')
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: crnixon-active_files
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Clinton R. Nixon
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-03-10 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: TODO
17
+ email: crnixon@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.rdoc
24
+ files:
25
+ - Manifest.txt
26
+ - README.rdoc
27
+ - VERSION.yml
28
+ - History.txt
29
+ - lib/active_files.rb
30
+ - lib/lib_helper.rb
31
+ - lib/active_files
32
+ - lib/active_files/record.rb
33
+ - test/test_active_files.rb
34
+ - test/test_helper.rb
35
+ has_rdoc: true
36
+ homepage: http://github.com/crnixon/active_files
37
+ post_install_message:
38
+ rdoc_options:
39
+ - --inline-source
40
+ - --charset=UTF-8
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: "0"
48
+ version:
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ requirements: []
56
+
57
+ rubyforge_project:
58
+ rubygems_version: 1.2.0
59
+ signing_key:
60
+ specification_version: 2
61
+ summary: A file store for arbitrary objects, all easy-peasy.
62
+ test_files: []
63
+