using_yaml 0.3.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.4
1
+ 1.0.0
@@ -0,0 +1,29 @@
1
+ module UsingYAML
2
+ # Calls add_extensions on all children. Also defines a +save+ method
3
+ # iff this is a top-level UsingYAML object (see below for details).
4
+ def self.add_array_extensions(array, pathname)
5
+ # Here we define a Module to extend the array
6
+ extensions = Module.new do
7
+ # Iterate over the items
8
+ array.each do |item|
9
+ # Recursively continue to extend.
10
+ UsingYAML.add_extensions(item)
11
+ end
12
+
13
+ # Define a save method if a pathname was supplied (only happens
14
+ # on the top-level object - that is, example.foo will have a
15
+ # +save+, but example.foo.bar will not).
16
+ if pathname
17
+ define_method(:save) do
18
+ # Serialise using +to_yaml+
19
+ File.open(pathname, 'w') do |f|
20
+ f.puts self.to_yaml
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ # Load in the extensions for this instance
27
+ array.extend(extensions)
28
+ end
29
+ end
@@ -0,0 +1,57 @@
1
+ module UsingYAML
2
+ # Calls add_extensions on all children. Also defines a +save+ method
3
+ # iff this is a top-level UsingYAML object (see below for details).
4
+ def self.add_hash_extensions(hash, pathname)
5
+ # Here we define a Module to extend the hash
6
+ extensions = Module.new do
7
+ # Recursively continue to extend.
8
+ hash.each_pair do |key, value|
9
+ UsingYAML.add_extensions(value)
10
+ end
11
+
12
+ define_method(:method_missing) do |*args|
13
+ name = args.shift.to_s
14
+
15
+ if args.empty?
16
+ send(:[], name) || UsingYAML.add_extensions(nil)
17
+ elsif args.size == 1 && name =~ /(.+)=/
18
+ # This is an "alias" turning self.key= into self[key]=
19
+ # Also extends the incoming value so that it behaves
20
+ # consistently with the other key-value pairs.
21
+ key = $1
22
+ value = args.first
23
+
24
+ # Save the value in the hashtable
25
+ send(:[]=, key, UsingYAML.add_extensions(value))
26
+
27
+ # Define the new reader (as above)
28
+ new_reader_extension = Module.new do
29
+ define_method(key) do
30
+ send(:[], key) || UsingYAML.add_extensions(nil)
31
+ end
32
+ end
33
+ extend(new_reader_extension)
34
+
35
+ value
36
+ else
37
+ super(name, args)
38
+ end
39
+ end
40
+
41
+ # Define a save method if a pathname was supplied (only happens
42
+ # on the top-level object - that is, example.foo will have a
43
+ # +save+, but example.foo.bar will not).
44
+ if pathname
45
+ define_method(:save) do
46
+ # Serialise using +to_yaml+
47
+ File.open(pathname, 'w') do |f|
48
+ f.puts self.to_yaml
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ # Load in the extensions for this instance
55
+ hash.extend(extensions)
56
+ end
57
+ end
@@ -0,0 +1,33 @@
1
+ module UsingYAML
2
+ def self.add_nilclass_extensions(instance, pathname)
3
+ extensions = Module.new do
4
+ define_method(:method_missing) do
5
+ # Child objects should not have #save
6
+ if respond_to? :save
7
+ add_extensions(nil)
8
+ else
9
+ # One nil is the same as the next :)
10
+ self
11
+ end
12
+ end
13
+
14
+ # Define a save method if a pathname was supplied (only happens
15
+ # on the top-level object - that is, example.foo will have a
16
+ # +save+, but example.foo.bar will not).
17
+ if pathname
18
+ # Being nil translates to "no file", not to "empty file", so we
19
+ # want to actually delete any existing file. This is a semantic
20
+ # difference, but important: there is a huge different between
21
+ # an _empty_ file and a _non-existant_ file. YAML does not
22
+ # reflect this difference, so we do.
23
+ define_method(:save) do
24
+ # If we can't delete it (permissions, ENOENT..), then there
25
+ # ain't much we can do, so just squelch the error.
26
+ FileUtils.rm(pathname) rescue nil
27
+ end
28
+ end
29
+ end
30
+
31
+ instance.extend(extensions)
32
+ end
33
+ end
data/lib/using_yaml.rb CHANGED
@@ -1,23 +1,69 @@
1
1
  require 'yaml'
