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.
- data/History.txt +9 -0
- data/Manifest.txt +19 -0
- data/README.rdoc +81 -0
- data/VERSION.yml +4 -0
- data/lib/active_files.rb +56 -0
- data/lib/active_files/record.rb +159 -0
- data/lib/lib_helper.rb +14 -0
- data/test/test_active_files.rb +120 -0
- data/test/test_helper.rb +7 -0
- metadata +63 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -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
|
data/README.rdoc
ADDED
@@ -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.
|
data/VERSION.yml
ADDED
data/lib/active_files.rb
ADDED
@@ -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
|
data/lib/lib_helper.rb
ADDED
@@ -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
|
data/test/test_helper.rb
ADDED
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
|
+
|