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