2
2
  require 'pathname'
3
- require 'using_yaml/open_hash'
4
- require 'using_yaml/patches/array'
5
- require 'using_yaml/patches/hash'
3
+ require 'using_yaml/array'
4
+ require 'using_yaml/hash'
5
+ require 'using_yaml/nilclass'
6
6
 
7
+ # UsingYAML provides convenient class extensions which make it easy to
8
+ # interact with YAML-storage of settings files by defining accessors
9
+ # which allow one to talk to the files as if they were fully
10
+ # implemented classes.
11
+ #
12
+ # Each of these files will be automagically enhanced to provide save
13
+ # and clean methods of talking to your YAML files without needing to
14
+ # worry about checking for nil's, re-serializing or long-winded hash
15
+ # access.
16
+ #
17
+ # This module also provides some static access to make it easy to
18
+ # enable/disable options such as squelching error messages (in
19
+ # production) and tracking where instances should load their files
20
+ # from.
21
+ #
22
+ # See +using_yaml+ for usage information.
7
23
  module UsingYAML
8
24
  class << self
25
+ # Extends the incoming Array/Hash with magic which makes it
26
+ # possible to +save+ and (in the case of Hashes) use methods
27
+ # instead of []-access.
28
+ def add_extensions(object, pathname = nil)
29
+ case object
30
+ when Array
31
+ add_array_extensions(object, pathname)
32
+ when Hash
33
+ add_hash_extensions(object, pathname)
34
+ when NilClass
35
+ add_nilclass_extensions(object, pathname)
36
+ end
37
+
38
+ object
39
+ end
40
+
41
+ # Returns the path where the given class will load from. See
42
+ # +using_yaml_path+ for information on how to override this on a
43
+ # per-instance basis.
9
44
  def path(klass)
10
45
  return @@path[klass] if defined?(@@path)
11
46
  end
12
47
 
48
+ # Sets the the path where the given class will load from. See
49
+ # +using_yaml_path+ for information on how to override this on a
50
+ # per-instance basis.
13
51
  def path=(args)
14
52
  (@@path ||= {})[args.first] = args.last
15
53
  end
16
54
 
55
+ # Because of the "safety" UsingYAML provides, it is easy for typos
56
+ # to creep in, which might result in unexpected nil's. UsingYAML
57
+ # therefore is verbose in warning about these potential errors.
58
+ #
59
+ # This method disables those warnings, which is useful in a
60
+ # production environment or if you are sure you aren't making mistakes.
17
61
  def squelch!
18
62
  @@squelched = true
19
63
  end
20
64
 
65
+ # Returns true if UsingYAML will warn about potential typos (or
66
+ # similar) which might otherwise be masked.
21
67
  def squelched?
22
68
  defined?(@@squelched) && @@squelched
23
69
  end
@@ -28,56 +74,113 @@ module UsingYAML
28
74
  end
29
75
 
30
76
  module ClassMethods
77
+ # Used to configure UsingYAML for a class by defining what files
78
+ # should be loaded and from where.
79
+ #
80
+ # include UsingYAML
81
+ # using_yaml :foo, :bar, :path => "/some/where"
82
+ #
83
+ # +args+ can contain either filenames or a hash which specifices a
84
+ # path which contains the corresponding files.
85
+ #
86
+ # The value of :path must either be a string or Proc (see
87
+ # +using_yaml_path+ for more information on overriding paths).
31
88
  def using_yaml(*args)
89
+ # Include the instance methods which provide accessors and
90
+ # mutators for reading/writing from/to the YAML objects.
32
91
  include InstanceMethods
