cuttlebone 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ .rvmrc
2
+ .bundle
3
+ .autotest
4
+ Gemfile.lock
5
+ coverage/
6
+ pkg/
data/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ ## 0.1.2 / 2011-03-18
2
+
3
+ * Changed directory structure to look like a real gem. :)
4
+ * Finalized initial test cases (spec/cucumber).
5
+ * github.com release!
6
+ * Added my original todo example using the new syntax.
7
+
8
+ ## 0.1.1 / 2011-03-15
9
+
10
+ * Added new test cases and new "polished" features.
11
+
12
+ ## 0.1.0 / 2010-07-06
13
+
14
+ * After a long (inactive) year, some projects generated a need to push
15
+ cuttlebone a little further. There are no new features but tests and
16
+ documentation.
17
+
18
+ ## 0.0.1 / 2009-05-13
19
+
20
+ * Birthday! Presentation on the topic (and my proof of concept demo):
21
+ http://www.viddler.com/explore/budapestrb/videos/4/
22
+
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :gemcutter
2
+ gemspec
data/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # cuttlebone
2
+
3
+ Cuttlebone helps you creating shell-alike applications. Easily.
4
+
5
+ ## INSTALL AND USAGE
6
+
7
+ Install Ruby 1.9.2, clone, setup, try. :) Check `examples` and `features`
8
+ directories for examples.
9
+
10
+ ## TODO
11
+
12
+ * Documentation-documentation-documentation...
13
+
14
+ * Tests-tests-tests...
15
+
16
+ * Methods defined in context definitions should be called from command/prompt
17
+ blocks.
18
+
19
+ * For Symbol contexts special attribute and method definitions should work
20
+ intuitively.
21
+
22
+ * Terminal handling, colors, sizes (eg.
23
+ http://codeidol.com/other/rubyckbk/User-Interface/Determining-Terminal-Size/,
24
+ ncurses(?)).
25
+
26
+ * Autocomplete (eg. cldwalker/bond or with a new approach based on 1.9.2's
27
+ readline enhancement).
28
+
29
+ * Asynchronous (server-initiated) output.
30
+
31
+ * Web driver.
32
+
33
+ ## LICENSE
34
+
35
+ (The MIT License)
36
+
37
+ Copyright (c) 2009-2011 Golda Bence <bence@golda.me>
38
+
39
+ Permission is hereby granted, free of charge, to any person obtaining
40
+ a copy of this software and associated documentation files (the
41
+ 'Software'), to deal in the Software without restriction, including
42
+ without limitation the rights to use, copy, modify, merge, publish,
43
+ distribute, sublicense, and/or sell copies of the Software, and to
44
+ permit persons to whom the Software is furnished to do so, subject to
45
+ the following conditions:
46
+
47
+ The above copyright notice and this permission notice shall be
48
+ included in all copies or substantial portions of the Software.
49
+
50
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
51
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
52
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
53
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
54
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
55
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
56
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'rake'
5
+ require 'rake/rdoctask'
6
+ require 'rspec/core/rake_task'
7
+ require 'cucumber/rake/task'
8
+
9
+ require 'rake/packagetask'
10
+ require 'rake/gempackagetask'
11
+
12
+ CUTTLEBONE_GEMSPEC = eval(File.read(File.expand_path('../cuttlebone.gemspec', __FILE__)))
13
+
14
+ desc 'Default: run specs'
15
+ task :default => 'spec'
16
+
17
+ namespace :spec do
18
+ desc 'Run all specs in spec directory (format=progress)'
19
+ RSpec::Core::RakeTask.new(:progress) do |t|
20
+ t.pattern = './spec/**/*_spec.rb'
21
+ t.rspec_opts = ['--color', '--format=progress']
22
+ end
23
+
24
+ desc 'Run all specs in spec directory (format=documentation)'
25
+ RSpec::Core::RakeTask.new(:documentation) do |t|
26
+ t.pattern = './spec/**/*_spec.rb'
27
+ t.rspec_opts = ['--color', '--format=documentation']
28
+ end
29
+
30
+ desc "Run specs with rcov"
31
+ RSpec::Core::RakeTask.new(:rcov) do |t|
32
+ t.pattern = './spec/**/*_spec.rb'
33
+ t.rcov = true
34
+ t.rcov_opts = ['--exclude', 'gems/,spec/,features/']
35
+ end
36
+ end
37
+
38
+ task :spec => 'spec:progress'
39
+
40
+ desc 'Run all cucumber tests.'
41
+ Cucumber::Rake::Task.new do |t|
42
+ end
43
+
44
+ desc 'Generate documentation for the a4-core plugin.'
45
+ Rake::RDocTask.new(:rdoc) do |rdoc|
46
+ rdoc.rdoc_dir = 'rdoc'
47
+ rdoc.title = 'A4::Core'
48
+ rdoc.options << '--line-numbers' << '--inline-source' << '--charset=UTF-8'
49
+ rdoc.rdoc_files.include('README')
50
+ rdoc.rdoc_files.include('lib/**/*.rb')
51
+ end
52
+
53
+ Rake::GemPackageTask.new(CUTTLEBONE_GEMSPEC) do |p|
54
+ p.gem_spec = CUTTLEBONE_GEMSPEC
55
+ end
@@ -0,0 +1,27 @@
1
+ require File.expand_path("../lib/cuttlebone/version", __FILE__)
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "cuttlebone"
5
+ s.version = Cuttlebone::VERSION
6
+ s.platform = Gem::Platform::RUBY
7
+ s.authors = ["Bence Golda"]
8
+ s.email = ["bence@golda.me"]
9
+ s.homepage = "http://github.com/gbence/cuttlebone"
10
+ s.summary = "cuttlebone-#{Cuttlebone::VERSION}"
11
+ s.description = "Cuttlebone helps you creating shell-alike applications."
12
+
13
+ s.rubyforge_project = "cuttlebone"
14
+ s.required_rubygems_version = ">= 1.3.7"
15
+
16
+ s.add_development_dependency "bundler", "~> 1.0.0"
17
+ s.add_development_dependency "rspec", "~> 2.5.0"
18
+ s.add_development_dependency "i18n"
19
+ s.add_development_dependency "cucumber", "~> 0.10.0"
20
+ s.add_development_dependency "rcov", "~> 0.9.0"
21
+
22
+ s.files = `git ls-files`.split("\n")
23
+ s.executables = `git ls-files`.split("\n").select{|f| f =~ /^bin/}
24
+ s.extra_rdoc_files = [ "README.md" ]
25
+ s.rdoc_options = ["--charset=UTF-8"]
26
+ s.require_path = 'lib'
27
+ end
data/examples/todo.rb ADDED
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require File.expand_path('../../lib/cuttlebone', __FILE__)
5
+
6
+ ##
7
+ # MODEL
8
+
9
+ class Task
10
+ @@id = 0
11
+ attr_accessor :id, :title
12
+ def initialize options={}
13
+ @id = (@@id+=1)
14
+ @title = options.delete(:title)
15
+ end
16
+ end
17
+
18
+ ##
19
+ # CUTTLEBONE DESCRIPTIONS
20
+
21
+ context Array do
22
+ #def find_task_by_id(id_s)
23
+ # id = id_s.to_i
24
+ # detect { |t| t.id == id }
25
+ #end
26
+
27
+ prompt() { "tasks(#{size})" }
28
+
29
+ command(?l) { each { |t| output('(%03d) %50s' % [t.id, t.title]) } }
30
+ command(?n) { send(:<<, t = Task.new); add t }
31
+ command /^([0-9]+)$/ do |id_s|
32
+ #t = find_task_by_id(id_s)
33
+ id = id_s.to_i
34
+ t = detect { |t| t.id == id }
35
+ add(t) if t
36
+ end
37
+ command /^d ([0-9]+)$/ do |id_s|
38
+ #t = find_task_by_id(id_s)
39
+ id = id_s.to_i
40
+ t = detect { |t| t.id == id }
41
+ delete(t)
42
+ end
43
+ command(?q) { drop }
44
+ end
45
+
46
+ context Task do
47
+ prompt() { ('%03d' % [id]) + (' ' + (title.size<=20 ? title : "#{title[0..18]}…") rescue '') }
48
+
49
+ command(?q) { drop }
50
+ command /^(.+)$/ do |text|
51
+ send(:title=, text)
52
+ end
53
+ end
54
+
55
+ at_exit do
56
+ Cuttlebone.run([Task.new(:title => 'x'), Task.new(:title => 'y')])
57
+ end
@@ -0,0 +1,53 @@
1
+ @console @example
2
+ Feature: a simple example
3
+ In order to be able to manage a simple todo list
4
+ As a programmer
5
+ I want to create cuttlebone program
6
+
7
+ Scenario: starting with no defined contexts
8
+ Given no cuttlebone code
9
+ When I start an "x" session
10
+ Then I should get an error
11
+
12
+ Scenario: starting with an existing context
13
+ Given the following cuttlebone code:
14
+ """
15
+ context "x" do
16
+ end
17
+ """
18
+ When I start an "x" session
19
+ Then I should see an empty prompt
20
+ And I should be in context "x"
21
+
22
+ Scenario: starting with a missing context
23
+ Given the following cuttlebone code:
24
+ """
25
+ context "x" do
26
+ end
27
+ """
28
+ When I start a "y" session
29
+ Then I should get an error
30
+
31
+ Scenario: starting a context with prompt defined
32
+ Given the following cuttlebone code:
33
+ """
34
+ context "x" do
35
+ prompt { "prompt" }
36
+ end
37
+ """
38
+ When I start an "x" session
39
+ Then I should see "prompt" as prompt
40
+
41
+ Scenario: invoking a simple command
42
+ Given the following cuttlebone code:
43
+ """
44
+ context "x" do
45
+ command /^y$/ do
46
+ self
47
+ end
48
+ end
49
+ """
50
+ When I start an "x" session
51
+ And I call command "y"
52
+ Then I should see an empty prompt
53
+ And I should be in context "x"
@@ -0,0 +1,76 @@
1
+ require 'pp'
2
+ # schema / definitions
3
+
4
+ Given /^no cuttlebone code$/ do
5
+ Cuttlebone.definitions.clear
6
+ end
7
+
8
+ Given /^the following cuttlebone code:$/ do |string|
9
+ Given %{no cuttlebone code}
10
+ Cuttlebone.instance_eval(string)
11
+ end
12
+
13
+ # initialization
14
+
15
+ Given /^a started "([^"]*)" session$/ do |objects|
16
+ When %{I start a #{objects.inspect} session}
17
+ end
18
+
19
+ When /^I start (?:an? )?"([^"]*)" session$/ do |objects|
20
+ @d = Cuttlebone::Session::Test.new(*(objects.scan(/([^,]{1,})(?:,\s*)?/).flatten))
21
+ end
22
+
23
+ # invocation
24
+
25
+ When /^I call command "([^"]*)"$/ do |command|
26
+ @d.call(command)
27
+ end
28
+
29
+ # context related steps
30
+
31
+ Then /^I should be in context "([^"]*)"$/ do |name|
32
+ @d.active_context.context.to_s.should == name
33
+ end
34
+
35
+ Then /^I should see a terminated session$/ do
36
+ @d.should be_terminated
37
+ end
38
+
39
+ Then /^I should be in the same context$/ do
40
+ @d.previous_active_context.should == @d.active_context
41
+ end
42
+
43
+ Then /^I should not be in the same context$/ do
44
+ @d.previous_active_context.should_not == @d.active_context
45
+ end
46
+
47
+ # output related steps
48
+
49
+ Then /^I should see "([^"]*)"$/ do |text|
50
+ @d.output.should include(text)
51
+ end
52
+
53
+ # prompt related steps
54
+
55
+ Then /^I should see "([^"]*)" as prompt$/ do |text|
56
+ @d.prompt.should include(text)
57
+ end
58
+
59
+ Then /^I should see \/([^\/]*)\/ as prompt$/ do |regexp|
60
+ @d.prompt.should match(regexp)
61
+ end
62
+
63
+ Then /^I should see an empty prompt$/ do
64
+ @d.prompt.should be_empty
65
+ end
66
+
67
+ # error related steps
68
+
69
+ Then /^I should see an error$/ do
70
+ @d.error.should_not be_blank
71
+ end
72
+
73
+ Then /^I should get an error$/ do
74
+ @d.internal_error.should_not be_blank
75
+ end
76
+
@@ -0,0 +1,2 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'lib', 'cuttlebone'))
2
+ require 'cucumber/formatter/unicode'
@@ -0,0 +1,24 @@
1
+ class Cuttlebone::Session::Test < Cuttlebone::Session::Base
2
+ def initialize *args
3
+ super
4
+ stack = []
5
+ @stack.each { |c| stack << c }
6
+ @stack_history = [ stack ]
7
+ end
8
+
9
+ def process command
10
+ stack = []
11
+ @stack.each { |c| stack << c }
12
+ @stack_history << stack
13
+ a, n, o, e = super(command)
14
+ @output = o
15
+ @error = e
16
+ [ a, n, o, e ]
17
+ end
18
+
19
+ attr_reader :output, :error
20
+
21
+ def previous_active_context
22
+ @stack_history[-2].last
23
+ end
24
+ end
@@ -0,0 +1,78 @@
1
+ Feature: switching between contexts
2
+ In order to be able to manage my todo list
3
+ As a programmer
4
+ I want to switch between cuttlebone contexts
5
+
6
+ Background:
7
+ Given the following cuttlebone code:
8
+ """
9
+ context 'x' do
10
+ command 'q' do
11
+ drop
12
+ end
13
+ command 'y' do
14
+ replace 'y'
15
+ end
16
+ command 'yy' do
17
+ add 'y'
18
+ end
19
+ command 'x' do
20
+ self
21
+ end
22
+ command 'xx' do
23
+ add 'x'
24
+ end
25
+ command 'r' do
26
+ replace 'x'
27
+ end
28
+ end
29
+
30
+ context 'y' do
31
+ command 'q' do
32
+ drop
33
+ end
34
+ command 'y' do
35
+ self
36
+ end
37
+ end
38
+ """
39
+
40
+ Scenario: quitting
41
+ Given a started "x" session
42
+ When I call command "q"
43
+ Then I should see a terminated session
44
+
45
+ Scenario: replacing current context
46
+ Given a started "x" session
47
+ When I call command "y"
48
+ Then I should be in context "y"
49
+
50
+ Scenario: returning the same context
51
+ Given a started "x" session
52
+ When I call command "x"
53
+ Then I should be in the same context
54
+
55
+ Scenario: replacing with a similar context
56
+ Given a started "x" session
57
+ When I call command "r"
58
+ Then I should not be in the same context
59
+
60
+ Scenario: entering into a new context
61
+ Given a started "x" session
62
+ When I call command "yy"
63
+ Then I should be in context "y"
64
+
65
+ Scenario: dropping previously built context
66
+ Given a started "x" session
67
+ When I call command "yy"
68
+ And I call command "q"
69
+ Then I should be in context "x"
70
+
71
+ Scenario: consume multiple contexts and quit
72
+ Given a started "x,y,y,x" session
73
+ When I call command "q"
74
+ And I call command "q"
75
+ And I call command "q"
76
+ Then I should be in context "x"
77
+ When I call command "q"
78
+ Then I should see a terminated session
data/lib/cuttlebone.rb ADDED
@@ -0,0 +1,19 @@
1
+ $: << File.expand_path('../', __FILE__)
2
+
3
+ require File.expand_path('../../vendor/active_support.rb', __FILE__)
4
+
5
+ module Cuttlebone
6
+ require 'cuttlebone/exceptions'
7
+ autoload :Controller, 'cuttlebone/controller'
8
+ autoload :Definition, 'cuttlebone/definition'
9
+ autoload :Session, 'cuttlebone/session'
10
+
11
+ @@definitions = []
12
+ def self.definitions; @@definitions; end
13
+
14
+ def self.run starting_objects, default_driver=Session::Shell
15
+ default_driver.new(starting_objects).run
16
+ end
17
+ end
18
+
19
+ include Cuttlebone::Definition::DSL
@@ -0,0 +1,69 @@
1
+ #
2
+ # NOTE: we don't really want to pollute this proxy with a lot of private
3
+ # (internal) methods to leave space for <<context>>'s real methods.
4
+ class Cuttlebone::Controller
5
+
6
+ attr_reader :session, :context, :definition
7
+
8
+ def initialize session, context
9
+ @session = session
10
+ @context = context
11
+ @definition = Cuttlebone.definitions.find { |d| d.match(context) }
12
+ raise Cuttlebone::InvalidContextError, context, "No definiton was found for #{context.inspect}!" unless @definition
13
+ end
14
+
15
+ ##
16
+ # Processes a command on its context's domain.
17
+ #
18
+ def process command
19
+ @next_action = nil
20
+ @output = []
21
+
22
+ return([:self, self, [], nil]) if command.empty?
23
+ block, arguments = definition.proc_for(command)
24
+
25
+ instance_exec(*arguments, &block)
26
+ action, context = @next_action || [ :self, self ]
27
+
28
+ return([ action, context, @output, nil ])
29
+ rescue Cuttlebone::DoubleActionError
30
+ raise
31
+ rescue => e
32
+ return([ :self, self, [], %{Cuttlebone::Controller: #{e.message} (#{e.class})} ])
33
+ end
34
+
35
+ def prompt
36
+ return(instance_exec(&@definition.prompt) || '')
37
+ rescue => e
38
+ %{error: #{e.message} (#{e.class.name})}
39
+ end
40
+
41
+ def drop
42
+ __save_next_action! :drop
43
+ end
44
+
45
+ def add context
46
+ __save_next_action! :add, context
47
+ end
48
+
49
+ def replace context
50
+ __save_next_action! :replace, context
51
+ end
52
+
53
+ def output text
54
+ @output << text
55
+ end
56
+
57
+ def method_missing method_name, *args, &block
58
+ return(@context.send(method_name, *args, &block)) if @context.respond_to?(method_name)
59
+ return(super)
60
+ end
61
+
62
+ private
63
+
64
+ def __save_next_action! action, context=nil
65
+ raise Cuttlebone::DoubleActionError if @next_action
66
+ @next_action = [ action, context ]
67
+ end
68
+
69
+ end
@@ -0,0 +1,58 @@
1
+ module Cuttlebone
2
+ class Definition
3
+
4
+ module DSL
5
+ def context object, options={}, &definition
6
+ Cuttlebone.definitions << Definition.new(object, options, &definition)
7
+ end
8
+ end
9
+
10
+ class Parser
11
+ def initialize definition
12
+ @definition = definition
13
+ end
14
+
15
+ def command string_or_regexp, &block
16
+ @definition.commands << [ string_or_regexp, block, @last_description ]
17
+ @last_description = nil
18
+ self
19
+ end
20
+
21
+ def description text
22
+ @last_description = text
23
+ self
24
+ end
25
+
26
+ def prompt text='', &block
27
+ @definition.prompt = block_given? ? block : proc { text }
28
+ end
29
+ end
30
+
31
+ attr_accessor :prompt, :commands
32
+
33
+ def initialize object_or_class, options={}, &definition
34
+ raise ArgumentError, 'missing block' unless block_given?
35
+ @object_or_class = object_or_class
36
+ @options = options
37
+ @commands = [] #Array.new(proc { |c| [:self, c, '', nil] })
38
+ @prompt = proc { |c| '' }
39
+
40
+ @parser = Parser.new(self)
41
+ @parser.instance_eval(&definition)
42
+ end
43
+
44
+ delegate :command, :to => '@parser'
45
+
46
+ def match object
47
+ @object_or_class === object
48
+ # TODO :if :unless options
49
+ end
50
+
51
+ def proc_for command
52
+ string_or_regexp, block, description = commands.find { |(sr,b,d)| sr === command } || raise(UnknownCommandError, "Unknown command: #{command.inspect}!")
53
+ return([ block, $~.captures ]) if $~
54
+ return([ block, [] ])
55
+ end
56
+ end
57
+
58
+ end
@@ -0,0 +1,24 @@
1
+ module Cuttlebone
2
+
3
+ ##
4
+ # Raised when Controller is invoked with an invalid (not matching) context.
5
+ #
6
+ class InvalidContextError < StandardError
7
+ attr_reader :context
8
+ def initialize context, *args, &block
9
+ @context = context
10
+ super *args, &block
11
+ end
12
+ end
13
+
14
+ ##
15
+ # Raised on unknown command calls.
16
+ #
17
+ class UnknownCommandError < StandardError; end
18
+
19
+ ##
20
+ # Raised when multiple actions are invoked simultanously.
21
+ #
22
+ class DoubleActionError < StandardError; end
23
+
24
+ end
@@ -0,0 +1,4 @@
1
+ module Cuttlebone::Session
2
+ autoload :Base, 'cuttlebone/session/base'
3
+ autoload :Shell, 'cuttlebone/session/shell'
4
+ end
@@ -0,0 +1,61 @@
1
+ class Cuttlebone::Session::Base
2
+
3
+ attr_reader :stack, :internal_error
4
+
5
+ def initialize *stack_objects
6
+ setup_contexts 'cuttlebone.stack_objects' => stack_objects
7
+ end
8
+
9
+ def active_context
10
+ stack.last
11
+ end
12
+
13
+ delegate :name, :prompt, :to => :active_context
14
+
15
+ def setup_contexts env={}
16
+ @stack ||= (env['cuttlebone.stack_objects'] || []).map{ |o| Cuttlebone::Controller.new(self, o) }
17
+ rescue Cuttlebone::InvalidContextError => e
18
+ @internal_error = %{Context initialization failed for #{e.context.inspect}!}
19
+ @stack = []
20
+ rescue => e
21
+ @internal_error = %{Internal error occured: #{e.message} (#{e.class})}
22
+ @stack = []
23
+ end
24
+ private :setup_contexts
25
+
26
+ def process command
27
+ active_context = @stack.pop
28
+
29
+ action, next_context, output, error = begin
30
+ a, n, o, e = active_context.process(command)
31
+ raise ArgumentError, "Unknown action: #{a}" unless [ :self, :replace, :add, :drop ].include?(a.to_s.to_sym)
32
+ raise TypeError, "Output must be an instance of String or nil!" unless o.is_a?(Array) or o.nil?
33
+ raise TypeError, "Error must be an instance of String or nil!" unless e.is_a?(String) or e.nil?
34
+ [ a.to_s.to_sym, n, o, e ]
35
+ rescue => e
36
+ [ :self, active_context, nil, %{Cuttlebone::Session: #{e.message} (#{e.class})} ]
37
+ end
38
+ case action
39
+ when :self
40
+ @stack << active_context
41
+ when :replace
42
+ @stack << Cuttlebone::Controller.new(self, next_context)
43
+ when :add
44
+ @stack << active_context << Cuttlebone::Controller.new(self, next_context)
45
+ when :drop
46
+ # noop
47
+ end
48
+ [ action, next_context, output, error ]
49
+ end
50
+ private :process
51
+
52
+ def call command, env={}
53
+ setup_contexts env
54
+ process command
55
+ end
56
+
57
+ def terminated?
58
+ stack.empty?
59
+ end
60
+
61
+ end
@@ -0,0 +1,4 @@
1
+ class Cuttlebone::Session::Rack < Cuttlebone::Session::Base
2
+ def run
3
+ end
4
+ end
@@ -0,0 +1,17 @@
1
+ # TODO FIXME load readline iff it was invoked through command line interface
2
+ require 'readline'
3
+
4
+ class Cuttlebone::Session::Shell < Cuttlebone::Session::Base
5
+ def run
6
+ loop do
7
+ break if terminated?
8
+ command = Readline::readline("#{prompt} > ")
9
+ break unless command
10
+ Readline::HISTORY.push(command)
11
+ _, _, output, error = call(command)
12
+ print (output<<'').join("\n")
13
+ print "\033[01;31m#{error}\033[00m\n" if error
14
+ end
15
+ end
16
+ end
17
+
@@ -0,0 +1,3 @@
1
+ module Cuttlebone
2
+ VERSION = "0.1.3"
3
+ end
@@ -0,0 +1,79 @@
1
+ require 'spec_helper'
2
+
3
+ describe Cuttlebone::Controller do
4
+ let(:valid_session) { mock("Cuttlebone::Session::Base") }
5
+ let(:valid_context) { 'x' }
6
+
7
+ let(:drop_proc) { proc { |*args| output 'dropped'; drop } }
8
+ let(:self_proc) { proc { |*args| output 'noop'; self } }
9
+ let(:add_proc) { proc { |*args| output 'added'; add 'x' } } # TODO FIXME change 'x' => valid_context somehow 'cause they are the same instance
10
+ let(:replace_proc) { proc { |*args| output 'replaced'; replace 'x' } }
11
+ let(:double_action_error_proc) { proc { |*args| output 'double action error'; drop; add 'x' } }
12
+ let(:prompt_proc) { proc { |*args| 'prompt' } }
13
+
14
+ let(:valid_context_definition) do
15
+ x = mock('Cuttlebone::Definition "x"')
16
+ x.stub!(:match).and_return() { |*args| args == [valid_context] }
17
+ x.stub!(:proc_for).with('drop').and_return(drop_proc)
18
+ x.stub!(:proc_for).with('self').and_return(self_proc)
19
+ x.stub!(:proc_for).with('add').and_return(add_proc)
20
+ x.stub!(:proc_for).with('replace').and_return(replace_proc)
21
+ x.stub!(:proc_for).with('double').and_return(double_action_error_proc)
22
+ x.stub!(:prompt).and_return(prompt_proc)
23
+ x
24
+ end
25
+
26
+ before :each do
27
+ Cuttlebone.stub!(:definitions).and_return([ valid_context_definition ])
28
+ end
29
+
30
+ context "given a valid context" do
31
+ subject { Cuttlebone::Controller.new(valid_session, valid_context) }
32
+
33
+ it "should return context object" do
34
+ subject.context.should == valid_context
35
+ end
36
+
37
+ it "should return [:self, self, nil, nil] to empty commands" do
38
+ subject.process('').should == [ :self, subject, [], nil ]
39
+ end
40
+
41
+ it "should return [:drop, nil, 'dropped', nil] to 'drop' commands" do
42
+ subject.process('drop').should == [:drop, nil, ['dropped'], nil]
43
+ end
44
+
45
+ it "should return [:self, self, 'noop', nil] to 'drop' commands" do
46
+ subject.process('self').should == [:self, subject, ['noop'], nil]
47
+ end
48
+
49
+ it "should return [:add, valid_context, 'added', nil] to 'drop' commands" do
50
+ subject.process('add').should == [:add, valid_context, ['added'], nil]
51
+ end
52
+
53
+ it "should return [:replace, valid_context, 'replaced', nil] to 'drop' commands" do
54
+ subject.process('replace').should == [:replace, valid_context, ['replaced'], nil]
55
+ end
56
+
57
+ it "should raise error on double action commands" do
58
+ expect{ subject.process('double') }.should raise_error(Cuttlebone::DoubleActionError)
59
+ end
60
+
61
+ it "should execute command in <<context>>'s context" do
62
+ valid_context_definition.stub!(:proc_for).with('x').and_return(proc{ x })
63
+ valid_context.should_receive(:x)
64
+ subject.process('x')
65
+ end
66
+
67
+ it "should execute prompt in <<context>>'s context" do
68
+ valid_context_definition.stub!(:prompt).and_return(proc{ x })
69
+ valid_context.should_receive(:x)
70
+ subject.prompt()
71
+ end
72
+ end
73
+
74
+ context "given an invalid context" do
75
+ it "should raise an exception" do
76
+ expect{ Cuttlebone::Controller.new(valid_session, 'invalid_context') }.should raise_error(Cuttlebone::InvalidContextError)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ describe Cuttlebone do
4
+ before :each do
5
+ Cuttlebone.definitions.clear
6
+ end
7
+
8
+ it "should start with no contexts defined" do
9
+ Cuttlebone.definitions.should be_empty
10
+ end
11
+
12
+ it "should be able to evaluate new context definitions" do
13
+ Cuttlebone.should respond_to(:context)
14
+ end
15
+
16
+ it "should start a new session" do
17
+ Cuttlebone.should respond_to(:run)
18
+ end
19
+ end
@@ -0,0 +1,100 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '/spec_helper'))
2
+
3
+ describe Cuttlebone::Definition do
4
+ let(:valid_context_identifier) { :c }
5
+ let(:valid_options) { {} }
6
+ let(:valid_block) { proc { command('x') {} } }
7
+
8
+ context "given valid but meaningless parameters" do
9
+ subject { Cuttlebone::Definition.new(valid_context_identifier, valid_options, &valid_block) }
10
+
11
+ it "should match to the context id given" do
12
+ subject.should match(valid_context_identifier)
13
+ end
14
+
15
+ it "should return prompt" do
16
+ subject.should respond_to(:prompt)
17
+ subject.prompt.should be_a(Proc)
18
+ end
19
+ end
20
+
21
+ context "given class for context matcher" do
22
+ let(:klass) { Class.new(Object) }
23
+ let(:instance1) { klass.new }
24
+ let(:instance2) { klass.new }
25
+ let(:other_instance) { Object.new }
26
+ let(:subclass) { Class.new(klass) }
27
+ let(:instance3) { subclass.new }
28
+ subject { Cuttlebone::Definition.new(klass, valid_options, &valid_block) }
29
+
30
+ it "should match instances" do
31
+ subject.should match(instance1)
32
+ subject.should match(instance2)
33
+ subject.should_not match(other_instance)
34
+ end
35
+
36
+ it "should match subclass instances" do
37
+ subject.should match(instance3)
38
+ end
39
+ end
40
+
41
+ context "given several commands" do
42
+ let(:drop_block) { proc { drop } }
43
+ let(:add_block) { proc { |arg| add arg.to_s.to_sym } }
44
+ let(:definition_with_several_commands) do
45
+ proc do
46
+ command 'nil_will_be_self' do
47
+ end
48
+ command 'drop' do
49
+ drop
50
+ end
51
+ command 'add x' do
52
+ add 0
53
+ end
54
+ command 'replace x' do
55
+ replace :x
56
+ end
57
+ command 'self' do
58
+ self
59
+ end
60
+ command 'double_action_error' do
61
+ add :x
62
+ drop
63
+ end
64
+ end
65
+ end
66
+
67
+ subject { Cuttlebone::Definition.new(valid_context_identifier, valid_options, &definition_with_several_commands) }
68
+
69
+ it "should have several commands" do
70
+ subject.should have(6).commands
71
+ end
72
+
73
+ it "should parse new commands" do
74
+ subject.command(/new command/) { self }
75
+ subject.should have(7).commands
76
+ end
77
+
78
+ it "should return the specific block for a command" do
79
+ subject.command /^add (.)$/, &add_block
80
+
81
+ subject.proc_for('add x').should_not == [add_block, []]
82
+ subject.proc_for('add y').should == [add_block, ['y']]
83
+ end
84
+
85
+ it "should parse arguments properly" do
86
+ subject.command /^add (.)(.)?$/, &(proc{|a,b|})
87
+
88
+ subject.proc_for('add y').last.should == ['y', nil]
89
+ subject.proc_for('add yz').last.should == ['y', 'z']
90
+ end
91
+
92
+ it "should return an error for unmatched commands" do
93
+ expect{ subject.proc_for('unmatched') }.should raise_error(Cuttlebone::UnknownCommandError)
94
+ end
95
+
96
+ it "should return no error on semantically wrong commands" do
97
+ expect{ subject.proc_for('double_action_error') }.should_not raise_error
98
+ end
99
+ end
100
+ end
data/spec/rcov.opts ADDED
@@ -0,0 +1,2 @@
1
+ --exclude "spec/*,gems/*"
2
+ --rails
@@ -0,0 +1,82 @@
1
+ require 'spec_helper'
2
+
3
+ describe Cuttlebone::Session::Base do
4
+ let(:valid_context) { 'x' }
5
+ let(:valid_command) { 'y' }
6
+
7
+ let(:valid_context_definition) do
8
+ x = mock('Definition:x')
9
+ x.stub!(:match).and_return() { |*args| args == [valid_context] }
10
+ x.stub!(:proc_for).with(any_args()).and_return([proc{}, []])
11
+ x
12
+ end
13
+
14
+ before :each do
15
+ Cuttlebone.stub!(:definitions).and_return([ valid_context_definition ])
16
+ end
17
+
18
+ context "having an active 'x' context" do
19
+ subject { Cuttlebone::Session::Base.new(valid_context) }
20
+
21
+ it { should_not be_terminated }
22
+
23
+ it "should have no internal error" do
24
+ subject.internal_error.should be_blank
25
+ end
26
+
27
+ it "should evaluate a string command" do
28
+ valid_context_definition.should_receive(:match).with(valid_context).and_return(true)
29
+ subject.call(valid_command)
30
+ end
31
+
32
+ it "should return active context" do
33
+ subject.should respond_to(:active_context)
34
+ subject.active_context.should be_a(Cuttlebone::Controller)
35
+ end
36
+
37
+ it "should return with a [action, context, output, error] quadruple" do
38
+ a, c, o, e = subject.call(valid_context)
39
+ [ :drop, :replace, :self, :add ].should include(a)
40
+ o and o.should be_a(Array)
41
+ e and e.should be_a(String)
42
+ end
43
+ end
44
+
45
+ context "having no active contexts" do
46
+ subject { Cuttlebone::Session::Base.new() }
47
+
48
+ it "should have no internal error" do
49
+ subject.internal_error.should be_blank
50
+ end
51
+
52
+ it { should be_terminated }
53
+ end
54
+
55
+ context "having 1 context" do
56
+ subject { Cuttlebone::Session::Base.new(valid_context) }
57
+
58
+ it "should have no internal error" do
59
+ subject.internal_error.should be_blank
60
+ end
61
+
62
+ it { should_not be_terminated }
63
+
64
+ it "should be terminated after a 'drop'" do
65
+ valid_context_definition.should_receive(:proc_for).with('drop').and_return([proc{drop}, []])
66
+ subject.call('drop')
67
+ subject.should be_terminated
68
+ end
69
+ end
70
+
71
+ context "given a wrong context" do
72
+ subject { Cuttlebone::Session::Base.new('invalid_context') }
73
+
74
+ it "should indicate internal error" do
75
+ subject.internal_error.should_not be_blank
76
+ end
77
+
78
+ it "should be terminated" do
79
+ subject.should be_terminated
80
+ end
81
+ end
82
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1,4 @@
1
+ --colour
2
+ --format progress
3
+ --loadby mtime
4
+ --reverse
@@ -0,0 +1,8 @@
1
+ require 'rspec'
2
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'cuttlebone'))
3
+
4
+ RSpec.configure do |config|
5
+ config.alias_it_should_behave_like_to(:it_should_behave_like, '')
6
+ config.filter_run :focused => true
7
+ config.run_all_when_everything_filtered = true
8
+ end
@@ -0,0 +1,56 @@
1
+ # (active_support)/lib/active_support/core_ext/module/remove_method.rb
2
+ class Module
3
+ def remove_possible_method(method)
4
+ remove_method(method)
5
+ rescue NameError
6
+ end
7
+
8
+ def redefine_method(method, &block)
9
+ remove_possible_method(method)
10
+ define_method(method, &block)
11
+ end
12
+ end
13
+
14
+ # (active_support)/lib/active_support/core_ext/module/delegation.rb (stripped)
15
+ class Module
16
+ def delegate(*methods)
17
+ options = methods.pop
18
+ unless options.is_a?(Hash) && to = options[:to]
19
+ raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, :to => :greeter)."
20
+ end
21
+
22
+ if options[:prefix] == true && options[:to].to_s =~ /^[^a-z_]/
23
+ raise ArgumentError, "Can only automatically set the delegation prefix when delegating to a method."
24
+ end
25
+
26
+ prefix = options[:prefix] && "#{options[:prefix] == true ? to : options[:prefix]}_" || ''
27
+
28
+ file, line = caller.first.split(':', 2)
29
+ line = line.to_i
30
+
31
+ methods.each do |method|
32
+ on_nil =
33
+ if options[:allow_nil]
34
+ 'return'
35
+ else
36
+ %(raise "#{self}##{prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
37
+ end
38
+
39
+ module_eval(<<-EOS, file, line - 5)
40
+ if instance_methods(false).map(&:to_s).include?("#{prefix}#{method}")
41
+ remove_possible_method("#{prefix}#{method}")
42
+ end
43
+
44
+ def #{prefix}#{method}(*args, &block) # def customer_name(*args, &block)
45
+ #{to}.__send__(#{method.inspect}, *args, &block) # client.__send__(:name, *args, &block)
46
+ rescue NoMethodError # rescue NoMethodError
47
+ if #{to}.nil? # if client.nil?
48
+ #{on_nil} # return # depends on :allow_nil
49
+ else # else
50
+ raise # raise
51
+ end # end
52
+ end # end
53
+ EOS
54
+ end
55
+ end
56
+ end
metadata ADDED
@@ -0,0 +1,142 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cuttlebone
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.1.3
6
+ platform: ruby
7
+ authors:
8
+ - Bence Golda
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-03-18 00:00:00 +01:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: bundler
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ~>
22
+ - !ruby/object:Gem::Version
23
+ version: 1.0.0
24
+ type: :development
25
+ prerelease: false
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: &id002 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ~>
33
+ - !ruby/object:Gem::Version
34
+ version: 2.5.0
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: *id002
38
+ - !ruby/object:Gem::Dependency
39
+ name: i18n
40
+ requirement: &id003 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ type: :development
47
+ prerelease: false
48
+ version_requirements: *id003
49
+ - !ruby/object:Gem::Dependency
50
+ name: cucumber
51
+ requirement: &id004 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ~>
55
+ - !ruby/object:Gem::Version
56
+ version: 0.10.0
57
+ type: :development
58
+ prerelease: false
59
+ version_requirements: *id004
60
+ - !ruby/object:Gem::Dependency
61
+ name: rcov
62
+ requirement: &id005 !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ~>
66
+ - !ruby/object:Gem::Version
67
+ version: 0.9.0
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: *id005
71
+ description: Cuttlebone helps you creating shell-alike applications.
72
+ email:
73
+ - bence@golda.me
74
+ executables: []
75
+
76
+ extensions: []
77
+
78
+ extra_rdoc_files:
79
+ - README.md
80
+ files:
81
+ - .gitignore
82
+ - CHANGELOG.md
83
+ - Gemfile
84
+ - README.md
85
+ - Rakefile
86
+ - cuttlebone.gemspec
87
+ - examples/todo.rb
88
+ - features/example.feature
89
+ - features/step_definitions/steps.rb
90
+ - features/support/env.rb
91
+ - features/support/test_driver.rb
92
+ - features/switching_contexts.feature
93
+ - lib/cuttlebone.rb
94
+ - lib/cuttlebone/controller.rb
95
+ - lib/cuttlebone/definition.rb
96
+ - lib/cuttlebone/exceptions.rb
97
+ - lib/cuttlebone/session.rb
98
+ - lib/cuttlebone/session/base.rb
99
+ - lib/cuttlebone/session/rack.rb
100
+ - lib/cuttlebone/session/shell.rb
101
+ - lib/cuttlebone/version.rb
102
+ - spec/controller_spec.rb
103
+ - spec/cuttlebone_spec.rb
104
+ - spec/definition_spec.rb
105
+ - spec/rcov.opts
106
+ - spec/session_base_spec.rb
107
+ - spec/spec.opts
108
+ - spec/spec_helper.rb
109
+ - vendor/active_support.rb
110
+ has_rdoc: true
111
+ homepage: http://github.com/gbence/cuttlebone
112
+ licenses: []
113
+
114
+ post_install_message:
115
+ rdoc_options:
116
+ - --charset=UTF-8
117
+ require_paths:
118
+ - lib
119
+ required_ruby_version: !ruby/object:Gem::Requirement
120
+ none: false
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ hash: 2700429317005123117
125
+ segments:
126
+ - 0
127
+ version: "0"
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: 1.3.7
134
+ requirements: []
135
+
136
+ rubyforge_project: cuttlebone
137
+ rubygems_version: 1.5.3
138
+ signing_key:
139
+ specification_version: 3
140
+ summary: cuttlebone-0.1.3
141
+ test_files: []
142
+