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,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,2 @@
1
+ port: 4321
2
+ address: 0.0.0.0
@@ -0,0 +1 @@
1
+ dir: /tmp/foo/bar
@@ -0,0 +1,53 @@
1
+ require 'yacl'
2
+ module MyApp
3
+ # This is a simplier version of 'myapp' example where we use a DSL instead of
4
+ # explicit classes. Under hte covers, this creats classes that do the same as
5
+ # the explicit version.
6
+ #
7
+ # For simpler cases where you want to put all of this together this may make
8
+ # more sense.
9
+ class Application < ::Yacl::Simple
10
+
11
+ # This defines a MyApp::Application::Defaults class. This class is also
12
+ # available via MyApp::Application.defaults
13
+ defaults do
14
+ default 'archive.uri' , 'http://archive.example.com'
15
+ default 'messaging.uri' , 'kestrel://messaging1.example.com:2229/'
16
+ default 'messaging.queue', 'pending'
17
+ default 'system' , 'main'
18
+ default 'timelimit' , '10'
19
+ end
20
+
21
+ # This defines a MyApp::Application::Parser class. This class is also
22
+ # available via MyApp::Application.parser
23
+ parser do
24
+ banner "myapp [options]+"
25
+ opt 'pipeline.dir', :long => 'pipeline-dir', :short => 'd', :description => "The pipeline directory we are using", :cast => :string
26
+ opt 'config.dir' , :long => 'config-dir', :short => 'c', :description => "The directory to load configuration from", :cast => :string
27
+ opt 'timelimit' , :long => 'time-limit', :short => 't', :description => "The amount of time to run for", :cast => :integer
28
+ opt 'system' , :long => 'system', :short => 's', :description => "The system setting", :cast => :string
29
+ opt 'debug' , :long => 'debug', :sortt => 'D', :description => "Turn on debugging", :cast => :boolean
30
+ end
31
+
32
+ # This defines a MyApp::Application::Plan class. This class is also
33
+ # available via MyApp::Application.plan
34
+ plan do
35
+ try MyApp::Application.parser
36
+ try Yacl::Loader::Env, :prefix => 'MY_APP'
37
+ try Yacl::Loader::YamlDir, :parameter => 'config.dir'
38
+ try MyApp::Application.defaults
39
+
40
+ on_error do |exception|
41
+ $stderr.puts "ERROR: #{exception}"
42
+ $stderr.puts "Try --help for help"
43
+ exit 1
44
+ end
45
+ end
46
+
47
+ # This is the method that will be run when it is executed on the
48
+ # commandline.
49
+ def run
50
+ puts properties.map.inspect
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # load path munging so the example works from a git checkout, do not do this
4
+ # in your program
5
+ $: << File.expand_path( "../../../../lib", __FILE__ ) # yacl library
6
+ $: << File.expand_path( "../../lib", __FILE__ ) # lib dir of this sample app
7
+ require 'rubygems'
8
+
9
+ #------------------------------------------------------------------------------
10
+ # This would be the top level commandline entry point for your program. Feel
11
+ # free to have multiple of them if that is what your situation requires.
12
+ #------------------------------------------------------------------------------
13
+
14
+ require 'myapp'
15
+ require 'myapp/cli'
16
+
17
+ MyApp::Cli::Runner.go( ARGV, ENV )
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #------------------------------------------------------------------------------
4
+ # This would be a top-level entrypoint into your system that was not commandline
5
+ # based. Think loaded by some other framework. Maybe a Torquebox Job or a
6
+ # Resqueue Job or something that was more static.
7
+ #------------------------------------------------------------------------------
8
+
9
+ require 'myapp'
10
+ MyApp::Job::ForSomething.new( "pipeline.dir" => "/over/there" )
@@ -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
@@ -0,0 +1 @@
1
+ dir: /tmp/foo/bar
@@ -0,0 +1,6 @@
1
+ # Top level namespace of your application
2
+ module MyApp
3
+ VERSION = "1.0.0"
4
+ end
5
+ require 'myapp/defaults'
6
+ require 'myapp/job'
@@ -0,0 +1,92 @@
1
+ require 'myapp/defaults'
2
+ module MyApp
3
+ # In this example I am encapsulating everything that has to do with a
4
+ # commandline program within the Cli module. For this example, there is one
5
+ # and only one commandline program.
6
+ module Cli
7
+
8
+ # The Commandline options for our program. This maps the commandline
9
+ # options, given in the :long and :short style for consistency in
10
+ # commandline functionalyt to their property names which have a dotten
11
+ # notation and make property access consistent throughout the program.
12
+ #
13
+ # These Options are to be used by the Parser class.
14
+ class Options < Yacl::Define::Cli::Options
15
+ opt 'pipeline.dir', :long => 'pipeline-dir', :short => 'd', :description => "The pipeline directory we are using", :cast => :string
16
+ opt 'config.dir' , :long => 'config-dir', :short => 'c', :description => "The directory to load configuration from", :cast => :string
17
+ opt 'timelimit' , :long => 'time-limit', :short => 't', :description => "The amount of time to run for", :cast => :integer
18
+ opt 'system' , :long => 'system', :short => 's', :description => "The system setting", :cast => :string
19
+ opt 'debug' , :long => 'debug', :sortt => 'D', :description => "Turn on debugging", :cast => :boolean
20
+ end
21
+
22
+ # The Parser class. This is kept explicitly separate from the Options class
23
+ # so that, you may, if you like use whatever commadnline parser
24
+ # library/program you whish. By default this library will automatically use
25
+ # trollop and generate a commandline for you from it. If you whish to use
26
+ # another commandline library, you may define your own Parser class.
27
+ #
28
+ # Use Yacl::Define::Cli::Parser as a template for how you would like to do
29
+ # your work. YOu can probably get by with inheriting from
30
+ # Yacl::Define::Cli::Parser and overwriting the #delegate_parser and #parse
31
+ # methods.
32
+ class Parser < Yacl::Define::Cli::Parser
33
+ banner "myapp [options]+"
34
+ options MyApp::Cli::Options
35
+ end
36
+
37
+ # The Plan class, this defines the order of loading of your properties and the
38
+ # order of resolution of when looking up a property. The property Loaders
39
+ # are loaded in the defined order below, and that is their priority. For
40
+ # example the 'config.dir' property may be defiend in the Parser and the
41
+ # Environment, and in this case if it has a value in the Env but not the
42
+ # Parser the Env value will take precidence. If there is a value in the
43
+ # Parser though, that will be the value used.
44
+ #
45
+ # In this case, since we know that this plan has a commandline, we're
46
+ # catching any load errors and printing out to stderr on a failure. If this
47
+ # perhaps was used in a non-commandline situation the on_error block could
48
+ # do something completely different.
49
+ class Plan < Yacl::Define::Plan
50
+
51
+ # 1) parse the commandline and load properties from the mapping of the
52
+ # commandline to the property names.
53
+ try MyApp::Cli::Parser
54
+
55
+ # 2) using the environment and only those environment variables that
56
+ # start with MY_APP load additional properties.
57
+ try Yacl::Loader::Env, :prefix => 'MY_APP'
58
+
59
+ # 3) using the config.dir value, which is probably set via the
60
+ # --config-dir parser option, or via the MY_APP_CONFIG_DIR environment
61
+ # variable laod up some more properties.
62
+ try Yacl::Loader::YamlDir, :parameter => 'config.dir'
63
+
64
+ # 4) if a property is not found in any of the previous locations, then use
65
+ # the defaults. And if it is not in the defaults, then we will return nil
66
+ try MyApp::Defaults
67
+
68
+ # If an error happens while attempting to load the properties the print a
69
+ # message on stderr and exit the program.
70
+ on_error do |exception|
71
+ $stderr.puts "ERROR: #{exception}"
72
+ $stderr.puts "Try --help for help"
73
+ exit 1
74
+ end
75
+ end
76
+
77
+ # The Runner class, this is what is instantiated in the bin script and
78
+ # invoked from the commandline. It uses the Plan and a #run method you
79
+ # define to take actione.
80
+ #
81
+ # In this particular case, it just prints out the properties.
82
+ class Runner < ::Yacl::Define::Cli::Runner
83
+ plan MyApp::Cli::Plan
84
+
85
+ def run
86
+ p = properties
87
+ puts p.map.inspect
88
+ end
89
+ end
90
+
91
+ end
92
+ end
@@ -0,0 +1,28 @@
1
+ require 'yacl'
2
+ module MyApp
3
+ # The core defaults of your system. Not all of your default properties need to
4
+ # be defined. It is your choice what is defined and what isn't. Think of these
5
+ # as the hardcoded defaults that if the property is not overwritte by
6
+ # something else in the system, then it will fall back to this.
7
+ #
8
+ # Sensible Defaults in other words.
9
+ #
10
+ # Possible ways of using these:
11
+ #
12
+ # - Have different Defaults for development/test/production
13
+ # - Use this for things that are hardly ever overwritten
14
+ # - global defaults that are common across many sub-components.
15
+ #
16
+ # It would be perfectly reasonable to have different classes defined for
17
+ # different things. Different commandline applications, or entrypoints into
18
+ # the library etc.
19
+ class Defaults < Yacl::Define::Defaults
20
+ default 'archive.uri' , 'http://archive.example.com'
21
+ default 'messaging.uri' , 'kestrel://messaging1.example.com:2229/'
22
+ default 'messaging.queue', 'pending'
23
+ default 'system' , 'main'
24
+ default 'timelimit' , '10'
25
+ end
26
+ end
27
+
28
+
@@ -0,0 +1,56 @@
1
+ require 'myapp/defaults'
2
+ module MyApp
3
+ # In this case we are assuming that the application is not part of a
4
+ # commandline program and it may be used from some other library and it still
5
+ # needs to load its config from some location.
6
+ #
7
+ # This could be the case of :
8
+ # - Resque or some other background job system
9
+ # - Part of a torquebox configuration
10
+ # - embedded in some other library
11
+ module Job
12
+
13
+ # We have a plan just as we did with the commandline one, but without the
14
+ # commandline parser.
15
+ class Plan < Yacl::Define::Plan
16
+ # First try to load properties from the environment variables that start
17
+ # with MY_PREFIX
18
+ try Yacl::Loader::Env, :prefix => 'MY_APP'
19
+
20
+ # Next we will load from an explicit directory.
21
+ try Yacl::Loader::YamlDir, :path => File.expand_path( "../../../config", __FILE__ )
22
+
23
+ # And finally we will use the built in defaults
24
+ try MyApp::Defaults
25
+
26
+ # On an error we just print to stderr saying that we do not know what is
27
+ # going on. We could re-reise the exception, or just let it naturally
28
+ # percolate up if we wanted something else to handle it.
29
+ on_error do |exception|
30
+ $stderr.puts exception
31
+ $stderr.puts exception.backtrace.join("\n")
32
+ end
33
+ end
34
+
35
+ # In this situation we have a class that needs to use the plan and do
36
+ # something with it. Instead of having a commandline program, this is the
37
+ # clasee that serves as the interface between our code and someone elses
38
+ # code.
39
+ class ForSomething
40
+
41
+ # We will take a hash here so that the external program can explicitly
42
+ # pass in as set of initial properties and have them bee the most highly
43
+ # prioritiezed values in the plan stack.
44
+ #
45
+ # We also explictly use the Plan here, and do not assume that there is a
46
+ # runner.
47
+ def initialize( params )
48
+ p = Yacl::Properties.new( params )
49
+ plan = Job::Plan.new( :initial_properties => p )
50
+ puts plan.properties.map.inspect
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+
@@ -0,0 +1,12 @@
1
+ # Yacl is Your Application Configuration Library.
2
+ #
3
+ # See the README
4
+ module Yacl
5
+ # The Current Version of the library
6
+ VERSION = "1.0.0"
7
+ class Error < ::StandardError; end
8
+ end
9
+ require 'yacl/properties'
10
+ require 'yacl/loader'
11
+ require 'yacl/define'
12
+ require 'yacl/simple'
@@ -0,0 +1,9 @@
1
+ module Yacl
2
+ # Define is the namespace underwhich lives those classes an end user will
3
+ # inherit from to create their own classes.
4
+ module Define
5
+ end
6
+ end
7
+ require 'yacl/define/defaults'
8
+ require 'yacl/define/cli'
9
+ require 'yacl/define/plan'
@@ -0,0 +1,7 @@
1
+ module Yacl::Define
2
+ module Cli
3
+ end
4
+ end
5
+ require 'yacl/define/cli/options'
6
+ require 'yacl/define/cli/parser'
7
+ require 'yacl/define/cli/runner'
@@ -0,0 +1,97 @@
1
+ module Yacl::Define::Cli
2
+
3
+ # Internal: Encapsulation of all the elements pertaining to a single
4
+ # commandline option.
5
+ Option = Struct.new( :property_name, :long, :short, :description, :cast)
6
+
7
+ # Public: Inherit from Cli::Options to define your the mapping between your
8
+ # application properties and commandline switches.
9
+ #
10
+ # The class you create from here is to be used in conjuction with a class you
11
+ # create that is a child class of Cli::Parser.
12
+ #
13
+ # Example:
14
+ #
15
+ # class MyOptions < ::Yacl::Define::Cli::Options
16
+ # opt 'log.level', :long => 'log-level', :short => 'l', :description => "Logging Level", :cast => :string
17
+ # opt 'config.dir',:long => 'config-dir', :short => 'c', :description => "Configuration directory", :cast => :string
18
+ # end
19
+ #
20
+ # o = MyOptions.new( 'log-level' => 'debug', 'config-dir' => '/tmp/foo' )
21
+ # o.properties # => Properties instance with 'log.level' => debug and 'config.dir' => '/tmp/foo'
22
+ #
23
+ class Options < ::Yacl::Loader
24
+
25
+ # Internal: access to the defined list of options
26
+ #
27
+ # Returns the Array of options
28
+ def self.opt_list
29
+ @opt_list ||= []
30
+ end
31
+
32
+ # Public: Declare a property and its corresponding commandline options.
33
+ #
34
+ # name - The property name as a String
35
+ # params - The Hash options to accompany the property name (default: {})
36
+ # :long - The long commandline option. Without leading dashes
37
+ # :short - The short commandline option. Without leading dashes
38
+ # :description - The text description of this commandline option
39
+ # :cast - What to cast the value of the commandline option
40
+ # to. This can be one of: :integer, :float, :string,
41
+ # :boolean
42
+ #
43
+ # Examples:
44
+ #
45
+ # opt 'app.thing.one', :long => 'thing-one', :short => 't', :description => "Thing One", :cast => :string
46
+ #
47
+ # Returns nothing.
48
+ def self.opt(name, params = {} )
49
+ opt_list << Option.new( name, params[:long], params[:short], params[:description ], params[:cast] )
50
+ end
51
+
52
+ # Public: Load the Properties from the options that were passed to the
53
+ # initializer.
54
+ #
55
+ # Returns a Properties instance.
56
+ def properties
57
+ load_properties( @options )
58
+ end
59
+
60
+ # Public: yield each defined Option in this class to the caller
61
+ #
62
+ # Yields the Option instance of the iteration.
63
+ #
64
+ # Returns nothing.
65
+ def each
66
+ opt_list.each do |o|
67
+ yield o
68
+ end
69
+ end
70
+
71
+ # Internal: Return the list of defined options for this Class.
72
+ #
73
+ # Returns an Array of Option instances
74
+ def opt_list
75
+ self.class.opt_list
76
+ end
77
+
78
+ # Internal: convert the given hash of long options into a Properties
79
+ # instance using the defined options.
80
+ #
81
+ # It is assumed that the commandline has already been parsed and that the
82
+ # hash passed in is the long options that were parsed out of the
83
+ # commandline.
84
+ #
85
+ # hash - a Hash of long options and their values.
86
+ #
87
+ # Returns a Properties instance
88
+ def load_properties( hash )
89
+ prop_hash = {}
90
+ opt_list.each do |o|
91
+ next unless hash.has_key?( o.long )
92
+ prop_hash[o.property_name] = hash[o.long]
93
+ end
94
+ return Yacl::Properties.new( prop_hash )
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,112 @@
1
+ require 'trollop'
2
+ module Yacl::Define::Cli
3
+ # Public: Parser is a Loader that uses a Cli::Options class and the Trollop
4
+ # parser to convert commandline options into a Properties instance. It is to
5
+ # be inherited from so you can have your own parser per commandline program in
6
+ # your application suite.
7
+ #
8
+ # Example:
9
+ #
10
+ # class MyOptions < ::Yacl::Define::Cli::Options
11
+ # opt 'log.level', :long => 'log-level', :short => 'l', :description => "Logging Level", :cast => :string
12
+ # opt 'config.dir',:long => 'config-dir', :short => 'c', :description => "Configuration directory", :cast => :string
13
+ # end
14
+ #
15
+ # class MyParser < ::Yacl::Define::Cli::Parser
16
+ # banner "Usage: myapp [options]+"
17
+ # options MyOptions
18
+ # end
19
+ #
20
+ # p = MyParser.new( :argv => ARGV )
21
+ # p.properties #=> a Properties instance
22
+ #
23
+ class Parser < ::Yacl::Loader
24
+
25
+ # Public: Define the options Class that is be used by this Parser, or return
26
+ # the existin Class if it is already defined.
27
+ #
28
+ # klass - A Class that is a child class of Cli::Options.
29
+ #
30
+ # Returns the current options Class.
31
+ def self.options( *args )
32
+ @options_klass ||= args.first unless args.empty?
33
+ return @options_klass
34
+ end
35
+
36
+ # Public: Set or retrieve the banner text.
37
+ #
38
+ # text - A String to be displayed as part of the help text.
39
+ #
40
+ # Returns the value set, or the default value if one is not set.
41
+ def self.banner( *args )
42
+ @banner = args.first unless args.empty?
43
+ @banner ||= "Usage : #{File.basename($0)} [options]+\nOptions:"
44
+ end
45
+
46
+ # Public: Define a commandline option for this Parser.
47
+ # Using 'opt' over options will dynamically create a child class of Options
48
+ # for use by this class
49
+ def self.opt( *args )
50
+ options( Class.new( Yacl::Define::Cli::Options ) )
51
+ options.opt( *args )
52
+ end
53
+
54
+ # Create a new Parser instance
55
+ #
56
+ # opts - The hash of options for this Loader
57
+ # :argv - The commandline options passed to the Parser.
58
+ # other options as per the Loader class.
59
+ #
60
+ def initialize( opts = {} )
61
+ super
62
+ @argv = opts[:argv] || []
63
+ end
64
+
65
+ # Public: Retrive the class level banner text.
66
+ #
67
+ # Returns the String banner text
68
+ def banner
69
+ self.class.banner
70
+ end
71
+
72
+ # Public: Return the Properties instance that is created by parsing the
73
+ # commandline options.
74
+ #
75
+ # Nil values from possible defaults from the commandline parser are
76
+ # filtered out before being merged onto the options.
77
+ #
78
+ # Returns a Properties instance.
79
+ def properties
80
+ h = {}
81
+ parse( @argv ).each do |k,v|
82
+ h[k] = v unless v.nil?
83
+ end
84
+ o = options_klass.new( h )
85
+ o.properties
86
+ end
87
+
88
+ private
89
+
90
+ # Internal: Return the class level value stored in Parser#options
91
+ #
92
+ # Returns a Class
93
+ def options_klass
94
+ self.class.options
95
+ end
96
+
97
+ def delegate_parser
98
+ Trollop::Parser.new( options_klass.new, self.banner ) do |trollop_options, banner_text|
99
+ text banner_text
100
+ trollop_options.each do |x|
101
+ opt x.long, x.description, :type => x.cast
102
+ end
103
+ end
104
+ end
105
+
106
+ def parse( argv )
107
+ Trollop::with_standard_exception_handling( delegate_parser ) do
108
+ delegate_parser.parse( argv )
109
+ end
110
+ end
111
+ end
112
+ end