houdini 0.2.4 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/.gitignore +3 -1
  2. data/Gemfile.lock +97 -88
  3. data/README.markdown +31 -27
  4. data/app/controllers/houdini/postbacks_controller.rb +7 -9
  5. data/config/routes.rb +2 -2
  6. data/houdini.gemspec +3 -2
  7. data/lib/houdini.rb +35 -7
  8. data/lib/houdini/model.rb +14 -49
  9. data/lib/houdini/postback_processor.rb +21 -0
  10. data/lib/houdini/task.rb +46 -10
  11. data/lib/houdini/task_manager.rb +20 -0
  12. data/lib/houdini/version.rb +1 -1
  13. data/spec/dummy/app/models/article.rb +12 -14
  14. data/spec/houdini/model_spec.rb +27 -0
  15. data/spec/houdini/postback_processor_spec.rb +44 -0
  16. data/spec/houdini/task_manager_spec.rb +26 -0
  17. data/spec/houdini/task_spec.rb +136 -0
  18. data/spec/houdini_spec.rb +25 -0
  19. data/spec/requests/integration_spec.rb +21 -19
  20. data/spec/spec_helper.rb +3 -3
  21. metadata +76 -118
  22. data/lib/houdini/base.rb +0 -35
  23. data/spec/controllers/houdini/postbacks_controller_spec.rb +0 -0
  24. data/spec/dummy/config/initializers/backtrace_silencers.rb +0 -7
  25. data/spec/dummy/config/initializers/inflections.rb +0 -10
  26. data/spec/dummy/config/initializers/mime_types.rb +0 -5
  27. data/spec/dummy/public/404.html +0 -26
  28. data/spec/dummy/public/422.html +0 -26
  29. data/spec/dummy/public/500.html +0 -26
  30. data/spec/dummy/public/favicon.ico +0 -0
  31. data/spec/dummy/public/javascripts/application.js +0 -2
  32. data/spec/dummy/public/javascripts/controls.js +0 -965
  33. data/spec/dummy/public/javascripts/dragdrop.js +0 -974
  34. data/spec/dummy/public/javascripts/effects.js +0 -1123
  35. data/spec/dummy/public/javascripts/prototype.js +0 -6001
  36. data/spec/dummy/public/javascripts/rails.js +0 -175
  37. data/spec/dummy/public/stylesheets/.gitkeep +0 -0
  38. data/spec/houdini_rails_spec.rb +0 -4
@@ -0,0 +1,21 @@
1
+ module Houdini
2
+ module PostbackProcessor
3
+ EnvironmentMismatchError = Class.new RuntimeError
4
+ APIKeyMistmatchError = Class.new RuntimeError
5
+
6
+ def self.process(class_name, model_id, params)
7
+ task_manager = params.delete(:task_manager) || TaskManager
8
+
9
+ if params[:environment] != Houdini.environment
10
+ raise EnvironmentMismatchError, "Environment received does not match Houdini.environment"
11
+ end
12
+
13
+ # # Houdini doesn't send the API key back
14
+ # if params[:api_key] != Houdini.api_key and params[:environment] == "production"
15
+ # raise APIKeyMistmatchError, "API key received doesn't match our API key."
16
+ # end
17
+
18
+ task_manager.process class_name, model_id, params[:blueprint], params[:output], params[:verbose_output]
19
+ end
20
+ end
21
+ end
data/lib/houdini/task.rb CHANGED
@@ -1,15 +1,51 @@
1
1
  module Houdini
2
2
  class Task
