crnixon-active_files 0.1.0

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,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
+