apify 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/.gitignore +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +25 -0
  4. data/Rakefile +40 -0
  5. data/VERSION +1 -0
  6. data/apify.gemspec +103 -0
  7. data/app/helpers/apify_helper.rb +14 -0
  8. data/app/views/apify/api/_actions.html.erb +38 -0
  9. data/app/views/apify/api/_client.html.erb +38 -0
  10. data/app/views/apify/api/_overview.html.erb +9 -0
  11. data/app/views/apify/api/_protocol.html.erb +38 -0
  12. data/app/views/apify/api/docs.html.erb +66 -0
  13. data/lib/apify.rb +7 -0
  14. data/lib/apify/action.rb +98 -0
  15. data/lib/apify/api.rb +65 -0
  16. data/lib/apify/api_controller.rb +99 -0
  17. data/lib/apify/client.rb +52 -0
  18. data/lib/apify/errors.rb +12 -0
  19. data/lib/apify/exchange.rb +53 -0
  20. data/lib/apify/schema_helper.rb +56 -0
  21. data/spec/apify/action_spec.rb +35 -0
  22. data/spec/apify/client_spec.rb +24 -0
  23. data/spec/app_root/app/controllers/api_controller.rb +8 -0
  24. data/spec/app_root/app/controllers/application_controller.rb +3 -0
  25. data/spec/app_root/app/models/api.rb +42 -0
  26. data/spec/app_root/config/boot.rb +114 -0
  27. data/spec/app_root/config/database.yml +21 -0
  28. data/spec/app_root/config/environment.rb +14 -0
  29. data/spec/app_root/config/environments/in_memory.rb +0 -0
  30. data/spec/app_root/config/environments/mysql.rb +0 -0
  31. data/spec/app_root/config/environments/postgresql.rb +0 -0
  32. data/spec/app_root/config/environments/sqlite.rb +0 -0
  33. data/spec/app_root/config/environments/sqlite3.rb +0 -0
  34. data/spec/app_root/config/routes.rb +12 -0
  35. data/spec/app_root/lib/console_with_fixtures.rb +4 -0
  36. data/spec/app_root/script/console +7 -0
  37. data/spec/controllers/api_controller_spec.rb +155 -0
  38. data/spec/rcov.opts +2 -0
  39. data/spec/spec.opts +4 -0
  40. data/spec/spec_helper.rb +31 -0
  41. metadata +151 -0
