cuttlebone 0.1.3 → 0.1.4

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 CHANGED
@@ -1,6 +1,7 @@
1
1
  .rvmrc
2
2
  .bundle
3
3
  .autotest
4
+ .*.swp
4
5
  Gemfile.lock
5
6
  coverage/
6
7
  pkg/
data/Rakefile CHANGED
@@ -9,11 +9,42 @@ require 'cucumber/rake/task'
9
9
  require 'rake/packagetask'
10
10
  require 'rake/gempackagetask'
11
11
 
12
+ begin
13
+ require 'haml'
14
+ require 'sass'
15
+ rescue LoadError
16
+ end
17
+
12
18
  CUTTLEBONE_GEMSPEC = eval(File.read(File.expand_path('../cuttlebone.gemspec', __FILE__)))
13
19
 
14
20
  desc 'Default: run specs'
15
21
  task :default => 'spec'
16
22
 
23
+ if defined?(Haml) and defined?(Sass)
24
+ task :gem => 'rack:compile'
25
+
26
+ namespace :rack do
27
+ desc 'Compiles HTML/CSS files.'
28
+ task :compile do
29
+ {
30
+ 'index.html.haml' => 'index.html',
31
+ 'error.html.haml' => 'error.html',
32
+ 'cuttlebone.sass' => 'stylesheets/cuttlebone.css'
33
+ }.each_pair do |f,t|
34
+ File.open(File.expand_path("../public/#{t}", __FILE__), 'w') do |tt|
35
+ tt.write(
36
+ (
37
+ f =~ /\.haml$/ ?
38
+ Haml::Engine.new(File.read(File.expand_path("../public/sources/#{f}",__FILE__)), :format => :html5, :ugly => true) :
39
+ Sass::Engine.new(File.read(File.expand_path("../public/sources/#{f}",__FILE__)))
40
+ ).render()
41
+ )
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
17
48
  namespace :spec do
18
49
  desc 'Run all specs in spec directory (format=progress)'
19
50
  RSpec::Core::RakeTask.new(:progress) do |t|
data/cuttlebone.gemspec CHANGED
@@ -18,6 +18,8 @@ Gem::Specification.new do |s|
18
18
  s.add_development_dependency "i18n"
19
19
  s.add_development_dependency "cucumber", "~> 0.10.0"
20
20
  s.add_development_dependency "rcov", "~> 0.9.0"
21
+ s.add_development_dependency "capybara", "~> 0.4.0"
22
+ s.add_development_dependency "haml", "~> 3.0.0"
21
23
 
22
24
  s.files = `git ls-files`.split("\n")
23
25
  s.executables = `git ls-files`.split("\n").select{|f| f =~ /^bin/}
data/examples/todo.rb CHANGED
@@ -53,5 +53,5 @@ context Task do
53
53
  end
54
54
 
55
55
  at_exit do
56
- Cuttlebone.run([Task.new(:title => 'x'), Task.new(:title => 'y')])
56
+ Cuttlebone.run([Task.new(:title => 'x'), Task.new(:title => 'y')], Cuttlebone::Drivers::Rack)
57
57
  end
@@ -0,0 +1,19 @@
1
+ @browser @wip
2
+ Feature: rack compatibility
3
+ In order to manage my todo list through the web
4
+ As a programmer
5
+ I want to connect to a cuttlebone web-server
6
+
7
+ Background:
8
+ Given the following cuttlebone code:
9
+ """
10
+ context "x" do
11
+ prompt { 'prompt' }
12
+ command(?y) { output 'ok' }
13
+ end
14
+ """
15
+
16
+ Scenario: start a rack on top of cuttlebone
17
+ When I start an "x" session on rack
18
+ And I go to "/"
19
+ Then I should see "prompt" in the prompt
@@ -0,0 +1,13 @@
1
+ When /^I start (?:an? )?"([^"]*)" session on rack$/ do |objects|
2
+ @d = Cuttlebone::Drivers::Rack.new(*(objects.scan(/([^,]{1,})(?:,\s*)?/).flatten))
3
+ Capybara.app = @d.send(:app)
4
+ end
5
+
6
+ When /^I go to "([^"]*)"$/ do |path|
7
+ visit path
8
+ end
9
+
10
+ Then /^I should see "([^"]*)" in the prompt$/ do |text|
11
+ page.should have_xpath('//span[@id="prompt"]', :text => text)
12
+ end
13
+
@@ -17,7 +17,7 @@ Given /^a started "([^"]*)" session$/ do |objects|
17
17
  end
