yacl 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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