yacl 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.
Files changed (49) hide show
  1. data/HISTORY.rdoc +5 -0
  2. data/LICENSE +16 -0
  3. data/Manifest.txt +48 -0
  4. data/README.rdoc +55 -0
  5. data/Rakefile +308 -0
  6. data/example/myapp-simple/bin/myapp +16 -0
  7. data/example/myapp-simple/config/database.yml +8 -0
  8. data/example/myapp-simple/config/host.yml +2 -0
  9. data/example/myapp-simple/config/pipeline.yml +1 -0
  10. data/example/myapp-simple/lib/myapp.rb +53 -0
  11. data/example/myapp/bin/myapp +17 -0
  12. data/example/myapp/bin/myapp-job +10 -0
  13. data/example/myapp/config/database.yml +8 -0
  14. data/example/myapp/config/httpserver.yml +3 -0
  15. data/example/myapp/config/pipeline.yml +1 -0
  16. data/example/myapp/lib/myapp.rb +6 -0
  17. data/example/myapp/lib/myapp/cli.rb +92 -0
  18. data/example/myapp/lib/myapp/defaults.rb +28 -0
  19. data/example/myapp/lib/myapp/job.rb +56 -0
  20. data/lib/yacl.rb +12 -0
  21. data/lib/yacl/define.rb +9 -0
  22. data/lib/yacl/define/cli.rb +7 -0
  23. data/lib/yacl/define/cli/options.rb +97 -0
  24. data/lib/yacl/define/cli/parser.rb +112 -0
  25. data/lib/yacl/define/cli/runner.rb +82 -0
  26. data/lib/yacl/define/defaults.rb +58 -0
  27. data/lib/yacl/define/plan.rb +197 -0
  28. data/lib/yacl/loader.rb +80 -0
  29. data/lib/yacl/loader/env.rb +103 -0
  30. data/lib/yacl/loader/yaml_dir.rb +137 -0
  31. data/lib/yacl/loader/yaml_file.rb +102 -0
  32. data/lib/yacl/properties.rb +144 -0
  33. data/lib/yacl/simple.rb +52 -0
  34. data/spec/data/yaml_dir/database.yml +8 -0
  35. data/spec/data/yaml_dir/httpserver.yml +3 -0
  36. data/spec/define/cli/options_spec.rb +47 -0
  37. data/spec/define/cli/parser_spec.rb +64 -0
  38. data/spec/define/cli/runner_spec.rb +57 -0
  39. data/spec/define/defaults_spec.rb +24 -0
  40. data/spec/define/plan_spec.rb +77 -0
  41. data/spec/loader/env_spec.rb +32 -0
  42. data/spec/loader/yaml_dir_spec.rb +43 -0
  43. data/spec/loader/yaml_file_spec.rb +80 -0
  44. data/spec/loader_spec.rb +16 -0
  45. data/spec/properties_spec.rb +60 -0
  46. data/spec/simple_spec.rb +85 -0
  47. data/spec/spec_helper.rb +31 -0
  48. data/spec/version_spec.rb +8 -0
  49. metadata +207 -0
