cuttlebone 0.1.3

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/.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
+