using_yaml 0.3.4 → 1.0.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/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