18
18
 
19
19
  When /^I start (?:an? )?"([^"]*)" session$/ do |objects|
20
- @d = Cuttlebone::Session::Test.new(*(objects.scan(/([^,]{1,})(?:,\s*)?/).flatten))
20
+ @d = Cuttlebone::Drivers::Test.new(*(objects.scan(/([^,]{1,})(?:,\s*)?/).flatten))
21
21
  end
22
22
 
23
23
  # invocation
@@ -71,6 +71,7 @@ Then /^I should see an error$/ do
71
71
  end
72
72
 
73
73
  Then /^I should get an error$/ do
74
- @d.internal_error.should_not be_blank
74
+ @d.internal_error.should_not be_nil
75
+ @d.internal_error.should_not be_empty
75
76
  end
76
77
 
@@ -1,2 +1,9 @@
1
- require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'lib', 'cuttlebone'))
1
+ require File.expand_path('../../../lib/cuttlebone.rb', __FILE__)
2
2
  require 'cucumber/formatter/unicode'
3
+
4
+ require 'capybara/cucumber'
5
+ require 'capybara/session'
6
+ Capybara.default_selector = :css
7
+ Capybara.default_driver = :selenium
8
+ #Capybara.app = 0 #
9
+
@@ -1,16 +1,14 @@
1
- class Cuttlebone::Session::Test < Cuttlebone::Session::Base
2
- def initialize *args
1
+ # TODO FIXME
2
+ class Cuttlebone::Drivers::Test < Cuttlebone::Drivers::Base
3
+ def initialize *stack_objects
3
4
  super
4
- stack = []
5
- @stack.each { |c| stack << c }
6
- @stack_history = [ stack ]
5
+ @session = Cuttlebone::Session.sessions.create(*stack_objects)
6
+ save_stack_to_history
7
7
  end
8
8
 
9
- def process command
10
- stack = []
11
- @stack.each { |c| stack << c }
12
- @stack_history << stack
13
- a, n, o, e = super(command)
9
+ def call command
10
+ a, n, o, e = @session.call(command)
11
+ save_stack_to_history
14
12
  @output = o
15
13
  @error = e
16
14
  [ a, n, o, e ]
@@ -18,7 +16,22 @@ class Cuttlebone::Session::Test < Cuttlebone::Session::Base
18
16
 
19
17
  attr_reader :output, :error
20
18
 
19
+ def active_context
20
+ @history[-1].last
21
+ end
22
+
21
23
  def previous_active_context
22
- @stack_history[-2].last
24
+ @history[-2].last
25
+ end
26
+
27
+ delegate :terminated?, :prompt, :internal_error, :to => '@session'
28
+
29
+ private
30
+
31
+ def save_stack_to_history
32
+ stack = []
33
+ @session.stack.each { |c| stack << c }
34
+ (@history ||= []) << stack
23
35
  end
36
+
24
37
  end