3
- attr_accessor :short_name, :version, :task_info, :on, :if, :after_submit, :on_task_completion
4
-
5
- def initialize(short_name, options)
6
- @short_name = short_name.to_s
7
- @version = options[:version]
8
- @task_info = options[:task_info]
9
- @on = options[:on] || :after_create
10
- @if = options[:if] || true
11
- @after_submit = options[:after_submit]
12
- @on_task_completion = options[:on_task_completion] || :update_attributes
3
+ attr :options
4
+ protected :options
5
+
6
+ def initialize(klass, blueprint, options={})
7
+ @class_name = klass.name
8
+ @blueprint = blueprint.to_s
9
+ @input = options[:input]
10
+ @after_submit = options[:after_submit]
11
+ @on_task_completion = options[:on_task_completion]
12
+ @finder = options[:finder] || :find
13
+ @id_method = options[:id_method] || :id
14
+ end
15
+
16
+ def process(object_id, results, verbose_results)
17
+ # we have to re-constantize the class name because Rails reloads the classes in development
18
+ obj = self.class.callable @class_name.constantize, @finder, object_id
19
+ self.class.callable obj, @on_task_completion, results, verbose_results if @on_task_completion
20
+ end
21
+
22
+ def submit!(object, options={})
23
+ get_input = options[:input_transformer] || self.class.method(:get_input)
24
+ submit_task = options[:submit_task] || Houdini.method(:submit!)
25
+
26
+ id = self.class.callable object, @id_method
27
+
28
+ submit_task.call @blueprint, object.class.name, id, get_input.call(object, @input)
29
+
30
+ self.class.callable(object, @after_submit) if @after_submit
31
+ end
32
+
33
+ def self.get_input(object, input)
34
+ input.inject({}) do |hash, (info_name, model_attribute)|
35
+ hash.merge info_name => callable(object, model_attribute){ model_attribute }
36
+ end
37
+ end
38
+
39
+ def self.callable(object, callable, *args)
40
+ if callable.is_a? Symbol
41
+ object.send callable, *args
42
+ elsif callable.respond_to? :call
43
+ object.instance_exec *args, &callable
44
+ elsif block_given?
45
+ yield callable, *args
46
+ else
47
+ raise "#{callable.inspect} is not a Symbol and does not respond to #call"
48
+ end
13
49
  end
14
50
  end
15
51
  end
@@ -0,0 +1,20 @@
1
+ module Houdini
2
+ module TaskManager
3
+ def self.register(klass, blueprint, options, task_builder=Task)
4
+ @tasks ||= {}
5
+ @tasks[ [klass.name, blueprint.to_sym] ] = task_builder.new(klass, blueprint, options)
6
+ end
7
+
8
+ def self.submit!(object, blueprint)
9
+ if @tasks
10
+ task = @tasks[ [object.class.name, blueprint.to_sym] ]
11
+ task.submit! object
12
+ end
13
+ end
14
+
15
+ def self.process(class_name, id, blueprint, output, verbose_output)
16
+ task = @tasks[ [class_name, blueprint.to_sym] ]
17
+ task.process id, output, verbose_output
18
+ end
19
+ end
20
+ end
@@ -1,3 +1,3 @@
1
1
  module Houdini
2
- VERSION = "0.2.4"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -2,24 +2,22 @@ class Article < ActiveRecord::Base
2
2
  include Houdini::Model
3
3
 
4
4
  houdini :edit_for_grammar,
