apify 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +25 -0
- data/Rakefile +40 -0
- data/VERSION +1 -0
- data/apify.gemspec +103 -0
- data/app/helpers/apify_helper.rb +14 -0
- data/app/views/apify/api/_actions.html.erb +38 -0
- data/app/views/apify/api/_client.html.erb +38 -0
- data/app/views/apify/api/_overview.html.erb +9 -0
- data/app/views/apify/api/_protocol.html.erb +38 -0
- data/app/views/apify/api/docs.html.erb +66 -0
- data/lib/apify.rb +7 -0
- data/lib/apify/action.rb +98 -0
- data/lib/apify/api.rb +65 -0
- data/lib/apify/api_controller.rb +99 -0
- data/lib/apify/client.rb +52 -0
- data/lib/apify/errors.rb +12 -0
- data/lib/apify/exchange.rb +53 -0
- data/lib/apify/schema_helper.rb +56 -0
- data/spec/apify/action_spec.rb +35 -0
- data/spec/apify/client_spec.rb +24 -0
- data/spec/app_root/app/controllers/api_controller.rb +8 -0
- data/spec/app_root/app/controllers/application_controller.rb +3 -0
- data/spec/app_root/app/models/api.rb +42 -0
- data/spec/app_root/config/boot.rb +114 -0
- data/spec/app_root/config/database.yml +21 -0
- data/spec/app_root/config/environment.rb +14 -0
- data/spec/app_root/config/environments/in_memory.rb +0 -0
- data/spec/app_root/config/environments/mysql.rb +0 -0
- data/spec/app_root/config/environments/postgresql.rb +0 -0
- data/spec/app_root/config/environments/sqlite.rb +0 -0
- data/spec/app_root/config/environments/sqlite3.rb +0 -0
- data/spec/app_root/config/routes.rb +12 -0
- data/spec/app_root/lib/console_with_fixtures.rb +4 -0
- data/spec/app_root/script/console +7 -0
- data/spec/controllers/api_controller_spec.rb +155 -0
- data/spec/rcov.opts +2 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +31 -0
- metadata +151 -0
data/lib/apify/api.rb
ADDED
@@ -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
|
data/lib/apify/client.rb
ADDED
@@ -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
|
data/lib/apify/errors.rb
ADDED
@@ -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,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
|