@@ -0,0 +1,13 @@
1
+ class Cuttlebone::Drivers::Base
2
+ def initialize *stack_objects
3
+ Cuttlebone::Session.set_default *stack_objects
4
+ end
5
+
6
+ def sessions
7
+ Cuttlebone::Session.sessions
8
+ end
9
+
10
+ def run
11
+ raise NotImplementedError, "You must implement #run in your #{send(:class).name}!"
12
+ end
13
+ end
@@ -0,0 +1,154 @@
1
+ require 'rack'
2
+ require 'haml'
3
+ require 'sass/plugin/rack'
4
+ require 'json'
5
+
6
+ class Cuttlebone::Drivers::Rack < Cuttlebone::Drivers::Base
7
+
8
+ ##
9
+ #
10
+ # Cuttlebone::Drivers::Rack::Middleware
11
+ #
12
+ # It responds only to predefined HTTP POST calls and deals them according to
13
+ # Cuttlebone definitions and the associated driver's internal state.
14
+ #
15
+ class Middleware
16
+
17
+ def initialize app, driver=nil
18
+ @app = app
19
+ @driver = driver || Cuttlebone::Drivers::Rack.new
20
+ end
21
+
22
+ def call env
23
+ @request = Rack::Request.new(env)
24
+
25
+ return(@app.call(env)) unless @request.post?
26
+ return(@app.call(env)) unless @request.path =~ %r{^/(?:(?:prompt|call)/([0-9a-f]{40}))|init$}
27
+ # return(@app.call(env)) unless @request.is_json_request?
28
+
29
+ @response = Rack::Response.new
30
+ @response['Content-Type'] = 'application/json'
31
+
32
+ if @request.path == '/init'
33
+ session = @driver.sessions.create
34
+ json 'id' => session.id, 'prompt' => session.prompt
35
+ elsif @request.path =~ %r{/prompt/([0-9a-f]{40})}
36
+ id = $1
37
+ session = @driver.sessions[id]
38
+ json 'id' => session.id, 'prompt' => session.prompt
39
+ elsif @request.path =~ %r{/call/([0-9a-f]{40})}
40
+ id = $1
41
+ session = @driver.sessions[id]
42
+
43
+ if command=@request.params['command']
44
+ _, _, output, error = session.call(command.force_encoding("UTF-8"))
45
+ json 'id' => session.id, 'prompt' => session.prompt, 'output' => output, 'error' => error
46
+ else
47
+ @response.status = 409
48
+ json 'id' => session.id, 'prompt' => session.prompt, 'error' => 'No command was given!'
49
+ end
50
+ end
51
+
52
+ return(@response.finish)
53
+ rescue
54
+ json 'id' => (session.id rescue nil), 'prompt' => (session.prompt rescue nil), 'error' => $!.message
55
+ return(@response.finish)
56
+ end
57
+
58
+ private
59
+
60
+ def json data
61
+ @response.write(data.to_json)
62
+ end
63
+ end
64
+
65
+ ##
66
+ #
67
+ # Cuttlebone::Driver::Rack::Application
68
+ #
69
+ # It's a fully functional rack-enabled application that serves all other
70
+ # materials for a fully functional Cuttlebone web server.
71
+ #
72
+ class Application
73
+
74
+ STATIC_FILES = %w{ /index.html /favicon.png /stylesheets/cuttlebone.css /javascripts/cuttlebone.js /javascripts/jquery.min.js }
75
+
76
+ def self.public_path path=''
77
+ File.expand_path("../../../../public/#{path}", __FILE__)
78
+ end
79
+
80
+ def call(env)
81
+ @request = Rack::Request.new(env)
82
+ @response = Rack::Response.new
83
+
84
+ if @request.get? and @request.path == '/'
85
+ redirect_to '/index.html'
86
+ elsif @request.get? and STATIC_FILES.include?(@request.path)
87
+ static @request.path
88
+ else
89
+ error 'Not found.', 404
90
+ end
91
+
92
+ @response.finish
93
+ end
94
+
95
+ private
96
+
97
+ def static path
98
+ @response.write(File.read(File.expand_path("../../../../public#{path}", __FILE__)))
99
+ end
100
+
101
+ def error message, status=500
102
+ @response.write(File.read(File.expand_path("../../../../public/error.html", __FILE__)).gsub(/Error happens. It always does./, message))
103
+ @response.status = status
104
+ end
105
+
106
+ def redirect_to path
107
+ @response.redirect(path)
108
+ end
109
+ end
110
+
111
+ ##
112
+ #
113
+ # Starts Cuttlebone Rack application.
114
+ #
115
+ def run
116
+ trap(:INT) do
117
+ if server.respond_to?(:shutdown)
118
+ server.shutdown
119
+ else
120
+ exit
121
+ end
122
+ end
123
+ server.run app, :Host=>'0.0.0.0', :Port=>9292
124
+ end
125
+
126
+ private
127
+
128
+ ##
129
+ #
130
+ # Builds Rack application stack.
131
+ #
132
+ # @return Rack::Application
133
+ #
134
+ def app
135
+ driver = self
136
+ Rack::Builder.app do
137
+ use Rack::ShowExceptions
138
+ use Rack::Lint
139
+ use Rack::ContentType
140
+ use Rack::ContentLength
141
+ use Rack::Session::Pool, :expire_after => 2592000
142
+ use Middleware, driver
143
+ run Application.new
144
+ end
145
+ end
146
+
147
+ ##
148
+ #
149
+ # Returns a Rack server.
150
+ #
151
+ def server
152
+ @server ||= ::Rack::Handler.default()
153
+ end
154
+ end
@@ -1,14 +1,15 @@
1
1
  # TODO FIXME load readline iff it was invoked through command line interface
