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,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