@@ -0,0 +1,65 @@
1
+ module Apify
2
+ class Api
3
+ class << self
4
+
5
+ def action(method, name, &block)
6
+ method = method.to_sym
7
+ name = name.to_sym
8
+ if block
9
+ action = Apify::Action.new(method, name, &block)
10
+ indexed_actions[name][method] = action
11
+ actions << action
12
+ else
13
+ indexed_actions[name][method] or raise "Unknown API action: #{name}"
14
+ end
15
+ end
16
+
17
+ def get(name, &block)
18
+ action(:get, name, &block)
19
+ end
20
+
21
+ def post(name, &block)
22
+ action(:post, name, &block)
23
+ end
24
+
25
+ def put(name, &block)
26
+ action(:put, name, &block)
27
+ end
28
+
29
+ def delete(name, &block)
30
+ action(:delete, name, &block)
31
+ end
32
+
33
+ def actions
34
+ @actions ||= []
35
+ end
36
+
37
+ def indexed_actions
38
+ @indexed_actions ||= Hash.new { |hash, k| hash[k] = {} }
39
+ end
40
+
41
+ def draw_routes(map, options = {})
42
+ options[:base_path] ||= 'api'
43
+ options[:controller] ||= 'api'
44
+ indexed_actions.each do |name, methods|
45
+ methods.each do |method, action|
46
+ connect_route(map, name, method, options)
47
+ end
48
+ end
49
+ connect_route(map, 'docs', :get, options)
50
+ end
51
+
52
+ private
53
+
54
+ def connect_route(map, name, method, options)
55
+ options = options.dup
56
+ base_path = options.delete :base_path
57
+ map.connect(
58
+ base_path ? "#{base_path}/#{name}" : name,
59
+ options.merge(:action => name.to_s, :conditions => { :method => method })
60
+ )
61
+ end
62
+
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,99 @@
1
+ module Apify
2
+ class ApiController < ActionController::Base
3
+
4
+ helper ApifyHelper
5
+
6
+ class << self
7
+
8
+ def authenticate(options)
9
+ @user = options[:user] or raise "Missing user for authentication"
10
+ @password = options[:password] or raise "Missing password for authentication"
11
+ @authenticate_condition = options[:if] || lambda { true }
12
+ end
13
+
14
+ def api(api)
15
+ @api = api
16
+ @api.indexed_actions.each do |name, methods|
17
+ define_method name do
18
+ if action = methods[request.method]
19
+ if params[:schema].present?
20
+ render_schema(params[:schema], action)
21
+ elsif params[:example].present?
22
+ render_example(params[:example], action)
23
+ else
24
+ respond_with_action(action)
25
+ end
26
+ else
27
+ render_method_not_allowed(request.method)
28
+ end
29
+ end
30
+ end
31
+ @api
32
+ end
33
+
34
+ end
35
+
36
+ skip_before_filter :verify_authenticity_token
37
+
38
+ before_filter :require_api_authentication
39
+
40
+ def docs
41
+ render 'apify/api/docs', :layout => false
42
+ end
43
+
44
+ def api
45
+ configuration(:api) || self.class.api(::Api)
46
+ end
47
+
48
+ def authentication_configured?
49
+ !!@authenticate_condition
50
+ end
51
+
52
+ helper_method :api, :authentication_configured?
53
+
54
+ private
55
+
56
+ def configuration(symb)
57
+ self.class.instance_variable_get("@#{symb}")
58
+ end
59
+
60
+ def respond_with_action(action)
61
+ args = params[:args].present? ? JSON.parse(params[:args]) : {}
62
+ exchange = action.respond(args)
63
+ render_api_response(exchange, "#{action.name}.json")
64
+ end
65
+
66
+ def render_api_response(exchange, filename)
67
+ # p exchange.value
68
+ if exchange.successful?
69
+ send_data exchange.value.to_json, :status => '200', :type => 'application/json', :filename => filename
70
+ else
71
+ send_data exchange.value, :status => '500', :type => 'text/plain', :filename => filename
72
+ end
73
+ end
74
+
75
+ def render_method_not_allowed(method)
76
+ send_data "Method not allowed: #{method}", :status => 405, :type => 'text/plain'
77
+ end
78
+
79
+ def render_schema(nature, action)
80
+ send_data JSON.pretty_generate(action.schema(nature)), :status => 200, :type => "application/schema+json", :filename => "#{action.name}.schema.json"
81
+ end
82
+
83
+ def render_example(nature, action)
84
+ send_data JSON.pretty_generate(action.example(nature)), :status => 200, :type => "application/json", :filename => "#{action.name}.example.json"
85
+ end
86
+
87
+ def require_api_authentication
88
+ condition = configuration(:authenticate_condition)
89
+ if condition && instance_eval(&condition)
90
+ authenticate_or_request_with_http_basic do |user, password|
91
+ required_user = configuration(:user)
92
+ required_password = configuration(:password)
93
+ required_user.present? && required_password.present? && user.strip == required_user && password.strip == required_password
94
+ end
95
+ end
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,52 @@
1
+ module Apify
2
+ class Client
3
+
4
+ def initialize(options)
5
+ @host = options[:host] or raise "Missing :host parameter"
6
+ @port = options[:port]
7
+ @protocol = options[:protocol] || 'http'
8
+ @user = options[:user]
9
+ @password = options[:password]
10
+ end
11
+
12
+ def get(*args)
13
+ request(:get, *args)
14
+ end
15
+
16
+ def post(*args)
17
+ request(:post, *args)
18
+ end
19
+
20
+ def put(*args)
21
+ request(:put, *args)
22
+ end
23
+
24
+ def delete(*args)
25
+ request(:delete, *args)
26
+ end
27
+
28
+ private
29
+
30
+ def request(method, path, args = nil)
31
+ url = build_url(path)
32
+ args ||= {}
33
+ json = RestClient.send(method, url, :args => args.to_json)
34
+ JSON.parse(json)
35
+ rescue RestClient::Unauthorized => e
36
+ raise Apify::RequestFailed.new("Unauthorized")
37
+ rescue RestClient::ExceptionWithResponse => e
38
+ raise Apify::RequestFailed.new("API request failed with status #{e.http_code}", e.http_body)
39
+ end
40
+
41
+ def build_url(path)
42
+ url = ""
43
+ url << @protocol
44
+ url << '://'
45
+ url << "#{@user}:#{@password}@" if @user
46
+ url << @host
47
+ url << ":#{@port}" if @port
48
+ url << path
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,12 @@
1
+ module Apify
2
+ class RequestFailed < StandardError
3
+
4
+ attr_reader :response_body
5
+
6
+ def initialize(message, response_body = nil)
7
+ super(message)
8
+ @response_body = response_body
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,53 @@
1
+
2
+ module Apify
3
+ class Exchange
4
+
5
+ attr_reader :args, :value
6
+
7
+ def initialize
8
+ @value = nil
9
+ @error = false
10
+ @args = nil
11
+ end
12
+
13
+ def successful?
14
+ not @error
15
+ end
16
+
17
+ def respond(args, action)
18
+ logging_errors do
19
+ @args = args
20
+ validate(@args, action.schema(:args), 'Invalid request args')
21
+ @value = instance_eval(&action.responder) || {}
22
+ validate(@value, action.schema(:value), 'Invalid response value')
23
+ end
24
+ successful?
25
+ end
26
+
27
+ private
28
+
29
+ attr_writer :value
30
+
31
+ def validate(object, schema, message_prefix)
32
+ JSON::Schema.validate(object, schema) if schema
33
+ rescue JSON::Schema::ValueError => e
34
+ raise "#{message_prefix}: #{e.message}"
35
+ end
36
+
37
+ def logging_errors(&block)
38
+ block.call
39
+ rescue Exception => e
40
+ @error = true
41
+ @value = e.message
42
+ end
43
+
44
+ def sql_datetime(time)
45
+ time.present? ? time.strftime("%Y-%m-%d %H:%M:%S") : nil
46
+ end
47
+
48
+ def sql_date(date)
49
+ date.present? ? date.strftime("%Y-%m-%d") : nil
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,56 @@
1
+ module Apify
2
+ module SchemaHelper
3
+
4
+ def optional(schema)
5
+ schema.merge('optional' => true)
6
+ end
7
+
8
+ def object(properties = nil)
9
+ { 'type' => 'object' }.tap do |schema|
10
+ schema['properties'] = properties if properties
11
+ end
12
+ end
13
+
14
+ def array(items = nil)
15
+ { 'type' => 'array' }.tap do |schema|
16
+ schema['items'] = items if items
17
+ end
18
+ end
19
+
20
+ def string
21
+ { 'type' => 'string' }
22
+ end
23
+
24
+ def boolean
25
+ { 'type' => 'boolean' }
26
+ end
27
+
28
+ def enum(schema, allowed_values)
29
+ schema.merge('enum' => allowed_values)
30
+ end
31
+
32
+ def number
33
+ { 'type' => 'number' }
34
+ end
35
+
36
+ def integer
37
+ { 'type' => 'integer' }
38
+ end
39
+
40
+ def sql_date
41
+ { 'type' => 'string',
42
+ 'pattern' => Patterns::SQL_DATE.source }
43
+ end
44
+
45
+ def sql_datetime
46
+ { 'type' => 'string',
47
+ 'pattern' => Patterns::SQL_DATETIME.source }
48
+ end
49
+
50
+ def email
51
+ { 'type' => 'string',
52
+ 'pattern' => Patterns::EMAIL.source }
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe Apify::Action do
4
+
5
+ describe '#uid' do
6
+
7
+ it "should return a string uniquely describing the action within an API" do
8
+ action = Apify::Action.new(:post, :hello_world) {}
9
+ action.uid.should == 'post_hello_world'
10
+ end
11
+
12
+ end
13
+
14
+ describe '#example' do
15
+
16
+ it "should return an example for a JSON object matching the schema" do
17
+ action = Apify::Action.new(:post, :hello_world) do
18
+ schema :value do
19
+ object(
20
+ 'string_property' => string,
21
+ 'integer_property' => integer
22
+ )
23
+ end
24
+ end
25
+
26
+ action.example(:value).should == {
27
+ 'string_property' => 'string',
28
+ 'integer_property' => 123
29
+ }
30
+
31
+ end
32
+
33
+ end
34
+
35
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe Apify::Client do
4
+
5
+ it "raise an exception if authorization is missing" do
6
+ client = Apify::Client.new(:host => 'host')
7
+ stub_http_request(:get, 'http://host/api').to_return(:status => 401, :body => 'the body')
8
+ expect { client.get '/api' }.to raise_error(Apify::RequestFailed)
9
+ end
10
+
11
+ it "raise an exception if there is a server error" do
12
+ client = Apify::Client.new(:host => 'host')
13
+ stub_http_request(:get, 'http://host/api').to_return(:status => 500, :body => 'the body')
14
+ expect { client.get '/api' }.to raise_error(Apify::RequestFailed)
15
+ end
16
+
17
+ it "should return the parsed JSON object if the request went through" do
18
+ client = Apify::Client.new(:host => 'host')
19
+ stub_http_request(:get, 'http://host/api').to_return(:status => 200, :body => '{ "key": "value" }')
20
+ client.get('/api').should == { 'key' => 'value' }
21
+ end
22
+
23
+ end
24
+
@@ -0,0 +1,8 @@
1
+ class ApiController < Apify::ApiController
2
+
3
+ attr_accessor :skip_authentication
4
+
5
+ api Api
6
+ authenticate :user => 'user', :password => 'password', :if => lambda { !skip_authentication }
7
+
8
+ end
@@ -0,0 +1,3 @@
1
+ class ApplicationController < ActionController::Base
2
+
3
+ end
@@ -0,0 +1,42 @@
1
+ class Api < Apify::Api
2
+
3
+ post :ping do
4
+ respond do
5
+ { 'message' => 'pong' }
6
+ end
7
+ end
8
+
9
+ post :fail do
10
+ respond do
11
+ raise "error message"
12
+ end
13
+ end
14
+
15
+ post :echo_args do
16
+ respond do
17
+ args
18
+ end
19
+ end
20
+
21
+ post :with_args_schema do
22
+ schema :args do
23
+ { "type" => "object",
24
+ "properties" => {
25
+ "string_arg" => string
26
+ }}
27
+ end
28
+ end
29
+
30
+ post :with_value_schema do
31
+ schema :value do
32
+ { "type" => "object",
33
+ "properties" => {
34
+ "string_value" => string
35
+ }}
36
+ end
37
+ respond do
38
+ args
39
+ end
40
+ end
41
+
42
+ end