cuttlebone 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
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
+