33
-
92
+
93
+ # Each argument is either a filename or a :path option
34
94
  args.each do |arg|
35
95
  case arg
36
96
  when Symbol, String
37
- filename = arg.to_s
38
- using_yaml_file(filename)
97
+ # Define accessors for this file
98
+ using_yaml_file(arg.to_s)
39
99
  when Hash
40
- arg.each do |key, value|
41
- case key
42
- when :path
43
- UsingYAML.path = [self.inspect, value]
44
- else
45
- filename = key
46
- options = value
47
- using_yaml_file(filename)
48
- end
49
- end
100
+ # Currently only accepts { :path => ... }
101
+ next unless arg.size == 1 && arg.keys.first == :path
102
+
103
+ # Take note of the path
104
+ UsingYAML.path = [self.inspect, arg.values.first]
50
105
  end
51
106
  end
52
107
  end
53
108
 
109
+ # Special attr_accessor for the suppiled +filename+ such that
110
+ # files are intelligently loaded/written to disk.
111
+ #
112
+ # using_yaml_file(:foo) # => attr_accessor(:foo) + some magic
113
+ #
114
+ # If class Example is setup with the above, then:
115
+ #
116
+ # example = Example.new
117
+ # example.foo # => loads from foo.yml
118
+ # example.foo.bar # => equivalent to example.foo['bar']
119
+ # example.foo.save # => serialises to foo.yml
120
+ #
54
121
  def using_yaml_file(filename)
122
+ # Define an reader for filename such that the corresponding
123
+ # YAML file is loaded. Example: using_yaml_file(:foo) will look
124
+ # for foo.yml in the specified path.
55
125
  define_method(filename) do
126
+ # Work out the absolute path for the filename and get a handle
127
+ # on the cachestore for that file.
56
128
  pathname = using_yaml_path.join("#{filename}.yml").expand_path
57
- cache = (@using_yaml_cache ||= {})
58
- data = @using_yaml_cache[pathname]
59
- return data if data
129
+ yaml = (@using_yaml_cache ||= {})[pathname]
60
130
 
131
+ # If the yaml exists in our cache, then we don't need to hit
132
+ # the disk.
133
+ return yaml if yaml
134
+
135
+ # Safe disk read which either reads and parses a YAML object
136
+ # (and caches it against future reads) or graciously ignores
137
+ # the file's existence. Note that an error will appear on
138
+ # stderr to avoid typos (or similar) from causing unexpected
139
+ # behavior. See +UsingYAML.squelch!+ if you wish to hide the
140
+ # error.
61
141
  begin
62
- data = YAML.load_file(pathname).to_ohash(pathname)
63
- @using_yaml_cache[pathname] = data
142
+ @using_yaml_cache[pathname] = UsingYAML.add_extensions(YAML.load_file(pathname), pathname)
64
143
  rescue Exception => e
65
144
  $stderr.puts "(UsingYAML) Could not load #{filename}: #{e.message}" unless UsingYAML.squelched?
66
- nil
145
+ UsingYAML.add_extensions(nil)
67
146
  end
68
147
  end
148
+
149
+ # Define a writer for filename such that the incoming object is
150
+ # treated as a UsingYAML-ized Hash (magical properties). Be
151
+ # aware that the incoming object will not be saved to disk
152
+ # unless you explicitly do so.
153
+ define_method("#{filename}=".to_sym) do |object|
154
+ # Work out the absolute path for the filename and get a handle
155
+ # on the cachestore for that file.
156
+ pathname = using_yaml_path.join("#{filename}.yml").expand_path
157
+ (@using_yaml_cache ||= {})[pathname] = UsingYAML.add_extensions(object, pathname)
158
+ end
69
159
  end
70
160
  end
71
161
 
162
+ # Instance methods which allow us to define where YAML files are
163
+ # read/written from/to.
72
164
  module InstanceMethods
