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,82 @@
1
+ module Yacl::Define::Cli
2
+ # Public: The Runner class is to be used by app developers as the entry point
3
+ # for the commandline programs. A class that inherits from Runner is what is
4
+ # placed in a bin/myapp file in the project structure.
5
+ #
6
+ # The Runner is configured with the Plan to use for loading the configuratin
7
+ # properties and when the plan is loaded, the properties are available via the
8
+ # #properties method.
9
+ #
10
+ # Example:
11
+ #
12
+ # class MyApp::Runner < Yacl::Define::Cli::Runner
13
+ # plan MyApp::Plan
14
+ #
15
+ # def run
16
+ # puts properties.database.server
17
+ # end
18
+ # end
19
+ #
20
+ # And in your 'bin/myapp-cli' file you would have:
21
+ #
22
+ # # automatically populate with ARGV and ENV
23
+ # MyApp::Runner.go
24
+ #
25
+ # # or be explicit
26
+ # MyApp::Runner.go( ARGV, ENV )
27
+ #
28
+ class Runner
29
+ class Error < ::Yacl::Error; end
30
+
31
+ # Public: Execute the Runner
32
+ #
33
+ # argv - The commandline args to use (default: ARGV)
34
+ # env - The enviroment hash to use (default: ENV )
35
+ #
36
+ # It is assumed that when this method exits, the program is over.
37
+ #
38
+ # Returns nothing.
39
+ def self.go( argv = ARGV, env = ENV )
40
+ r = self.new( argv, env )
41
+ r.run
42
+ end
43
+
44
+ # Public: Define the Plan associated with this Runner.
45
+ #
46
+ # plan - A class you define that inherits from Yacl::Define::Plan
47
+ #
48
+ # Returns: the plan class if it is set, nil otherwise.
49
+ def self.plan( *args )
50
+ @plan_klass = args.first unless args.empty?
51
+ return @plan_klass
52
+ end
53
+
54
+ # Internal: Initalize the Runner.
55
+ #
56
+ # argv - The commandline args to use (default: ARGV)
57
+ # env - The enviroment hash to use (default: ENV )
58
+ #
59
+ # This method should be avoided by end users, they should call #go instead.
60
+ def initialize( argv = ARGV, env = ENV )
61
+ raise Error, "No plan class specified in #{self.class}" unless self.class.plan
62
+ @plan = plan_klass.new( :argv => argv, :env => env )
63
+ end
64
+
65
+ # Public: Access the Properties instance that is created by the Plan. This
66
+ # is the fully realized Properties of the plan and should be considered
67
+ # read-only.
68
+ #
69
+ # Returns a Properties instance.
70
+ def properties
71
+ @plan.properties
72
+ end
73
+
74
+ # Internal: return the Plan class defined for this Runner class
75
+ #
76
+ # Returns a Plan class
77
+ def plan_klass
78
+ self.class.plan
79
+ end
80
+ end
81
+ end
82
+
@@ -0,0 +1,58 @@
1
+ module Yacl::Define
2
+ # Public: A base class for use in defining your application's default
3
+ # configuration properties.
4
+ #
5
+ # For the API purposes, classes that inherit from Defaults may behave as
6
+ # both Loader and Properties classes.
7
+ #
8
+ # Examples:
9
+ #
10
+ # class MyDefaults < Yacl::Define::Defaults
11
+ # default 'host.name', 'localhost'
12
+ # default 'host.port', 4321
13
+ # end
14
+ #
15
+ # Now you can use an instances of MyDefaults to find your defaults
16
+ #
17
+ # d = MyDefaults.new
18
+ # d.host.name # => 'localhost'
19
+ class Defaults
20
+ class Error< ::Yacl::Error; end
21
+ extend Forwardable
22
+
23
+ # Internal: Create the instance of Properties that will be populated by
24
+ # calls to Properties::default.
25
+ #
26
+ # Returns: Properties
27
+ def self.properties
28
+ @properties ||= ::Yacl::Properties.new
29
+ end
30
+
31
+ # Public: Define a property and its value.
32
+ #
33
+ # name - The String name of the property.
34
+ # value - The obj to be the default value for the given name.
35
+ #
36
+ # Returns nothing.
37
+ def self.default( name, value )
38
+ args = name.to_s.split('.')
39
+ args << value
40
+ properties.set( *args )
41
+ end
42
+
43
+ # Behave as if we are an instance of Properties
44
+ def_delegators :@properties, *Yacl::Properties.delegatable_methods
45
+
46
+ # Internal: Return the Properties so we behave like a Loader
47
+ attr_reader :properties
48
+
49
+ # Internal: Fake out being a Loader.
50
+ #
51
+ # This method is here to implement the LoaderAPI
52
+ #
53
+ # Returns nothing.
54
+ def initialize( opts = {} )
55
+ @properties = self.class.properties
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,197 @@
1
+ module Yacl::Define
2
+ # Public: The Plan is the order an method by which the Properties of your
3
+ # application are loaded and looked up. This is a base class from which you
4
+ # will define your Property lookups.
5
+ #
6
+ # Example:
7
+ #
8
+ # class MyPlan < Yacl::Define::Plan
9
+ # try MyApp::Cli::Parser
10
+ # try Yacl::Loader::Env, :prefix => 'MY_APP'
11
+ # try Yacl::Loader::YamlDir, :parameter => 'config.dir'
12
+ # try MyApp::Defaults
13
+ #
14
+ # on_error do |exception|
15
+ # $stderr.puts "ERROR: #{exception}"
16
+ # $stderr.puts "Try --help for help"
17
+ # exit 1
18
+ # end
19
+ # end
20
+ #
21
+ # This creates a class MyPlan that when utilized by a Runner loads properties
22
+ # in the following cascading order:
23
+ #
24
+ # 1) Commandline is parsed and converted to Properties, these have the highest
25
+ # priority
26
+ # 2) The environemtn variables are used, only those that start with 'MY_APP'
27
+ # as the variable prefix. These have the 2nd higest priority
28
+ # 3) A configuration directory is loaded, using the 'config.dir' parameter
29
+ # that comes from either (1) or (2). Properties found in this config dir
30
+ # hve the 3rd highest priority
31
+ # 4) Built in defaults if the property is not found any any of the previous
32
+ # locations.
33
+ #
34
+ class Plan
35
+ class Error < ::Yacl::Error; end
36
+
37
+ # Internal: An internal class used to encapsulate the individual items of
38
+ # the lookup plan.
39
+ class Item
40
+ # Internal: Create a new Item. Thiis is what is created from a call to
41
+ # Plan.try
42
+ def initialize( klass, options )
43
+ @loader_klass = klass
44
+ @options = options
45
+ end
46
+
47
+ # Internal: load hte properties from the given Loader class
48
+ #
49
+ # Returns a Properties instance
50
+ def load_properties( params )
51
+ opt = @options.merge( params )
52
+ loader = @loader_klass.new( opt )
53
+ loader.properties
54
+ end
55
+ end
56
+
57
+ class << self
58
+ # Public: Add the given Loader child class or Loader duck-type class to the
59
+ # definition of the Plan.
60
+ #
61
+ # klass - A Class that implements the Loader API.
62
+ # options - A Hash that will be passed to klass.new() when the klass is
63
+ # initialized
64
+ #
65
+ # Example:
66
+ #
67
+ # try Yacl::Loader::YamlDir, :parameter => 'config.dir'
68
+ #
69
+ # Returns nothing.
70
+ def try( klass , options = {} )
71
+ items << Item.new( klass, options )
72
+ end
73
+
74
+ # Public: Define a callable to be invoked should an error happen while
75
+ # loading the properties of your application.
76
+ #
77
+ # callable - the Callable to be invoked.
78
+ #
79
+ # Example:
80
+ #
81
+ # on_error( Proc.new{ fail "kaboom!" } )
82
+ #
83
+ # on_error do
84
+ # raise "Kaboom!"
85
+ # end
86
+ #
87
+ # Return the callable if no parameters are given and the callable is
88
+ # defined.
89
+ #
90
+ # Raises an error if no parameters are given and the callable is NOT
91
+ # defined.
92
+ #
93
+ def on_error( callable = nil, &block )
94
+ if callable then
95
+ @on_error = callable
96
+ elsif block_given?
97
+ @on_error = block
98
+ elsif defined? @on_error
99
+ return @on_error
100
+ else
101
+ raise Error, "on_error requires the use of a callable or a block"
102
+ end
103
+ end
104
+
105
+ # Internal: Test to see if this class has an on_error callable defined.
106
+ #
107
+ # Returns true or false if the on_error is defined.
108
+ def has_on_error?
109
+ defined? @on_error
110
+ end
111
+
112
+ # Internal: Return the array of Items that are used in the child class
113
+ # definition.
114
+ #
115
+ # Returns: an Array of Items
116
+ def items
117
+ @items ||= []
118
+ end
119
+ end
120
+
121
+ # Internal: Return the array of Items defined for this class
122
+ #
123
+ # Returns an Array of Items.
124
+ def items
125
+ self.class.items
126
+ end
127
+
128
+ # Public: Return the Properties instance that results from instantiating the
129
+ # Plan.
130
+ #
131
+ # Returns an Properties instance.
132
+ attr_reader :properties
133
+
134
+ # Public: Create a new instance of the Plan. This should be invoked via
135
+ # #super in a child class.
136
+ #
137
+ def initialize( params = {} )
138
+ initial_properties = params[:initial_properties] || Yacl::Properties.new
139
+ @properties = load_properties( initial_properties, params, items )
140
+ rescue Yacl::Error => ye
141
+ if self.class.has_on_error? then
142
+ self.class.on_error.call( ye )
143
+ else
144
+ raise ye
145
+ end
146
+ end
147
+
148
+ #######
149
+ private
150
+ #######
151
+
152
+ # Internal: Create the Proprites from an initial properties, a set of params
153
+ # and the list of Items in the class.
154
+ #
155
+ # initial_properties - a blank Properties, or an initialized Properties
156
+ # instance to use as the highest priority properties
157
+ # params - A Hash that will be passed to each Item when it is
158
+ # loaded.
159
+ # items - An Array of Item instances
160
+ #
161
+ # The properties are loaded in definitiaion order, that is the order they
162
+ # appear in the class definition. The initial properties are recalculated on
163
+ # each iteration as the next Properties may use some information from the
164
+ # previous Properties to load itself.
165
+ #
166
+ # The final Properties that is returned is a merging of all the loaded
167
+ # properties in reverse order from the bottom to the top. This results in
168
+ # the last Item defined with #try being the "bottom" property with the last
169
+ # priority. It may be overwritten by any of the Properties that are loaded
170
+ # from higher in the loader stack
171
+ #
172
+ # Returns a Properties instance.
173
+ def load_properties( initial_properties, params, items )
174
+ loaded_properties = [ initial_properties ]
175
+ items.each do |item|
176
+ properties_so_far = merge_properties( loaded_properties )
177
+ item_params = params.merge( :properties => properties_so_far )
178
+ loaded_properties << item.load_properties( item_params )
179
+ end
180
+ return merge_properties( loaded_properties )
181
+ end
182
+
183
+ # Internal: Merge a list of Properties instances in the reverse order of the
184
+ # Array in which they exist
185
+ #
186
+ # properties_list - an Array of Properties
187
+ #
188
+ # Returns a Properties instances.
189
+ def merge_properties( properties_list )
190
+ props = ::Yacl::Properties.new
191
+ properties_list.reverse.each do |p|
192
+ props.merge!( p )
193
+ end
194
+ return props
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,80 @@
1
+ require 'pathname'
2
+ module Yacl
3
+ # Loader - the base class of all Loaders. It defines the Loader API>
4
+ #
5
+ # The Loader API is:
6
+ #
7
+ # 1) The initializer method takes an Hash. This hash is stored in the
8
+ # @options and is avaialble to all child clasess via the #options
9
+ # method.
10
+ #
11
+ # 2) The #properties method takes no parameters and MUST return a Properties
12
+ # instance
13
+ #
14
+ # 3) If the options passed into the initializer has a :reference_properites
15
+ # key, its value is made available via the #reference_properties method.
16
+ #
17
+ # Loader also provides a couple of utility methods for use by child classes.
18
+ #
19
+ class Loader
20
+ class Error < ::Yacl::Error; end
21
+
22
+ # Internal: The options object this loader is associated with
23
+ attr_reader :options
24
+
25
+ # Create a new instance of the Loader
26
+ #
27
+ # opts - as Hash of options. Well known options are:
28
+ # :properties - A Properties object
29
+ # :path - A directory/file path
30
+ #
31
+ def initialize( opts = {} )
32
+ @options = opts
33
+ end
34
+
35
+ # Internal:
36
+ #
37
+ # Load the properties according to the type of loader it is
38
+ #
39
+ # Returns: Properties
40
+ def properties
41
+ Properties.new
42
+ end
43
+
44
+ # Internal:
45
+ #
46
+ # The properties that are passed in to use should we need them while loading
47
+ #
48
+ # Returns: properties
49
+ def reference_properties
50
+ @options[:properties]
51
+ end
52
+
53
+ # Internal: Return param split on "."
54
+ #
55
+ # This will only be done if param is a String
56
+ #
57
+ # Returns an Array or param
58
+ def self.mapify_key( param )
59
+ return param unless param.kind_of?( String )
60
+ return param.split('.')
61
+ end
62
+
63
+ # Internal: Extract the value from :path and covert it to a Pathname
64
+ # If a :path key is found, then extract it and convert the value to a
65
+ # Pathname
66
+ #
67
+ # options - a Hash
68
+ # key = the key to extract as a path (default: 'path')
69
+ #
70
+ # Returns a Pathname or nil.
71
+ def self.extract_path( options, key = 'path' )
72
+ ps = options[key.to_sym] || options[key.to_s]
73
+ return nil unless ps
74
+ return Pathname.new( ps )
75
+ end
76
+ end
77
+ end
78
+ require 'yacl/loader/env'
79
+ require 'yacl/loader/yaml_file'
80
+ require 'yacl/loader/yaml_dir'
@@ -0,0 +1,103 @@
1
+ class Yacl::Loader
2
+ # Env loads a Configuration object from a hash, generally this would be ENV.
3
+ #
4
+ # The keys in the hash may be filtered by a prefix. For instance if you had
5
+ # keys in the hash of :
6
+ #
7
+ # MY_APP_OPTION_A => 'foo'
8
+ # MY_APP_OPTION_B => 'bar'
9
+ #
10
+ # And you wanted to have those loaded so they were accessible as
11
+ # option.a and option.b you would use Loader::Env like:
12
+ #
13
+ # Example:
14
+ #
15
+ # properties = Yacl::Loader::Env.new( ENV, 'MY_APP' ).properties
16
+ # properties.option.a # => 'foo'
17
+ # properties.option.b # => 'bar'
18
+ #
19
+ class Env < ::Yacl::Loader
20
+ class Error < ::Yacl::Loader::Error; end
21
+
22
+ # Create a new Env instance from the loaded options
23
+ #
24
+ # opts - a hash of options:
25
+ # :env => The environment has to pass in (default: ENV). required.
26
+ # :prefix => the prefix to strip off of each key in the :env hash. required.
27
+ #
28
+ def initialize( opts = {} )
29
+ super
30
+ @env = @options.fetch( :env, ENV )
31
+ @prefix = @options.fetch( :prefix, nil )
32
+ raise Error, "No environment variable prefix is given" unless @prefix
33
+ end
34
+
35
+ # Public: return the Properties that are created from the environment hash.
36
+ #
37
+ # Returns a Properties instance
38
+ def properties
39
+ load_properties( @env, @prefix )
40
+ end
41
+
42
+ private
43
+
44
+ # Internal: given a hash, and a key prefix, convert the hash into a
45
+ # Properties instance.
46
+ #
47
+ # env - a Hash of environment variables
48
+ # prefix - a String of indicated the prefix of keys in env that are to be
49
+ # stripped off
50
+ def load_properties( env, prefix )
51
+ dot_env = convert_to_dotted_keys( env )
52
+ dot_prefix = to_property_path( prefix )
53
+ prefix_only = prefix_keys_only( dot_prefix, dot_env )
54
+ p = Yacl::Properties.new( prefix_only )
55
+ p = p.scoped_by( dot_prefix ) if dot_prefix
56
+ return p
57
+ end
58
+
59
+ # Internal: Return a new Hash that has only those key/values where the key
60
+ # is prefixed with the given prefix item.
61
+ #
62
+ # prefix - a string to compare against the keys of the hash
63
+ # hash - the hash to return a subset from
64
+ #
65
+ # Returns a Hash.
66
+ def prefix_keys_only( prefix, hash)
67
+ only = {}
68
+ hash.each do |k,v|
69
+ only[k] = v if k =~ /#{prefix}/
70
+ end
71
+ return only
72
+ end
73
+
74
+ # Internal: Take the input Hash, convert all the keys to a dotted notation
75
+ # and return the new hash.
76
+ #
77
+ # The returnd hash will have keys that have been converted using
78
+ # #to_property_path
79
+ #
80
+ # hash - the hash to convert
81
+ #
82
+ # Returns a hash.
83
+ def convert_to_dotted_keys( hash )
84
+ converted = {}
85
+ hash.each do |k,v|
86
+ dot_k = to_property_path( k )
87
+ converted[dot_k] = v
88
+ end
89
+ return converted
90
+ end
91
+
92
+ # Internal: Convert the input String to a property style. The String is
93
+ # downcased and all _ are converted to .
94
+ #
95
+ # name - the String to convert
96
+ #
97
+ # Returns the converted String.
98
+ def to_property_path( name )
99
+ return nil unless name
100
+ name.downcase.gsub('_','.')
101
+ end
102
+ end
103
+ end