@@ -0,0 +1,137 @@
1
+ require 'yacl/loader/yaml_file'
2
+ class Yacl::Loader
3
+ # YamlDir uses a directory of yaml files to load a Properties instance.
4
+ # It does so as if the entire directory were one big yaml file.
5
+ #
6
+ # Examples:
7
+ #
8
+ # Given the following directory structure -
9
+ #
10
+ # ./config
11
+ # ├── database.yml
12
+ # ├── httpserver.yml
13
+ # └── pipeline.yml
14
+ #
15
+ # And Creating a YamlDir instance -
16
+ #
17
+ # yd = YamlDir.new( :path => "./config" )
18
+ #
19
+ # Then you will have propertes like these.
20
+ #
21
+ # props = yd.properties
22
+ #
23
+ # props.dataabase.adapter # => postgre
24
+ # props.httpserver.port # => 80
25
+ #
26
+ # You may also create a YamlDir and have it use the propety from a Properites
27
+ # instance to indicate the directory to load from.
28
+ #
29
+ # other_props.config.dir # => ./config
30
+ #
31
+ # yd = YamlDir.new( :properties => other_props, :parameter => 'config.dir' )
32
+ #
33
+ # props = yd.properties
34
+ # props.dataabase.adapter # => postgre
35
+ # props.httpserver.port # => 80
36
+ #
37
+ # This latter approach use what is used when incorparting a commandline option
38
+ # of a configuration directory within a Plan and the loading of the
39
+ # configuration directory after the pipeline is parsed.
40
+ #
41
+ # class MyPlan < ::Yacl::Define::Plan
42
+ # try MyApp::Cli::Parser
43
+ # try Yacl::Loader::YamlDir, :parameter => 'config.dir'
44
+ # end
45
+ #
46
+ # Since this use of YamlDir is part of a Plan, the Plan will pass in the
47
+ # Properties accumulated so far to YamlDir
48
+ #
49
+ class YamlDir < ::Yacl::Loader
50
+ class Error < ::Yacl::Loader::Error; end
51
+
52
+ # Public: Create a new instance of YamlDir
53
+ #
54
+ # opts - A Hash of options
55
+ # :path - The filesystem path to the directory to load
56
+ # :properties - A Properties instance to use in conjunction with
57
+ # :parameter
58
+ # :pararmeter - The parameter to look up within the Properties that
59
+ # passed in with :properites in order to populate
60
+ # :path
61
+ # :scope - Scope the loded Properties by this value and only
62
+ # return that subset from #properties
63
+ #
64
+ def initialize( opt = {} )
65
+ super
66
+ @path = YamlDir.extract_path( options )
67
+ @parameter = YamlDir.mapify_key( options[:parameter] )
68
+
69
+ if (not @path) and (reference_properties and @parameter) then
70
+ value = reference_properties.get( *@parameter )
71
+ @path = Pathname.new( value ) if value
72
+ end
73
+
74
+ @scope = options[:scope]
75
+ YamlDir.validate_directory( @path )
76
+ end
77
+
78
+ # Public: Return the Properites that are described by the this YamlDir
79
+ # instance
80
+ #
81
+ # Returns a Properties instance
82
+ def properties
83
+ YamlDir.validate_and_load_properties( @path, @scope )
84
+ end
85
+
86
+ class << self
87
+
88
+ # Internal: Validate the directory and then load the properties.
89
+ #
90
+ # dirname - The directory from which to load properties
91
+ # scope - Apply the given scope to the loaded properties
92
+ #
93
+ # Returns a Properties instance.
94
+ def validate_and_load_properties( dirname , scope = nil )
95
+ validate_directory( dirname )
96
+ YamlDir.load_properties( dirname, scope )
97
+ end
98
+
99
+ # Internal: Validate the given directory.
100
+ #
101
+ # path - the Pathname to check.
102
+ #
103
+ # 1) make sure it is not nil
104
+ # 2) make sure that it exists
105
+ # 3) make sure that it is a directory
106
+ #
107
+ # Raise an error if any of the above fail
108
+ #
109
+ # Returns nothing.
110
+ def validate_directory( path )
111
+ raise Error, "No directory specified" unless path
112
+ raise Error, "#{path} does not exist" unless path.exist?
113
+ raise Error, "#{path} is not a directory" unless path.directory?
114
+ end
115
+
116
+ # Interal: Load the proprerties scoped by the given scope
117
+ #
118
+ # dirname - The directory from which to load properties
119
+ # scope - Apply the given scope to the loaded properties
120
+ #
121
+ # Returns a Properties instance.
122
+ def load_properties( dirname, scope )
123
+ p = Yacl::Properties.new
124
+ Dir.glob( File.join( dirname, '*.yml') ).each do |config_fname|
125
+ file_properties = Yacl::Loader::YamlFile.new( :path => config_fname ).properties
126
+ namespace = File.basename( config_fname, ".yml" )
127
+ file_properties.each do |k,v|
128
+ args = [ namespace, k ].flatten
129
+ args << v
130
+ p.set( *args )
131
+ end
132
+ end
133
+ return p
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,102 @@
1
+ require 'yaml'
2
+ class Yacl::Loader
3
+ # YamlFile loads a Properites instance from a single yaml file.
4
+ #
5
+ # Examples:
6
+ #
7
+ # yd = YamlFile.new( :filename => "./config/database.yml" )
8
+ # props = yd.properties
9
+ # props.adapter #=> 'postgres'
10
+ #
11
+ # You may also create a YamlFile and have it use the propety from a Properites
12
+ # instance to indicate the directory to load from.
13
+ #
14
+ # other_props.config.file # => ./config/database.yml
15
+ #
16
+ # yd = YamFile.new( :properties => other_props, :parameter => 'config.file' )
17
+ #
18
+ # props = yd.properties
19
+ # props.adapter # => postgre
20
+ #
21
+ class YamlFile < ::Yacl::Loader
22
+ class Error < ::Yacl::Loader::Error; end
23
+
24
+ def initialize( opts = {} )
25
+ super
26
+ @path = YamlFile.extract_path( options )
27
+ @scope = options.fetch( :scope, nil )
28
+ @parameter = YamlFile.mapify_key( options[:parameter] )
29
+
30
+ if (not @path) and (reference_properties and @parameter) then
31
+ @path = Pathname.new( reference_properties.get( *@parameter ) )
32
+ end
33
+
34
+
35
+ YamlFile.validate_file( @path )
36
+ end
37
+
38
+ def properties
39
+ YamlFile.validate_and_load_properties( @path, @scope )
40
+ end
41
+
42
+ class << self
43
+ # Internal: load a Properties from the given filename.
44
+ #
45
+ # filename - The name of the yaml file to load
46
+ # scope - scope the loaded Properties by the given scope
47
+ #
48
+ # Returns a Properties instance.
49
+ def validate_and_load_properties( filename, scope = nil )
50
+ validate_file( filename )
51
+ YamlFile.load_properties( filename, scope )
52
+ end
53
+
54
+ # Internal: Validate that the give Pathname is a valid, readable file
55
+ #
56
+ # path - the Patname we are going to check
57
+ #
58
+ # Validates:
59
+ #
60
+ # 1) that path has a value
61
+ # 1) that the path exists
62
+ # 2) that the path is readable
63
+ #
64
+ # Raises an error if either of the above failes
65
+ def validate_file( path )
66
+ raise Error, "No path specified" unless path
67
+ raise Error, "#{path} does not exist" unless path.exist?
68
+ raise Error, "#{path} is not readable" unless path.readable?
69
+ end
70
+
71
+ # Internal: load a Properties from the given filename.
72
+ #
73
+ # filename - The name of the yaml file to load
74
+ # scope - scope the loaded Properties by the given scope
75
+ #
76
+ # Returns a Properties instance.
77
+ def load_properties( filename, scope )
78
+ loaded = ::YAML.load_file( filename )
79
+ raise Error, "#{filename} does not contain a top level hash" unless loaded.kind_of?( Hash )
80
+
81
+ p = Yacl::Properties.new( loaded )
82
+ return scoped( p, scope, filename )
83
+ end
84
+
85
+ # Internal: scope the given Properties that are loaded from the given
86
+ # filename.
87
+ #
88
+ # p - The Properties to scope by scope
89
+ # scope - The scope to apply to p
90
+ # filename - The filename from which Properties was loaded
91
+ #
92
+ # Raises Error if the scope does not exist
93
+ #
94
+ # Returns a new Properties instance.
95
+ def scoped( p, scope, filename )
96
+ return p unless scope
97
+ raise Error, "#{filename} does not contain a top level scope of '#{scope}'. Options are #{p.scopes.join(", ")}" unless p.has_scope?( scope )
98
+ return p.scoped_by( scope )
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,144 @@
1
+ require 'forwardable'
2
+ require 'set'
3
+ require 'map'
4
+
5
+ module Yacl
6
+ # Properties is a wrapper around keys and values that are part of a
7
+ # Configuration. This is just the keys and the values, it does not tie it to
8
+ # where the data comes from.
9
+ #
10
+ # The properties interface is a limited subset of the Hash interface.
11
+ #
12
+ # Properties may also be accessed via a dotted method call location
13
+ #
14
+ # Example:
15
+ #
16
+ # p = Properties.new( 'my.a' => 'foo', 'my.b' => 'bar' )
17
+ # p.my.a #=> 'foo'
18
+ # p.my.b #=> 'bar'
19
+ # p.my #=> Properties
20
+ #
21
+ #
22
+ class Properties
23
+ extend Forwardable
24
+
25
+ # Internal: returns the list of methods to delegate to the internal Map
26
+ # instance
27
+ #
28
+ # Returns an Array of method names
29
+ def self.delegating_to_map
30
+ [ :set, :get, :fetch, :store, :delete, :clear, :[], :[]=, :has?, :has_key?, :each, :length, :keys, :method_missing ]
31
+ end
32
+
33
+ # Internal: Returns the list of methods that may be delegated to a
34
+ # Properties instance.
35
+ #
36
+ # Returns an Array of method names
37
+ def self.delegatable_methods
38
+ delegating_to_map + [ :scoped_by, :scopes, :has_scope? ]
39
+ end
40
+
41
+ # Internal: Delegate methods to the internal Map instance
42
+ def_delegators :@map, *Properties.delegating_to_map
43
+
44
+ # Internal: used only for merging
45
+ #
46
+ # Returns the internal Map instances
47
+ attr_reader :map
48
+
49
+ # Create a new Properites
50
+ #
51
+ # initial - A Hash or Map that is used as the initial values of the
52
+ # Properites
53
+ #
54
+ def initialize( initial = {} )
55
+ @map = Map.new
56
+ expanded_keys( initial ) do |k,v|
57
+ k << v
58
+ @map.set( *k )
59
+ end
60
+ end
61
+
62
+ # Public: Mereg another Properties instance on top of this one, overwriting
63
+ # any commaon key/values.
64
+ #
65
+ # This is a destructive operation on this Properties instance.
66
+ #
67
+ # other - The Properties instance to lay on top of this one.
68
+ #
69
+ # Returns nothing
70
+ def merge!( other )
71
+ @map.merge!( other.map )
72
+ end
73
+
74
+ # Public: Return a new Properties that is a subset of the properties with the first
75
+ # prefix removed.
76
+ #
77
+ # scope - the scope under which to use
78
+ #
79
+ # Returns a new Properties object
80
+ def scoped_by( *scope )
81
+ scope = scope.length == 1 ? scope.first : scope
82
+ sub_map = @map.get( expand_key( scope ) ) || {}
83
+ Properties.new( sub_map )
84
+ end
85
+
86
+ # Public: List the knownn scops, i.e. top level keys, of the current
87
+ # Properitess.
88
+ #
89
+ # Returns an Array
90
+ def scopes
91
+ scope_names.to_a.sort
92
+ end
93
+
94
+ # Public: Return true or false if the Properites instance has the given
95
+ # scope.
96
+ #
97
+ # Returns true or false.
98
+ def has_scope?( scope )
99
+ scope_names.include?( scope )
100
+ end
101
+
102
+ private
103
+
104
+ # Internal: The list of all scope names
105
+ #
106
+ # Returns an Array of Strings
107
+ def scope_names
108
+ keys.map { |k| k.to_s }
109
+ end
110
+
111
+ # Internal: Expand the hash into an Array including the multi-value key and
112
+ # the value.
113
+ #
114
+ # hash - the Hash to convert to an array of arrays.
115
+ #
116
+ # Yields the Array of the currenty key and the value
117
+ #
118
+ # Returns nothing.
119
+ def expanded_keys( hash )
120
+ hash.each do |k,v|
121
+ ek = expand_key( k )
122
+ yield ek, v unless ek.empty?
123
+ end
124
+ end
125
+
126
+ # Internal: Convert the given key into an Array key suitable for use with
127
+ # Map.set
128
+ #
129
+ # key - the Key
130
+ #
131
+ # Retursn an Array
132
+ def expand_key( key )
133
+ ek = case key
134
+ when Array
135
+ key
136
+ when /\./
137
+ key.split(".")
138
+ else
139
+ [ key ]
140
+ end
141
+ ek.select { |k| k.to_s.strip.length > 0 }
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,52 @@
1
+ module Yacl
2
+ class Simple < ::Yacl::Define::Cli::Runner
3
+ class << self
4
+ # Public: Create a child class of Yacl::Define::Defaults
5
+ #
6
+ # block - an optional block will be evaluated in the context of the
7
+ # created class.
8
+ #
9
+ # The child class is created once, and the block is evaluated once.
10
+ # Further class to this method will result in returning the already
11
+ # defined class.
12
+ def defaults( &block )
13
+ nested_class( 'Defaults', Yacl::Define::Defaults, &block )
14
+ end
15
+
16
+ # Public: Creates a child class of Yacl::Define::Cli::Parser
17
+ #
18
+ # block - an optional block will be evaluated in the context of the
19
+ # created class.
20
+ #
21
+ # The child class is created once, and the block is evaluated once.
22
+ # Further class to this method will result in returning the already
23
+ # defined class.
24
+ def parser( &block )
25
+ nested_class( 'Parser', Yacl::Define::Cli::Parser, &block )
26
+ end
27
+
28
+ # Public: Creates a child class of Yacl::Define::Plan
29
+ #
30
+ # block - an optional block will be evaluated in the context of the
31
+ # created class.
32
+ #
33
+ # The child class is created once, and the block is evaluated once.
34
+ # Further class to this method will result in returning the already
35
+ # defined class.
36
+ def plan( &block )
37
+ nested_class( 'Plan', Yacl::Define::Plan, &block )
38
+ end
39
+
40
+ # Internal: Define a class that is a nested class of the module
41
+ def nested_class( name, parent, &block )
42
+ unless const_defined?( name ) then
43
+ klass = Class.new( parent )
44
+ klass.class_eval( &block ) if block_given?
45
+ const_set( name , klass )
46
+ end
47
+ return const_get( name )
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,8 @@
1
+ adapter: postgres
2
+ host: 127.0.0.1
3
+ username: myusername
4
+ password: mypassword
5
+ subpart:
6
+ foo: bar
7
+ baz: wibble
8
+
@@ -0,0 +1,3 @@
1
+ port: 4321
2
+ bind: tcp://0.0.0.0
3
+ docroot: /usr/local/www/htdocs