73
- attr_accessor :using_yaml_cache
74
-
165
+ # Reader which determines where to find files according to the
166
+ # following recipe:
167
+ #
168
+ # (1) Load the :path option from +using_yaml+, if set
169
+ # (2) Possibly invoke a Proc (if supplied to step 1)
170
+ # (3) Default to the current location if 1 & 2 failed
171
+ #
172
+ # You can, of course, overrite this method if you wish to supply
173
+ # your own logic.
75
174
  def using_yaml_path
175
+ return @using_yaml_path unless @using_yaml_path.nil?
176
+
76
177
  path = UsingYAML.path(self.class.name)
77
178
  path = path.call(self) if path.is_a? Proc
78
- @using_yaml_path ||= Pathname.new(path || ENV['HOME'])
179
+ @using_yaml_path = Pathname.new(path || '.')
79
180
  end
80
181
 
182
+ # Sets the YAML load path to the given argument by invoking
183
+ # +Pathname.new+ on +path+.
81
184
  def using_yaml_path=(path)
82
185
  @using_yaml_path = path && Pathname.new(path)
83
186
  end
data/spec/spec_helper.rb CHANGED
@@ -21,6 +21,6 @@ def reset_person!
21
21
  end
22
22
 
23
23
  UsingYAML.path = ['Person', nil]
24
- @person.using_yaml_path = nil
25
- @person.using_yaml_cache = nil
24
+ @person.instance_variable_set('@using_yaml_path', nil)
25
+ @person.instance_variable_set('@using_yaml_cache', nil)
26
26
  end
@@ -5,8 +5,8 @@ describe "UsingYAML#paths" do
5
5
  @person = Person.new
6
6
  end
7
7
 
8
- it "should return $HOME for non-existant pathnames" do
9
- @person.using_yaml_path.expand_path.to_s.should == ENV['HOME']
8
+ it "should return pwd for non-existant pathnames" do
9
+ @person.using_yaml_path.expand_path.to_s.should == `pwd`.strip
10
10
  end
11
11
 
12
12
  it "should return path/to/defined for globally set pathnames" do
@@ -1,23 +1,53 @@
1
1
  require File.dirname(__FILE__) + '/../spec_helper'
2
2
 
3
- describe 'UsingYAML#core' do
3
+ describe 'UsingYAML' do
4
4
  before(:each) do
5
5
  @person = Person.new
6
6
  end
7
7
 
8
- it "should return nil for invalid settings files" do
9
- @person.children.should be_nil
8
+ describe 'robustness' do
9
+ it "should return nil for invalid settings files" do
10
+ @person.children.should be_nil
11
+ end
12
+
13
+ it "should gracefully handle nil.nil..." do
14
+ @person.children.invalid.call.should be_nil
15
+ end
16
+ end
17
+
18
+ describe 'hashes' do
19
+ it "should work" do
20
+ YAML.stubs(:load_file).with(anything).returns({ 'foo' => 'bar' })
21
+ @person.children.should == { 'foo' => 'bar' }
22
+ @person.children.foo.should == 'bar'
23
+ end
24
+
25
+ it "should define methods when a hash gets a new key" do
26
+ YAML.stubs(:load_file).with(anything).returns({})
27
+ @person.children.foo = 'bar'
28
+ @person.children.foo.should == 'bar'
29
+ end
30
+
31
+ it "should catchup when a hash gets a new key via []=" do
32
+ YAML.stubs(:load_file).with(anything).returns({})
33
+ @person.children['foo'] = 'bar'
34
+ @person.children.foo.should == 'bar'
35
+ end
10
36
  end
11
37
 
12
- it "should return valid settings files" do
13
- YAML.stubs(:load_file).with(anything).returns({ 'foo' => 'bar' })
14
- @person.children.should == { 'foo' => 'bar' }
15
- @person.children.foo.should == 'bar'
38
+ describe 'arrays' do
39
+ it "should work" do
40
+ YAML.stubs(:load_file).with(anything).returns([ { 'foo' => 'bar' } ])
41
+ @person.children.class.name.should == 'Array'
42
+ @person.children.first.should == { 'foo' => 'bar' }
43
+ end
16
44
  end
