apify 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 (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