houdini 0.2.4 → 0.3.0

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