17
45
 
18
- it "should work with arrays" do
19
- YAML.stubs(:load_file).with(anything).returns([ { 'foo' => 'bar' } ])
20
- @person.children.class.name.should == 'Array'
21
- @person.children.first.should == { 'foo' => 'bar' }
46
+ describe 'assignments' do
47
+ it "should work" do
48
+ @person.children = [ :example ]
49
+ @person.children.should == [ :example ]
50
+ @person.children.respond_to?(:save).should == true
51
+ end
22
52
  end
23
53
  end
data/using_yaml.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{using_yaml}
8
- s.version = "0.3.4"
8
+ s.version = "1.0.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Marc Bowes"]
12
- s.date = %q{2010-02-15}
12
+ s.date = %q{2010-03-06}
13
13
  s.description = %q{"Load, save and use YAML files as if they were objects"}
14
14
  s.email = %q{marcbowes@gmail.com}
15
15
  s.extra_rdoc_files = [
@@ -24,9 +24,9 @@ Gem::Specification.new do |s|
24
24
  "Rakefile",
25
25
  "VERSION",
26
26
  "lib/using_yaml.rb",
27
- "lib/using_yaml/open_hash.rb",
28
- "lib/using_yaml/patches/array.rb",
29
- "lib/using_yaml/patches/hash.rb",
27
+ "lib/using_yaml/array.rb",
28
+ "lib/using_yaml/hash.rb",
29
+ "lib/using_yaml/nilclass.rb",
30
30
  "spec/spec_helper.rb",
31
31
  "spec/using_yaml/path_spec.rb",
32
32
  "spec/using_yaml/using_yaml_spec.rb",
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: using_yaml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marc Bowes
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-02-15 00:00:00 +02:00
12
+ date: 2010-03-06 00:00:00 -05:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -39,9 +39,9 @@ files:
39
39
  - Rakefile
40
40
  - VERSION
41
41
  - lib/using_yaml.rb
42
- - lib/using_yaml/open_hash.rb
43
- - lib/using_yaml/patches/array.rb
44
- - lib/using_yaml/patches/hash.rb
42
+ - lib/using_yaml/array.rb
43
+ - lib/using_yaml/hash.rb
44
+ - lib/using_yaml/nilclass.rb
45
45
  - spec/spec_helper.rb
46
46
  - spec/using_yaml/path_spec.rb
47
47
  - spec/using_yaml/using_yaml_spec.rb
@@ -1,46 +0,0 @@
1
- module OpenHash
2
- class << self
3
- def from_hash(hash, pathname = nil)
4
- Module.new do
5
- hash.each_pair do |key, value|
6
- define_method(key) do
7
- value.respond_to?(:to_ohash) ? value.to_ohash(value) : value
8
- end
9
-
10
- define_method("#{key}=") do |value|
11
- case value
12
- when Hash
13
- send(:[]=, key, value.to_ohash)
14
- else
15
- send(:[]=, key, value)
16
- end
17
- end
18
- end
19
-
20
- if pathname
21
- define_method(:save) do
22
- File.open(pathname, 'w') do |f|
23
- f.puts self.to_yaml
24
- end
25
- end
26
- end
27
- end
28
- end
29
-
30
- def from_array(array, pathname = nil)
31
- Module.new do
32
- array.each do |e|
33
- e.to_ohash(e)
34
- end
35
-
36
- if pathname
37
- define_method(:save) do
38
- File.open(pathname, 'w') do |f|
39
- f.puts self.to_yaml
40
- end
41
- end
42
- end
43
- end
44
- end
45
- end
46
- end
@@ -1,5 +0,0 @@
1
- class Array
2
- def to_ohash(pathname)
3
- self.extend OpenHash.from_array(self, pathname)
4
- end
5
- end
@@ -1,5 +0,0 @@
1
- class Hash
2
- def to_ohash(pathname)
3
- self.extend OpenHash.from_hash(self, pathname)
4
- end
5
- end