2
2
  require 'readline'
3
3
 
4
- class Cuttlebone::Session::Shell < Cuttlebone::Session::Base
4
+ class Cuttlebone::Drivers::Shell < Cuttlebone::Drivers::Base
5
5
  def run
6
+ session = Cuttlebone::Session.sessions.create
6
7
  loop do
7
- break if terminated?
8
- command = Readline::readline("#{prompt} > ")
8
+ break if session.terminated?
9
+ command = Readline::readline("#{session.prompt} > ")
9
10
  break unless command
10
11
  Readline::HISTORY.push(command)
11
- _, _, output, error = call(command)
12
+ _, _, output, error = session.call(command)
12
13
  print (output<<'').join("\n")
13
14
  print "\033[01;31m#{error}\033[00m\n" if error
14
15
  end
@@ -1,4 +1,91 @@
1
- module Cuttlebone::Session
2
- autoload :Base, 'cuttlebone/session/base'
3
- autoload :Shell, 'cuttlebone/session/shell'
1
+ require 'digest/sha1'
2
+
3
+ class Cuttlebone::Session
4
+
5
+ # TODO FIXME locking / mutexes / serialization / deserialization
6
+ class NotFound < StandardError; end
7
+
8
+ @@sessions = {}
9
+
10
+ @@default_stack_objects = []
11
+
12
+ module SessionCollectionExtensions
13
+ def [] key
14
+ raise NotFound unless has_key?(key)
15
+ super(key)
16
+ end
17
+
18
+ def create *stack_objects
19
+ s = Cuttlebone::Session.new(*stack_objects)
20
+ send(:[]=, s.id, s)
21
+ s
22
+ end
23
+ end
24
+
25
+ @@sessions.extend SessionCollectionExtensions
26
+
27
+ def self.set_default *stack_objects
28
+ @@default_stack_objects = stack_objects
29
+ end
30
+
31
+ def self.sessions
32
+ @@sessions
33
+ end
34
+
35
+ def initialize *stack_objects
36
+ options = stack_objects.extract_options!
37
+ @id = options[:id] || Digest::SHA1.hexdigest(Time.now.to_s + Time.now.usec.to_s + rand(1000).to_s) # TODO FIXME
38
+ @@default_stack_objects.each { |so| stack_objects << (so.dup rescue so) } if stack_objects.empty?
39
+ @stack ||= stack_objects.map{ |o| Cuttlebone::Controller.new(self, o) }
40
+ rescue Cuttlebone::InvalidContextError => e
41
+ @internal_error = %{Context initialization failed for #{e.context.inspect}!}
42
+ rescue => e
43
+ @internal_error = %{Internal error occured: #{e.message} (#{e.class})}
44
+ ensure
45
+ @stack ||= []
46
+ end
47
+
48
+ attr_reader :id, :stack, :internal_error
49
+
50
+ def active_context
51
+ stack.last
52
+ end
53
+
54
+ delegate :name, :prompt, :to => :active_context
55
+
56
+ def call command
57
+ process command
58
+ end
59
+
60
+ def terminated?
61
+ stack.empty?
62
+ end
63
+
64
+ private
65
+
66
+ def process command
67
+ active_context = stack.pop
68
+
69
+ action, next_context, output, error = begin
70
+ a, n, o, e = active_context.process(command)
71
+ raise ArgumentError, "Unknown action: #{a}" unless [ :self, :replace, :add, :drop ].include?(a.to_s.to_sym)
72
+ raise TypeError, "Output must be an instance of String or nil!" unless o.is_a?(Array) or o.nil?
73
+ raise TypeError, "Error must be an instance of String or nil!" unless e.is_a?(String) or e.nil?
74
+ [ a.to_s.to_sym, n, o, e ]
75
+ rescue => e
76
+ [ :self, active_context, nil, %{Cuttlebone::Session: #{e.message} (#{e.class})} ]
77
+ end
78
+ case action
79
+ when :self
80
+ stack << active_context
81
+ when :replace
82
+ stack << Cuttlebone::Controller.new(self, next_context)
83
+ when :add
84
+ stack << active_context << Cuttlebone::Controller.new(self, next_context)
85
+ when :drop
86
+ # noop
87
+ end
88
+ [ action, next_context, output, error ]
89
+ end
90
+
4
91
  end
@@ -1,3 +1,3 @@
1
1
  module Cuttlebone
2
- VERSION = "0.1.3"
2
+ VERSION = "0.1.4"
3
3
  end
data/lib/cuttlebone.rb CHANGED
@@ -8,11 +8,17 @@ module Cuttlebone
8
8
  autoload :Definition, 'cuttlebone/definition'
9
9
  autoload :Session, 'cuttlebone/session'
10
10
 
11
+ module Drivers
12
+ autoload :Base, 'cuttlebone/drivers/base'
13
+ autoload :Shell, 'cuttlebone/drivers/shell'
14
+ autoload :Rack, 'cuttlebone/drivers/rack'
15
+ end
16
+
11
17
  @@definitions = []
12
18
  def self.definitions; @@definitions; end
13
19
 
14
- def self.run starting_objects, default_driver=Session::Shell
15
- default_driver.new(starting_objects).run
20
+ def self.run stack_objects, default_driver=Drivers::Shell
21
+ default_driver.new(stack_objects).run
16
22
  end
17
23
  end
18
24
 
data/public/error.html ADDED
@@ -0,0 +1,21 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Cuttlebone - Error</title>
5
+ <link href='/stylesheets/cuttlebone.css?1301248065' rel='stylesheet' type='text/css'>
6
+ </head>
7
+ <body>
8
+ <div class='output'>
9
+ <ul>
10
+ <li class='error'>Error happens. It always does.</li>
11
+ </ul>
12
+ </div>
13
+ <div class='actions'>
14
+ <ul>
15
+ <li>
16
+ <a href='/'>back to console</a>
17
+ </li>
18
+ </ul>
19
+ </div>
20
+ </body>
21
+ </html>
File without changes
data/public/index.html ADDED
@@ -0,0 +1,20 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Cuttlebone</title>
5
+ <link href='/favicon.png?1301248065' rel='icon' type='image/png'>
6
+ <link href='/stylesheets/cuttlebone.css?1301248065' rel='stylesheet' type='text/css'>
7
+ <script src='/javascripts/jquery.min.js?1301248065' type='text/javascript'></script>
8
+ <script src='/javascripts/cuttlebone.js?1301248065' type='text/javascript'></script>
9
+ </head>
10
+ <body>
11
+ <div class='output'>
12
+ <ul></ul>
13
+ </div>
14
+ <div class='input'>
15
+ <span id='prompt'></span>
16
+ <span>&gt;</span>
17
+ <input id='input' name='command' type='text'>
18
+ </div>
19
+ </body>
20
+ </html>
@@ -0,0 +1,41 @@
1
+ // I know that it's ugly, but this is a POC/WIP project.
2
+ // TODO:
3
+ // * get rid of that global variable
4
+ // * #input should determine (with a special html5 attribute) which session
5
+ // will it talk to
6
+ // * #input should know about the updated output too
7
+ // * create a convenience function that sets up and handles all the magic
8
+
9
+ var cuttlebone_session_id;
10
+
11
+ jQuery(document).ready(function(){
12
+ // updates prompt
13
+ jQuery.ajax({type:'POST',url:"/init",dataType:"json",success:function(d){if(d['id']){cuttlebone_session_id=d['id'];};if(d['prompt']){jQuery('#prompt').html(d['prompt']);};}});
14
+ });
15
+
16
+ // evaluates input
17
+ jQuery("#input").live("keypress", function(e) {
18
+ if (e.keyCode == 13) {
19
+ jQuery.ajax({
20
+ type: "POST",
21
+ url: "/call/"+cuttlebone_session_id,
22
+ data: {command:jQuery('#input').val()},
23
+ dataType: "json",
24
+ success: function(d){
25
+ if (d['output']) {
26
+ jQuery.each(d['output'], function(i,v) {
27
+ jQuery('.output ul').append('<li><pre>'+v+'</pre></li>');
28
+ });
29
+ };
30
+ if (d['error']) {
31
+ jQuery('.output ul').append('<li class="error">'+d['error']+'</li>');
32
+ };
33
+ if (d['prompt']) {
34
+ jQuery('#prompt').html(d['prompt']);
35
+ }
36
+ }
37
+ });
38
+ $(this).val('');
39
+ };
40
+ });
41
+