rubikon 0.1.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.
- data/LICENSE +25 -0
- data/README.md +129 -0
- data/Rakefile +55 -0
- data/VERSION.yml +4 -0
- data/lib/rubikon/action.rb +71 -0
- data/lib/rubikon/application.rb +331 -0
- data/lib/rubikon/exceptions.rb +28 -0
- data/lib/rubikon.rb +9 -0
- data/test/test.rb +173 -0
- data/test/testapp.rb +64 -0
- metadata +70 -0
data/LICENSE
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
Copyright (c) 2009, Sebastian Staudt
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without modification,
|
5
|
+
are permitted provided that the following conditions are met:
|
6
|
+
|
7
|
+
* Redistributions of source code must retain the above copyright notice,
|
8
|
+
this list of conditions and the following disclaimer.
|
9
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
11
|
+
and/or other materials provided with the distribution.
|
12
|
+
* Neither the name of the author nor the names of its contributors
|
13
|
+
may be used to endorse or promote products derived from this software
|
14
|
+
without specific prior written permission.
|
15
|
+
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
17
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
18
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
19
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
20
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
21
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
22
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
23
|
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
24
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
25
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
Rubikon
|
2
|
+
=======
|
3
|
+
|
4
|
+
Rubikon is a simple to use, yet powerful Ruby framework for building
|
5
|
+
console-based applications.
|
6
|
+
|
7
|
+
### Installation
|
8
|
+
|
9
|
+
You can install Rubikon using RubyGem. This is the easiest way of installing
|
10
|
+
and recommended for most users.
|
11
|
+
|
12
|
+
$ gem install rubikon
|
13
|
+
|
14
|
+
If you want to use the development code you should clone the Git repository:
|
15
|
+
|
16
|
+
$ git clone git://github.com/koraktor/rubikon.git
|
17
|
+
$ cd rubikon
|
18
|
+
$ rake install
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
Creating a Rubikon application is as simple as creating a Ruby class:
|
23
|
+
|
24
|
+
require 'rubygems'
|
25
|
+
require 'rubikon'
|
26
|
+
|
27
|
+
class MyApplication < Rubikon::Application
|
28
|
+
end
|
29
|
+
|
30
|
+
If you save this code in a file called `myapp.rb` you can run it using
|
31
|
+
`ruby myapp.rb`. Or you could even add a *shebang* (`#!/usr/bin/env ruby`) to
|
32
|
+
the top of the file and make it executable. You would then be able to run it
|
33
|
+
even more easy by typing `./myapp.rb`.
|
34
|
+
|
35
|
+
Now go on and define what your application should do when the user runs it.
|
36
|
+
This is done using `default`:
|
37
|
+
|
38
|
+
class MyApplication < Rubikon::Application
|
39
|
+
|
40
|
+
default do
|
41
|
+
puts 'Hello World!'
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
If you run this application it will just print `Hello World!`.
|
47
|
+
|
48
|
+
You can also add command-line options to your appication using `action`:
|
49
|
+
|
50
|
+
class MyApplication < Rubikon::Application
|
51
|
+
|
52
|
+
action 'hello' do
|
53
|
+
puts 'Hello World!'
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
This way your application would do nothing when called without options, but it
|
59
|
+
would print `Hello World!` when called using `ruby myapp.rb --hello`.
|
60
|
+
Please note that Rubikon will add dashes to options by default. If you don't
|
61
|
+
like this behaviour and want options like RubyGem's `install` or `update` just
|
62
|
+
use the following inside your application class:
|
63
|
+
|
64
|
+
set :dashed_options, false
|
65
|
+
|
66
|
+
Please see the `samples` directory for more in detail sample applications.
|
67
|
+
|
68
|
+
|
69
|
+
**Warning**:
|
70
|
+
|
71
|
+
Rubikon is still in an early development stage. If you want to use it be aware
|
72
|
+
that you will probably run into problems and or restrictions. See the
|
73
|
+
Contribute section if you want to help making Rubikon better.
|
74
|
+
|
75
|
+
## Features
|
76
|
+
|
77
|
+
* A simple to use DSL
|
78
|
+
* Automatic checks for option arguments
|
79
|
+
* User defined type safety of option arguments
|
80
|
+
* Built-in methods to capture user input and display throbbers
|
81
|
+
|
82
|
+
## Future plans
|
83
|
+
|
84
|
+
* Automatic generation of help screens
|
85
|
+
* Improved error handling
|
86
|
+
* Built-in support for configuration files
|
87
|
+
* Built-in support for colored output and progress bars
|
88
|
+
|
89
|
+
## Requirements
|
90
|
+
|
91
|
+
* Linux, MacOS X or Windows
|
92
|
+
* Ruby 1.8.6 or newer
|
93
|
+
|
94
|
+
## Contribute
|
95
|
+
|
96
|
+
There are several ways of contributing to Rubikon's development:
|
97
|
+
|
98
|
+
* Build apps using it and spread the word.<br />
|
99
|
+
* Report problems and request features using the [issue tracker][2].
|
100
|
+
* Write patches yourself to fix bugs and implement new functionality.
|
101
|
+
* Create a Rubikon fork on [GitHub][1] and start hacking.
|
102
|
+
|
103
|
+
## About the name
|
104
|
+
|
105
|
+
Rubikon is the German name of the river Rubicone in Italy. It had a historical
|
106
|
+
relevance in ancient Rome when Julius Caesar crossed that river with his army
|
107
|
+
and thereby declared war to the Roman senate. The phrase "to cross the Rubicon"
|
108
|
+
originates from this event.
|
109
|
+
|
110
|
+
You may also see Rubikon as a morphed composition of *"Ruby"* and *"console"*.
|
111
|
+
|
112
|
+
## License
|
113
|
+
|
114
|
+
This code is free software; you can redistribute it and/or modify it under the
|
115
|
+
terms of the new BSD License. A copy of this license can be found in the LICENSE
|
116
|
+
file.
|
117
|
+
|
118
|
+
## Credits
|
119
|
+
|
120
|
+
* Sebastian Staudt -- koraktor(at)gmail.com
|
121
|
+
|
122
|
+
## See Also
|
123
|
+
|
124
|
+
* [API documentation](http://www.rdoc.info/projects/koraktor/rubikon)
|
125
|
+
* [GitHub project page][1]
|
126
|
+
* [GitHub issue tracker][2]
|
127
|
+
|
128
|
+
[1]: http://github.com/koraktor/rubikon
|
129
|
+
[2]: http://github.com/koraktor/rubikon/issues
|
data/Rakefile
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# This code is free software; you can redistribute it and/or modify it under the
|
2
|
+
# terms of the new BSD License.
|
3
|
+
#
|
4
|
+
# Copyright (c) 2009, Sebastian Staudt
|
5
|
+
|
6
|
+
require 'rake/rdoctask'
|
7
|
+
require 'rake/testtask'
|
8
|
+
|
9
|
+
src_files = Dir.glob(File.join('lib', '**', '*.rb'))
|
10
|
+
test_files = Dir.glob(File.join('test', '**', '*.rb'))
|
11
|
+
|
12
|
+
task :default => :test
|
13
|
+
|
14
|
+
# Test task
|
15
|
+
Rake::TestTask.new do |t|
|
16
|
+
t.libs << 'lib'
|
17
|
+
t.test_files = test_files
|
18
|
+
t.verbose = true
|
19
|
+
end
|
20
|
+
|
21
|
+
begin
|
22
|
+
require 'jeweler'
|
23
|
+
# Gem specification
|
24
|
+
Jeweler::Tasks.new do |gem|
|
25
|
+
gem.authors = ['Sebastian Staudt']
|
26
|
+
gem.email = 'koraktor@gmail.com'
|
27
|
+
gem.description = 'A simple to use, yet powerful Ruby framework for building console-based applications.'
|
28
|
+
gem.date = Time.now
|
29
|
+
gem.homepage = 'http://koraktor.github.com/rubikon'
|
30
|
+
gem.name = gem.rubyforge_project = 'rubikon'
|
31
|
+
gem.summary = 'Rubikon - A Ruby console app framework'
|
32
|
+
|
33
|
+
gem.files = %w(README.md Rakefile LICENSE VERSION.yml) + src_files + test_files
|
34
|
+
gem.rdoc_options = ['--all', '--inline-source', '--line-numbers', '--charset=utf-8', '--webcvs=http://github.com/koraktor/rubikon/blob/master/%s']
|
35
|
+
end
|
36
|
+
rescue LoadError
|
37
|
+
puts "You need Jeweler to build the gem. Install it using `gem install jeweler`."
|
38
|
+
end
|
39
|
+
|
40
|
+
# Create a rake task +:rdoc+ to build the documentation
|
41
|
+
desc 'Building docs'
|
42
|
+
Rake::RDocTask.new do |rdoc|
|
43
|
+
rdoc.title = 'Rubikon - API documentation'
|
44
|
+
rdoc.rdoc_files.include ['lib/**/*.rb', 'LICENSE', 'README.md']
|
45
|
+
rdoc.main = 'README.md'
|
46
|
+
rdoc.rdoc_dir = 'doc'
|
47
|
+
rdoc.options = ['--all', '--inline-source', '--line-numbers', '--charset=utf-8', '--webcvs=http://github.com/koraktor/rubikon/blob/master/%s']
|
48
|
+
end
|
49
|
+
|
50
|
+
# Task for cleaning documentation and package directories
|
51
|
+
desc 'Clean documentation and package directories'
|
52
|
+
task :clean do
|
53
|
+
FileUtils.rm_rf 'doc'
|
54
|
+
FileUtils.rm_rf 'pkg'
|
55
|
+
end
|
data/VERSION.yml
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# This code is free software; you can redistribute it and/or modify it under the
|
2
|
+
# terms of the new BSD License.
|
3
|
+
#
|
4
|
+
# Copyright (c) 2009, Sebastian Staudt
|
5
|
+
|
6
|
+
module Rubikon
|
7
|
+
|
8
|
+
# Instances of the Action class are used to define the real code that should
|
9
|
+
# be executed when running the application.
|
10
|
+
class Action
|
11
|
+
|
12
|
+
@@action_count = 0
|
13
|
+
|
14
|
+
attr_reader :block, :description, :name, :param_type
|
15
|
+
|
16
|
+
# Create a new Action using the given name, options and code block.
|
17
|
+
#
|
18
|
+
# +name+:: The name of this Action, used in Application options
|
19
|
+
# +options+:: A Hash of options that define more details about the Action
|
20
|
+
# +block+:: The code block which should be executed by this Action
|
21
|
+
#
|
22
|
+
# Options:
|
23
|
+
# +description+:: A description of the action. This isn't used at the
|
24
|
+
# moment.
|
25
|
+
# +param_type+:: A single Class or a Array of classes that represent the
|
26
|
+
# type(s) of argument(s) this action expects
|
27
|
+
def initialize(name, options = {}, &block)
|
28
|
+
raise BlockMissingError unless block_given?
|
29
|
+
|
30
|
+
@name = name
|
31
|
+
|
32
|
+
@description = options[:description] || ''
|
33
|
+
@param_type = options[:param_type] || Object
|
34
|
+
|
35
|
+
@block = block
|
36
|
+
end
|
37
|
+
|
38
|
+
# Run this action's code block
|
39
|
+
#
|
40
|
+
# +args+:: The argument which should be relayed to the block of this Action
|
41
|
+
def run(*args)
|
42
|
+
if (@block.arity >= 0 and args.size < @block.arity) or (@block.arity < 0 and args.size < -@block.arity - 1)
|
43
|
+
raise MissingArgumentError
|
44
|
+
end
|
45
|
+
raise Rubikon::ArgumentTypeError unless check_argument_types(args)
|
46
|
+
@block[*args]
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# Checks the types of the given arguments using the Class or Array of
|
52
|
+
# classes given in the +:param_type+ option of this action.
|
53
|
+
#
|
54
|
+
# +args+:: The arguments which should be checked
|
55
|
+
def check_argument_types(args)
|
56
|
+
if @param_type.is_a? Array
|
57
|
+
args.each_index do |i|
|
58
|
+
return false unless args[i].is_a? @param_type[i]
|
59
|
+
end
|
60
|
+
else
|
61
|
+
args.each do |arg|
|
62
|
+
return false unless arg.is_a? @param_type
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
true
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,331 @@
|
|
1
|
+
# This code is free software; you can redistribute it and/or modify it under the
|
2
|
+
# terms of the new BSD License.
|
3
|
+
#
|
4
|
+
# Copyright (c) 2009, Sebastian Staudt
|
5
|
+
|
6
|
+
require 'singleton'
|
7
|
+
require 'yaml'
|
8
|
+
|
9
|
+
require 'rubikon/action'
|
10
|
+
require 'rubikon/exceptions'
|
11
|
+
|
12
|
+
module Rubikon
|
13
|
+
|
14
|
+
version = YAML.load_file(File.join(File.dirname(__FILE__), '..', '..', 'VERSION.yml'))
|
15
|
+
VERSION = "#{version[:major]}.#{version[:minor]}.#{version[:patch]}"
|
16
|
+
|
17
|
+
# The main class of Rubikon. Let your own application class inherit from this
|
18
|
+
# one.
|
19
|
+
class Application
|
20
|
+
|
21
|
+
include Singleton
|
22
|
+
|
23
|
+
attr_reader :settings
|
24
|
+
|
25
|
+
# Initialize with default settings (see set for more detail)
|
26
|
+
#
|
27
|
+
# If you really need to override this in your application class, be sure to
|
28
|
+
# call +super+
|
29
|
+
def initialize
|
30
|
+
@actions = {}
|
31
|
+
@aliases = {}
|
32
|
+
@default = nil
|
33
|
+
@settings = {
|
34
|
+
:autorun => true,
|
35
|
+
:dashed_options => true,
|
36
|
+
:help_banner => "Usage: #{$0}",
|
37
|
+
:istream => $stdin,
|
38
|
+
:name => self.class.to_s,
|
39
|
+
:ostream => $stdout,
|
40
|
+
:raise_errors => false
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
# Define an Application Action
|
45
|
+
#
|
46
|
+
# +name+:: The name of the action. Used as an option parameter.
|
47
|
+
# +options+:: A Hash of options to be used on the created Action
|
48
|
+
# (default: <tt>{}</tt>)
|
49
|
+
# +block+:: A block containing the code that should be executed when this
|
50
|
+
# Action is called, i.e. when the Application is called with
|
51
|
+
# the associated option parameter
|
52
|
+
def action(name, options = {}, &block)
|
53
|
+
raise "No block given" unless block_given?
|
54
|
+
|
55
|
+
key = name
|
56
|
+
key = "--#{key}" if @settings[:dashed_options]
|
57
|
+
|
58
|
+
@actions[key.to_sym] = Action.new(name, options, &block)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Define an alias to an Action
|
62
|
+
#
|
63
|
+
# +name+:: The name of the alias
|
64
|
+
# +action+:: The name of the Action that should be aliased
|
65
|
+
#
|
66
|
+
# Example:
|
67
|
+
#
|
68
|
+
# action_alias :doit, :dosomething
|
69
|
+
def action_alias(name, action)
|
70
|
+
@aliases[name.to_sym] = action.to_sym
|
71
|
+
end
|
72
|
+
|
73
|
+
# Define the default Action of the Application
|
74
|
+
#
|
75
|
+
# +options+:: A Hash of options to be used on the created Action
|
76
|
+
# (default: <tt>{}</tt>)
|
77
|
+
# +block+:: A block containing the code that should be executed when this
|
78
|
+
# Action is called, i.e. when no option is given to the
|
79
|
+
# Application
|
80
|
+
def default(options = {}, &block)
|
81
|
+
@default = Action.new(:default, options, &block)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Prompts the user for input
|
85
|
+
#
|
86
|
+
# If +prompt+ is not empty this will display a prompt using
|
87
|
+
# <tt>prompt.to_s</tt>.
|
88
|
+
#
|
89
|
+
# +prompt+:: A String or other Object responding to +to_s+ used for
|
90
|
+
# displaying a prompt to the user (default: <tt>''</tt>)
|
91
|
+
#
|
92
|
+
# Example:
|
93
|
+
#
|
94
|
+
# action 'interactive' do
|
95
|
+
# # Display a prompt "Please type something: "
|
96
|
+
# user_provided_value = input 'Please type something'
|
97
|
+
#
|
98
|
+
# # Do something with the data
|
99
|
+
# ...
|
100
|
+
# end
|
101
|
+
def input(prompt = '')
|
102
|
+
unless prompt.to_s.empty?
|
103
|
+
ostream << "#{prompt}: "
|
104
|
+
end
|
105
|
+
@settings[:istream].gets[0..-2]
|
106
|
+
end
|
107
|
+
|
108
|
+
# Convenience method for accessing the user-defined output stream
|
109
|
+
#
|
110
|
+
# Use this if you want to work directly with the output stream
|
111
|
+
#
|
112
|
+
# Example:
|
113
|
+
#
|
114
|
+
# ostream.flush
|
115
|
+
def ostream
|
116
|
+
@settings[:ostream]
|
117
|
+
end
|
118
|
+
|
119
|
+
# Output text using +IO#<<+ of the output stream
|
120
|
+
#
|
121
|
+
# +text+:: The text to write into the output stream
|
122
|
+
def put(text)
|
123
|
+
ostream << text
|
124
|
+
ostream.flush
|
125
|
+
end
|
126
|
+
|
127
|
+
# Output a character using +IO#putc+ of the output stream
|
128
|
+
#
|
129
|
+
# +char+:: The character to write into the output stream
|
130
|
+
def putc(char)
|
131
|
+
ostream.putc char
|
132
|
+
end
|
133
|
+
|
134
|
+
# Output a line of text using +IO#puts+ of the output stream
|
135
|
+
#
|
136
|
+
# +text+:: The text to write into the output stream
|
137
|
+
def puts(text)
|
138
|
+
ostream.puts text
|
139
|
+
end
|
140
|
+
|
141
|
+
# Run this application
|
142
|
+
#
|
143
|
+
# +args+:: The command line arguments that should be given to the
|
144
|
+
# application as options
|
145
|
+
#
|
146
|
+
# Calling this method explicitly is not required when you want to create a
|
147
|
+
# simple application (having one main class inheriting from
|
148
|
+
# Rubikon::Application). But it's useful for testing or if you want to have
|
149
|
+
# some sort of sub-applications.
|
150
|
+
def run(args = ARGV)
|
151
|
+
begin
|
152
|
+
assign_aliases unless @aliases.empty?
|
153
|
+
action_results = []
|
154
|
+
|
155
|
+
if !@default.nil? and args.empty?
|
156
|
+
action_results << @default.run
|
157
|
+
else
|
158
|
+
parse_options(args).each do |action, args|
|
159
|
+
action_results << @actions[action].run(*args)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
rescue
|
163
|
+
if @settings[:raise_errors]
|
164
|
+
raise $!
|
165
|
+
else
|
166
|
+
puts "Error:\n #{$!.message}"
|
167
|
+
puts " #{$!.backtrace.join("\n ")}" if $DEBUG
|
168
|
+
exit 1
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
action_results
|
173
|
+
end
|
174
|
+
|
175
|
+
# Sets an application setting
|
176
|
+
#
|
177
|
+
# +setting+:: The name of the setting to change, will be symbolized first.
|
178
|
+
# +value+:: The value the setting should be changed to
|
179
|
+
#
|
180
|
+
# Available settings
|
181
|
+
# +autorun+:: If true, let the application run as soon as its class
|
182
|
+
# is defined
|
183
|
+
# +dashed_options+:: If true, each option is prepended with a double-dash
|
184
|
+
# (<tt>-</tt><tt>-</tt>)
|
185
|
+
# +help_banner+:: Defines a banner for the help message (<em>unused</em>)
|
186
|
+
# +istream+:: Defines an input stream to use
|
187
|
+
# +name+:: Defines the name of the application
|
188
|
+
# +ostream+:: Defines an output stream to use
|
189
|
+
# +raise_errors+:: If true, raise errors, otherwise fail gracefully
|
190
|
+
#
|
191
|
+
# Example:
|
192
|
+
#
|
193
|
+
# set :name, 'My App'
|
194
|
+
# set :autorun, false
|
195
|
+
def set(setting, value)
|
196
|
+
@settings[setting.to_sym] = value
|
197
|
+
end
|
198
|
+
|
199
|
+
# Displays a throbber while the given block is executed
|
200
|
+
#
|
201
|
+
# Example:
|
202
|
+
#
|
203
|
+
# action 'slow' do
|
204
|
+
# throbber do
|
205
|
+
# # Add some long running code here
|
206
|
+
# ...
|
207
|
+
# end
|
208
|
+
# end
|
209
|
+
def throbber(&block)
|
210
|
+
spinner = '-\|/'
|
211
|
+
current_ostream = ostream
|
212
|
+
@settings[:ostream] = StringIO.new
|
213
|
+
|
214
|
+
code_thread = Thread.new { block.call }
|
215
|
+
|
216
|
+
throbber_thread = Thread.new do
|
217
|
+
i = 0
|
218
|
+
current_ostream.putc 32
|
219
|
+
while code_thread.alive?
|
220
|
+
current_ostream.putc 8
|
221
|
+
current_ostream.putc spinner[i]
|
222
|
+
current_ostream.flush
|
223
|
+
i = (i + 1) % 4
|
224
|
+
sleep 0.25
|
225
|
+
end
|
226
|
+
current_ostream.putc 8
|
227
|
+
end
|
228
|
+
|
229
|
+
code_thread.join
|
230
|
+
throbber_thread.join
|
231
|
+
|
232
|
+
current_ostream << ostream.string
|
233
|
+
@settings[:ostream] = current_ostream
|
234
|
+
end
|
235
|
+
|
236
|
+
private
|
237
|
+
|
238
|
+
# Returns whether this application should be ran automatically
|
239
|
+
def self.autorun?
|
240
|
+
instance.settings[:autorun] || false
|
241
|
+
end
|
242
|
+
|
243
|
+
# Enables autorun functionality using <tt>Kernel#at_exit</tt>
|
244
|
+
#
|
245
|
+
# +subclass+:: The subclass inheriting from Application. This is the user's
|
246
|
+
# application.
|
247
|
+
#
|
248
|
+
# <em>This is called automatically when subclassing Application.</em>
|
249
|
+
def self.inherited(subclass)
|
250
|
+
Singleton.__init__(subclass)
|
251
|
+
at_exit { subclass.run if subclass.autorun? }
|
252
|
+
end
|
253
|
+
|
254
|
+
# This is used for convinience. Method calls on the class itself are
|
255
|
+
# relayed to the singleton instance.
|
256
|
+
#
|
257
|
+
# +method_name+:: The name of the method being called
|
258
|
+
# +args+:: Any arguments that are given to the method
|
259
|
+
# +block+:: A block that may be given to the method
|
260
|
+
#
|
261
|
+
# <em>This is called automatically when calling methods on the class.</em>
|
262
|
+
def self.method_missing(method_name, *args, &block)
|
263
|
+
instance.send(method_name, *args, &block)
|
264
|
+
end
|
265
|
+
|
266
|
+
# Relay putc to the instance method
|
267
|
+
#
|
268
|
+
# This is used to hide <tt>Kernel#putc</tt> so that the Application's
|
269
|
+
# output IO object is used for printing text
|
270
|
+
#
|
271
|
+
# +text+:: The text to write into the output stream
|
272
|
+
def self.putc(text)
|
273
|
+
instance.putc text
|
274
|
+
end
|
275
|
+
|
276
|
+
# Relay puts to the instance method
|
277
|
+
#
|
278
|
+
# This is used to hide <tt>Kernel#puts</tt> so that the Application's
|
279
|
+
# output IO object is used for printing text
|
280
|
+
#
|
281
|
+
# +text+:: The text to write into the output stream
|
282
|
+
def self.puts(text)
|
283
|
+
instance.puts text
|
284
|
+
end
|
285
|
+
|
286
|
+
# Assigns aliases to the actions that have been defined using action_alias
|
287
|
+
#
|
288
|
+
# Clears the aliases Hash afterwards
|
289
|
+
def assign_aliases
|
290
|
+
@aliases.each do |key, action|
|
291
|
+
if @settings[:dashed_options]
|
292
|
+
action = "--#{action}".to_sym
|
293
|
+
key = "--#{key}".to_sym
|
294
|
+
end
|
295
|
+
|
296
|
+
unless @actions.key? key
|
297
|
+
@actions[key] = @actions[action]
|
298
|
+
else
|
299
|
+
warn "There's already an action called \"#{key}\"."
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
@aliases = {}
|
304
|
+
end
|
305
|
+
|
306
|
+
# Parses the options used when starting the application
|
307
|
+
#
|
308
|
+
# +options+:: An Array of Strings that should be used as application
|
309
|
+
# options. Usually +ARGV+ is used for this.
|
310
|
+
def parse_options(options)
|
311
|
+
actions_to_call = {}
|
312
|
+
last_action = nil
|
313
|
+
|
314
|
+
options.each do |option|
|
315
|
+
option_sym = option.to_sym
|
316
|
+
if @actions.keys.include? option_sym
|
317
|
+
actions_to_call[option_sym] = []
|
318
|
+
last_action = option_sym
|
319
|
+
elsif last_action.nil? || (option.is_a?(String) && @settings[:dashed_options] && option[0..1] == '--')
|
320
|
+
raise UnknownOptionError.new(option)
|
321
|
+
else
|
322
|
+
actions_to_call[last_action] << option
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
actions_to_call
|
327
|
+
end
|
328
|
+
|
329
|
+
end
|
330
|
+
|
331
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# This code is free software; you can redistribute it and/or modify it under the
|
2
|
+
# terms of the new BSD License.
|
3
|
+
#
|
4
|
+
# Copyright (c) 2009, Sebastian Staudt
|
5
|
+
|
6
|
+
module Rubikon
|
7
|
+
|
8
|
+
class ArgumentTypeError < ArgumentError
|
9
|
+
end
|
10
|
+
|
11
|
+
class BlockMissingError < ArgumentError
|
12
|
+
end
|
13
|
+
|
14
|
+
class MissingArgumentError < ArgumentError
|
15
|
+
end
|
16
|
+
|
17
|
+
class MissingOptionError < ArgumentError
|
18
|
+
end
|
19
|
+
|
20
|
+
class UnknownOptionError < ArgumentError
|
21
|
+
|
22
|
+
def initialize(arg)
|
23
|
+
super "Unknown argument: #{arg}"
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
data/lib/rubikon.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# This code is free software; you can redistribute it and/or modify it under the
|
2
|
+
# terms of the new BSD License.
|
3
|
+
#
|
4
|
+
# Copyright (c) 2009, Sebastian Staudt
|
5
|
+
|
6
|
+
libdir = File.dirname(__FILE__)
|
7
|
+
$:.unshift(libdir) unless $:.include?(libdir)
|
8
|
+
|
9
|
+
require 'rubikon/application'
|
data/test/test.rb
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
# This code is free software; you can redistribute it and/or modify it under the
|
2
|
+
# terms of the new BSD License.
|
3
|
+
#
|
4
|
+
# Copyright (c) 2009, Sebastian Staudt
|
5
|
+
|
6
|
+
require 'rubygems'
|
7
|
+
require 'shoulda'
|
8
|
+
|
9
|
+
begin require 'redgreen'; rescue LoadError; end
|
10
|
+
|
11
|
+
$: << File.join(File.dirname(__FILE__), '..', 'lib')
|
12
|
+
$: << File.dirname(__FILE__)
|
13
|
+
require 'rubikon'
|
14
|
+
require 'testapp'
|
15
|
+
|
16
|
+
class RubikonTests < Test::Unit::TestCase
|
17
|
+
|
18
|
+
context 'A Rubikon application\'s class' do
|
19
|
+
|
20
|
+
setup do
|
21
|
+
@app = RubikonTestApp.instance
|
22
|
+
end
|
23
|
+
|
24
|
+
should 'run it\'s instance for called methods' do
|
25
|
+
assert_equal @app.run(%w{--object_id}), RubikonTestApp.run(%w{--object_id})
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'A Rubikon application' do
|
30
|
+
|
31
|
+
setup do
|
32
|
+
@app = RubikonTestApp
|
33
|
+
@ostream = StringIO.new
|
34
|
+
@app.set :ostream, @ostream
|
35
|
+
end
|
36
|
+
|
37
|
+
should 'be a singleton' do
|
38
|
+
assert_raise NoMethodError do
|
39
|
+
RubikonTestApp.new
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
should 'exit gracefully' do
|
44
|
+
unknown = '--unknown'
|
45
|
+
@app.set :raise_errors, false
|
46
|
+
begin
|
47
|
+
@app.run([unknown])
|
48
|
+
rescue Exception => e
|
49
|
+
end
|
50
|
+
assert_instance_of SystemExit, e
|
51
|
+
assert_equal 1, e.status
|
52
|
+
@ostream.rewind
|
53
|
+
assert_equal "Error:\n", @ostream.gets
|
54
|
+
assert_equal " Unknown argument: #{unknown}\n", @ostream.gets
|
55
|
+
@app.set :raise_errors, true
|
56
|
+
end
|
57
|
+
|
58
|
+
should 'run it\'s default action without options' do
|
59
|
+
result = @app.run
|
60
|
+
assert_equal 1, result.size
|
61
|
+
assert_equal 'default action', result.first
|
62
|
+
end
|
63
|
+
|
64
|
+
should 'run with a mandatory option' do
|
65
|
+
result = @app.run(%w{--required arg})
|
66
|
+
assert_equal 1, result.size
|
67
|
+
assert_equal 'required argument was arg', result.first
|
68
|
+
end
|
69
|
+
|
70
|
+
should 'not run without a mandatory argument' do
|
71
|
+
assert_raise Rubikon::MissingArgumentError do
|
72
|
+
@app.run(%w{--required})
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
should 'require an argument type if it has been defined' do
|
77
|
+
assert_raise Rubikon::ArgumentTypeError do
|
78
|
+
@app.run(['--output', 6])
|
79
|
+
end
|
80
|
+
assert_raise Rubikon::ArgumentTypeError do
|
81
|
+
@app.run(['--number_string', 6, 7])
|
82
|
+
end
|
83
|
+
assert_raise Rubikon::ArgumentTypeError do
|
84
|
+
@app.run(['--number_string', 'test' , 6])
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
should 'raise an exception when calling an action with the wrong number of
|
89
|
+
arguments' do
|
90
|
+
assert_raise Rubikon::MissingArgumentError do
|
91
|
+
@app.run(%w{--output})
|
92
|
+
end
|
93
|
+
assert_raise ArgumentError do
|
94
|
+
@app.run(%w{--output}, 'test', 3)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
should 'raise an exception when using an unknown option' do
|
99
|
+
assert_raise Rubikon::UnknownOptionError do
|
100
|
+
@app.run(%w{--unknown})
|
101
|
+
end
|
102
|
+
assert_raise Rubikon::UnknownOptionError do
|
103
|
+
@app.run(%w{--noarg --unknown})
|
104
|
+
end
|
105
|
+
assert_raise Rubikon::UnknownOptionError do
|
106
|
+
@app.run(%w{--unknown --noarg})
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
should 'be able to handle user input' do
|
111
|
+
@istream = StringIO.new
|
112
|
+
@app.set :istream, @istream
|
113
|
+
|
114
|
+
input_string = 'test'
|
115
|
+
@istream.puts input_string
|
116
|
+
@istream.rewind
|
117
|
+
assert_equal [input_string], @app.run(%w{--input})
|
118
|
+
@ostream.rewind
|
119
|
+
assert_equal 'input: ', @ostream.gets
|
120
|
+
end
|
121
|
+
|
122
|
+
should 'write output to the user given output stream' do
|
123
|
+
input_string = 'test'
|
124
|
+
@app.run(['--output', input_string])
|
125
|
+
@ostream.rewind
|
126
|
+
assert_equal "#{input_string}\n", @ostream.gets
|
127
|
+
assert_equal "#{input_string}#{input_string[0].chr}", @ostream.gets
|
128
|
+
end
|
129
|
+
|
130
|
+
should 'provide a throbber' do
|
131
|
+
@app.run(%w{--throbber})
|
132
|
+
@ostream.rewind
|
133
|
+
assert_equal " \b-\b\\\b|\b/\b", @ostream.string
|
134
|
+
@app.run(%w{--throbber true})
|
135
|
+
@ostream.rewind
|
136
|
+
assert_equal " \b-\b\\\b|\b/\bdon't\nbreak\n", @ostream.string
|
137
|
+
end
|
138
|
+
|
139
|
+
should 'have working action aliases' do
|
140
|
+
assert_equal @app.run(%w{--alias_before}), @app.run(%w{--object_id})
|
141
|
+
assert_equal @app.run(%w{--alias_after}), @app.run(%w{--object_id})
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
context 'A Rubikon action' do
|
147
|
+
|
148
|
+
should 'throw an exception when no code block is given' do
|
149
|
+
assert_raise Rubikon::BlockMissingError do
|
150
|
+
Rubikon::Action.new 'name'
|
151
|
+
end
|
152
|
+
assert_raise Rubikon::BlockMissingError do
|
153
|
+
Rubikon::Action.new 'name', {}
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
should 'not raise an exception when created without options' do
|
158
|
+
action_name = 'someaction'
|
159
|
+
action_options = {
|
160
|
+
:description => 'this is an action',
|
161
|
+
:param_type => String
|
162
|
+
}
|
163
|
+
assert_nothing_raised do
|
164
|
+
action = Rubikon::Action.new action_name, action_options do end
|
165
|
+
assert_equal action_name, action.name
|
166
|
+
assert_equal action_options[:description], action.description
|
167
|
+
assert_equal action_options[:param_type], action.param_type
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
data/test/testapp.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# This code is free software; you can redistribute it and/or modify it under the
|
2
|
+
# terms of the new BSD License.
|
3
|
+
#
|
4
|
+
# Copyright (c) 2009, Sebastian Staudt
|
5
|
+
|
6
|
+
class RubikonTestApp < Rubikon::Application
|
7
|
+
|
8
|
+
set :autorun, false
|
9
|
+
set :name, 'Rubikon test application'
|
10
|
+
set :raise_errors, true
|
11
|
+
|
12
|
+
default do
|
13
|
+
'default action'
|
14
|
+
end
|
15
|
+
|
16
|
+
action 'input' do
|
17
|
+
input 'input'
|
18
|
+
end
|
19
|
+
|
20
|
+
action_alias :alias_before, :object_id
|
21
|
+
|
22
|
+
action 'object_id' do
|
23
|
+
object_id
|
24
|
+
end
|
25
|
+
|
26
|
+
action_alias :alias_after, :object_id
|
27
|
+
|
28
|
+
action 'noarg' do
|
29
|
+
'noarg action'
|
30
|
+
end
|
31
|
+
|
32
|
+
action 'realnoarg' do ||
|
33
|
+
end
|
34
|
+
|
35
|
+
action 'noarg2' do
|
36
|
+
end
|
37
|
+
|
38
|
+
action 'number_string', :param_type => [Numeric, String] do |s,n|
|
39
|
+
end
|
40
|
+
|
41
|
+
action 'output', :param_type => String do |s|
|
42
|
+
puts s
|
43
|
+
put s
|
44
|
+
putc s[0]
|
45
|
+
end
|
46
|
+
|
47
|
+
action 'required' do |what|
|
48
|
+
"required argument was #{what}"
|
49
|
+
end
|
50
|
+
|
51
|
+
action 'throbber' do |*output|
|
52
|
+
throbber do
|
53
|
+
if output[0]
|
54
|
+
sleep 0.5
|
55
|
+
puts 'don\'t'
|
56
|
+
sleep 0.5
|
57
|
+
puts 'break'
|
58
|
+
else
|
59
|
+
sleep 1
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
metadata
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rubikon
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sebastian Staudt
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-10-28 00:00:00 +01:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: A simple to use, yet powerful Ruby framework for building console-based applications.
|
17
|
+
email: koraktor@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- LICENSE
|
24
|
+
- README.md
|
25
|
+
files:
|
26
|
+
- LICENSE
|
27
|
+
- README.md
|
28
|
+
- Rakefile
|
29
|
+
- VERSION.yml
|
30
|
+
- lib/rubikon.rb
|
31
|
+
- lib/rubikon/action.rb
|
32
|
+
- lib/rubikon/application.rb
|
33
|
+
- lib/rubikon/exceptions.rb
|
34
|
+
- test/test.rb
|
35
|
+
- test/testapp.rb
|
36
|
+
has_rdoc: true
|
37
|
+
homepage: http://koraktor.github.com/rubikon
|
38
|
+
licenses: []
|
39
|
+
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options:
|
42
|
+
- --all
|
43
|
+
- --inline-source
|
44
|
+
- --line-numbers
|
45
|
+
- --charset=utf-8
|
46
|
+
- --webcvs=http://github.com/koraktor/rubikon/blob/master/%s
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: "0"
|
60
|
+
version:
|
61
|
+
requirements: []
|
62
|
+
|
63
|
+
rubyforge_project: rubikon
|
64
|
+
rubygems_version: 1.3.5
|
65
|
+
signing_key:
|
66
|
+
specification_version: 3
|
67
|
+
summary: Rubikon - A Ruby console app framework
|
68
|
+
test_files:
|
69
|
+
- test/test.rb
|
70
|
+
- test/testapp.rb
|