5
- :version => 1,
6
- :task_info => {
7
- 'original_text' => :original_text,
5
+ :input => {
6
+ 'input1' => :original_text,
7
+ 'input2' => proc{ original_text },
8
+ 'input3' => "some text"
8
9
  },
9
- :after_submit => :update_houdini_attributes,
10
- :on_task_completion => :process_houdini_edited_text
11
-
12
- after_create :moderate_image, :if => :original_text
13
-
14
- def moderate_image
15
- Houdini.perform!(:edit_for_grammar, self)
16
- end
10
+ :on => :after_create,
11
+ :after_submit => :update_houdini_attributes,
12
+ :on_task_completion => :process_houdini_edited_text,
13
+ :finder => lambda{|id| last },
14
+ :id_method => lambda{ 'model-slug' }
17
15
 
18
16
  def update_houdini_attributes
19
- update_attribute(:houdini_request_sent_at, Time.now)
17
+ update_attribute(:houdini_request_sent_at, Date.today.to_time)
20
18
  end
21
19
 
22
- def process_houdini_edited_text(params)
23
- update_attribute(:edited_text, params[:edited_text])
20
+ def process_houdini_edited_text(output, verbose_output)
21
+ update_attribute(:edited_text, output[:edited_text])
24
22
  end
25
23
  end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ describe Houdini::Model do
4
+ let :test_model do
5
+ Class.new do
6
+ include Houdini::Model
7
+ end
8
+ end
9
+
10
+ describe ".houdini" do
11
+ it "should define a method that submits the task" do
12
+ task_manager = double :task_manager
13
+ task_manager.should_receive(:register).with(test_model, :blueprint_name, :other_options => {})
14
+ test_model.houdini :blueprint_name, :task_manager => task_manager, :other_options => {}
15
+
16
+ test_model_instance = test_model.new
17
+
18
+ task_manager.should_receive(:submit!).with(test_model_instance, :blueprint_name)
19
+ test_model_instance.houdini_submit_blueprint_name!
20
+ end
21
+
22
+ it "should attach callback methods" do
23
+ test_model.should_receive(:after_create).with(:houdini_submit_blueprint_name!)
24
+ test_model.houdini :blueprint_name, :on => :after_create
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ describe Houdini::PostbackProcessor, ".process!" do
4
+ let(:houdini_settings) do
5
+ { :environment => "production", :api_key => "MY_KEY", :app_url => "http://localhost:3000" }
6
+ end
7
+ context "when the environments dont match" do
8
+ before do
9
+ Houdini.setup "production", houdini_settings
10
+ end
11
+
12
+ it "should raise an EnvironmentMismatchError" do
13
+ lambda{
14
+ Houdini::PostbackProcessor.process 'Class', 42, :environment => "sandbox"
15
+ }.should raise_error(Houdini::PostbackProcessor::EnvironmentMismatchError)
16
+ end
17
+ end
18
+
19
+ context "when in production and given API key doesn't match" do
20
+ it "should raise an APIKeyMistmatchError" do
21
+ pending "Houdini doesn't send the API key back."
22
+ lambda{
23
+ Houdini::PostbackProcessor.process 'Class', 42, :environment => "production", :api_key => "ANOTHER_KEY"
24
+ }.should raise_error(Houdini::PostbackProcessor::APIKeyMistmatchError)
25
+ end
26
+ end
27
+
28
+ context "given a classname and id" do
29
+ it "should load the object, and have it process the postback, and return the result" do
30
+ task_manager = double :task_manager
31
+ output = double :output
32
+ verbose_output = double :verbose_output
33
+
34
+ task_manager.should_receive(:process).with('MyClass', 42, 'blueprint_name', output, verbose_output)
35
+
36
+ Houdini::PostbackProcessor.process 'MyClass', 42, houdini_settings.merge(
37
+ :blueprint => 'blueprint_name',
38
+ :output => output,
39
+ :verbose_output => verbose_output,
40
+ :task_manager => task_manager
41
+ )
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe Houdini::TaskManager do
4
+ describe ".register, .submit!, and .process" do
5
+ class MyClass; end
6
+
7
+ it "should save it and let us call it again, when we give the right blueprint and class" do
8
+ options = double :options
9
+ task_builder = double :task_builder
10
+ task = double :task
11
+ obj = MyClass.new
12
+ houdini_output = double :houdini_output
13
+ verbose_houdini_output = double :verbose_houdini_output
14
+ processing_result = double :processing_result
15
+
16
+ task_builder.should_receive(:new).with(MyClass, :blueprint_name, options).and_return(task)
17
+ Houdini::TaskManager.register(MyClass, :blueprint_name, options, task_builder)
18
+
19
+ task.should_receive(:submit!).with(obj)
20
+ Houdini::TaskManager.submit!(obj, :blueprint_name)
21
+
22
+ task.should_receive(:process).with(42, houdini_output, verbose_houdini_output).and_return(processing_result)
23
+ Houdini::TaskManager.process('MyClass', 42, 'blueprint_name', houdini_output, verbose_houdini_output).should == processing_result
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,136 @@
1
+ require 'spec_helper'
2
+
3
+ describe Houdini::Task do
4
+ describe "submit!(obj)" do
5
+ class TestModel
6
+ def id; 42; end
7
+ end
8
+
9
+ let(:model_object){ TestModel.new }
10
+ let(:input){ double :input }
11
+ it "should use the input transformer and submit it to houdini" do
12
+ task = Houdini::Task.new TestModel, :blueprint
13
+ submit_task = double(:callable)
14
+ submit_task.should_receive(:call).with('blueprint', "TestModel", 42, input)
15
+ task.submit! model_object, :input_transformer => proc{ input }, :submit_task => submit_task
16
+ end
17
+
18
+ it "should call the after_submit callback, when given a symbol" do
19
+ task = Houdini::Task.new TestModel, :blueprint, :after_submit => :after_submit
20
+ model_object.should_receive :after_submit
21
+ task.submit! model_object, :input_transformer => proc{}, :submit_task => proc{}
22
+ end
23
+
24
+ it "should call the after_submit callback, when given a block" do
25
+ task = Houdini::Task.new TestModel, :blueprint, :after_submit => proc{ after_submit }
26
+ model_object.should_receive :after_submit
27
+ task.submit! model_object, :input_transformer => proc{}, :submit_task => proc{}
28
+ end
29
+
30
+ it "should call the :id you provided to get the id" do
31
+ task = Houdini::Task.new TestModel, :blueprint, :id_method => lambda{ 'model-id' }
32
+ submit_task = double(:callable)
33
+ submit_task.should_receive(:call).with('blueprint', "TestModel", 'model-id', nil)
34
+ task.submit! model_object, :input_transformer => proc{}, :submit_task => submit_task
35
+ end
36
+ end
37
+
38
+ describe ".callable" do
39
+ let(:obj){ double :obj, :some_method => result }
40
+ subject{ Houdini::Task.callable(obj, callable, *args) }
41
+ let(:result){ double :result }
42
+ let(:args){ [double(:arg1), double(:arg2)] }
43
+
44
+ context "given a symbol" do
45
+ let(:callable){ :some_method }
46
+ before{ obj.should_receive(:some_method).with(*args) }
47
+ it{ should == result }
48
+ end
49
+
50
+ context "given a proc" do
51
+ let(:callable){ lambda{|*args| some_method(*args) } }
52
+ before{ obj.should_receive(:some_method).with(*args) }
53
+ it{ should == result }
54
+ end
55
+
56
+ context "given something else" do
57
+ let(:callable){ 42 }
58
+ it "should raise an error" do
59
+ lambda{
60
+ Houdini::Task.callable(obj, callable)
61
+ }.should raise_error
62
+ end
63
+
64
+ context "and also given a block" do
65
+ it "should call the block with the callable and return the result" do
66
+ Houdini::Task.callable(double, 42){|c| c * 2 }.should == 84
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ describe ".get_input" do
73
+ subject{ Houdini::Task.get_input(input_object, input_hash) }
74
+ let(:input_object){ double :obj, :some_method => "some input", :some_method2 => "some more input" }
75
+
76
+ context "given an object and an instance hash" do
77
+ let(:input_hash){ { :input1 => :some_method, :input2 => :some_method2 } }
78
+ it "should build a hash based on the one given, and the values in the object" do
79
+ should == { :input1 => "some input", :input2 => "some more input" }
80
+ end
81
+ end
82
+ context "given symbols" do
83
+ let(:input_hash){ { :input1=>:some_method } }
84
+ it "should get call the corresponding methods" do
85
+ should == { :input1 => "some input" }
86
+ end
87
+ end
88
+ context "given lambdas" do
89
+ let(:input_hash){ { :input1=>lambda{some_method} } }
90
+ it "should use evaluate procs in the context of the given object" do
91
+ should == { :input1 => "some input" }
92
+ end
93
+ end
94
+ context "given procs" do
95
+ let(:input_hash){ { :input1=>proc{some_method} } }
96
+ it "should use evaluate procs in the context of the given object" do
97
+ should == { :input1 => "some input" }
98
+ end
99
+ end
100
+ context "given other values" do
101
+ let(:input_hash){ { :input1=>"some input", :input2=>42 } }
102
+ it "should return the value, if it's not callable" do
103
+ should == input_hash
104
+ end
105
+ end
106
+ end
107
+
108
+ describe "#process" do
109
+ class TestClass
110
+ end
111
+
112
+ it "should call the callback on the object, giving it the houdini output, and verbose output, and return the result" do
113
+ obj = double :obj
114
+ houdini_output = double :houdini_output
115
+ verbose_houdini_output = double :verbose_houdini_output
116
+ process_result = double :process_result
117
+
118
+ task = Houdini::Task.new TestClass, :blueprint_name, :on_task_completion => :after_houdini_completion, :finder => :custom_finder
119
+
120
+ TestClass.should_receive(:custom_finder).with(42).and_return(obj)
121
+ obj.should_receive(:after_houdini_completion).with(houdini_output, verbose_houdini_output).and_return(process_result)
122
+
123
+ task.process 42, houdini_output, verbose_houdini_output
124
+ end
125
+
126
+ it "should reload the class from it's name, so that this can work in Rails dev mode" do
127
+ klass = double :klass, :name => 'TestClass'
128
+
129
+ task = Houdini::Task.new klass, :blueprint_name
130
+
131
+ TestClass.should_receive(:find)
132
+
133
+ task.process 42, double, double
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+
3
+ describe Houdini, ".submit!" do
4
+ it "should call .request with the right paramers" do
5
+ Houdini.setup "some_environment", :api_key =>"SOME-API-KEY", :app_url => "http://localhost:3000"
6
+
7
+ expected_params = {
8
+ :environment => "some_environment",
9
+ :api_key => "SOME-API-KEY",
10
+ :blueprint => :blueprint_name,
11
+ :postback_url => "http://localhost:3000/houdini/ClassName/42/postbacks",
12
+ :input => { :input1 => 1, :input2 => 'two' }
13
+ }
14
+
15
+ Houdini.should_receive(:request).with(expected_params)
16
+
17
+ Houdini.submit! :blueprint_name, "ClassName", 42, :input1 => 1, :input2 => 'two'
18
+ end
19
+
20
+ it "should not send requests if environment is 'test'" do
21
+ Houdini.setup "test", :api_key => "SOME-API-KEY", :app_url => "http://localhost:3000"
22
+ Houdini.should_receive(:request).never
23
+ Houdini.submit! :blueprint_name, "ClassName", 42, :input1 => 1, :input2 => 'two'
24
+ end
25
+ end
@@ -1,35 +1,37 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe "Text Classification" do
3
+ describe "End-to-end" do
4
4
  before do
5
+ Houdini.setup 'production', :app_url => "http://my-app:3333/"
5
6
  Article.delete_all
6
- Houdini::Base.stub!(:request).and_return(["200", "{response:success}"]) # TODO: VCR
7
7
  end
8
8
 
9
9
  it "should send task to Houdini and properly receive the postback" do
10
- p = Article.new(:original_text => 'This is incorect.')
11
- p.stub!(:id).and_return(1)
10
+ article = Article.new :original_text => 'This is incorect.'
12
11
 
13
12
  params = {
14
- "api_key" => Houdini.api_key,
15
- "environment" => Houdini.environment,
16
- "postback_url" => "http://example.com:80/houdini/Article/1/edit_for_grammar/postbacks",
17
- "task_design" => "edit_for_grammar",
18
- "task_design_version" => 1,
19
- "task_info" => {
20
- "original_text" => "This is incorect."
13
+ "api_key" => Houdini.api_key,
14
+ "environment" => Houdini.environment,
15
+ "postback_url" => "http://my-app:3333/houdini/Article/model-slug/postbacks",
16
+ "blueprint" => "edit_for_grammar",
17
+ "input" => {
18
+ "input1" => "This is incorect.",
19
+ "input2" => "This is incorect.",
20
+ "input3" => "some text"
21
21
  }
22
22
  }.symbolize_keys
23
23
 
24
- Houdini::Base.should_receive(:request).with(params)
24
+ Houdini.should_receive(:request).with(params)
25
25
 
26
- p.save
27
- p.reload
28
- p.houdini_request_sent_at.to_date.should == Time.now.to_date
26
+ article.save!
27
+ article.reload
28
+ article.houdini_request_sent_at.should == Date.today.to_time
29
29
 
30
- post "houdini/article/#{p.id}/edit_for_grammar/postbacks", {:edited_text => "This is incorrect."}.to_json
30
+ output_params = {"edited_text"=>"This is incorrect."}
31
31
 
32
- p.reload
33
- p.edited_text.should == "This is incorrect."
32
+ post "houdini/Article/model-slug/postbacks", params.merge("id" => "000000000000", "status"=>"complete", "output" => output_params, "verbose_output"=> output_params).to_json
33
+
34
+ article.reload
35
+ article.edited_text.should == "This is incorrect."
34
36
  end
35
- end
37
+ end