moonrope 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Rakefile +5 -0
- data/lib/moonrope.rb +37 -0
- data/lib/moonrope/action.rb +163 -0
- data/lib/moonrope/action_result.rb +63 -0
- data/lib/moonrope/base.rb +130 -0
- data/lib/moonrope/before_action.rb +24 -0
- data/lib/moonrope/controller.rb +49 -0
- data/lib/moonrope/dsl/action_dsl.rb +70 -0
- data/lib/moonrope/dsl/base_dsl.rb +74 -0
- data/lib/moonrope/dsl/controller_dsl.rb +57 -0
- data/lib/moonrope/dsl/structure_dsl.rb +63 -0
- data/lib/moonrope/dsl/structure_restriction_dsl.rb +27 -0
- data/lib/moonrope/errors.rb +48 -0
- data/lib/moonrope/eval_environment.rb +141 -0
- data/lib/moonrope/eval_helpers.rb +35 -0
- data/lib/moonrope/helper.rb +28 -0
- data/lib/moonrope/param_set.rb +44 -0
- data/lib/moonrope/rack_middleware.rb +73 -0
- data/lib/moonrope/railtie.rb +30 -0
- data/lib/moonrope/request.rb +165 -0
- data/lib/moonrope/structure.rb +83 -0
- data/lib/moonrope/version.rb +3 -0
- metadata +152 -0
@@ -0,0 +1,35 @@
|
|
1
|
+
module Moonrope
|
2
|
+
module EvalHelpers
|
3
|
+
|
4
|
+
#
|
5
|
+
# Raise an error.
|
6
|
+
#
|
7
|
+
# @param type [Symbol] the type of error to raise
|
8
|
+
# @param message [String, Hash or Array] options to pass with the error (usually a message)
|
9
|
+
#
|
10
|
+
def error(type, message)
|
11
|
+
case type
|
12
|
+
when :not_found then raise(Moonrope::Errors::NotFound, message)
|
13
|
+
when :access_denied then raise(Moonrope::Errors::AccessDenied, message)
|
14
|
+
when :validation_error then raise(Moonrope::Errors::ValidationError, message)
|
15
|
+
when :parameter_error then raise(Moonrope::Errors::ParameterError, message)
|
16
|
+
else
|
17
|
+
raise Moonrope::Errors::RequestError, message
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# Return paginated information
|
23
|
+
#
|
24
|
+
def paginate(collection, max_per_page = 60, &block)
|
25
|
+
per_page = params.per_page || 30
|
26
|
+
per_page = max_per_page if per_page < 1 || per_page > max_per_page
|
27
|
+
paginated_results = collection.page(params.page).per(per_page)
|
28
|
+
set_flag :paginated, {:page => params.page, :per_page => per_page, :total_pages => paginated_results.total_pages, :total_records => paginated_results.total_count}
|
29
|
+
paginated_results.to_a.map do |result|
|
30
|
+
block.call(result)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Moonrope
|
2
|
+
class Helper
|
3
|
+
|
4
|
+
# @return [Symbol] the name of the helper
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
# @return [Moonrope::Controller] the controller this helper belongs to
|
8
|
+
attr_reader :controller
|
9
|
+
|
10
|
+
# @return [Proc] the proc to execute
|
11
|
+
attr_reader :block
|
12
|
+
|
13
|
+
|
14
|
+
#
|
15
|
+
# Initialize a new helper
|
16
|
+
#
|
17
|
+
# @param name [Symbol] the name of the helper
|
18
|
+
# @param controller [Moonrope::Controller] the controller the helper belongs to
|
19
|
+
# @yield stores the block for use later
|
20
|
+
#
|
21
|
+
def initialize(name, controller, &block)
|
22
|
+
@name = name
|
23
|
+
@controller = controller
|
24
|
+
@block = block
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Moonrope
|
2
|
+
class ParamSet
|
3
|
+
|
4
|
+
#
|
5
|
+
# Initialize a new ParamSet
|
6
|
+
#
|
7
|
+
# @param params [Hash or String] the initial params. If string, will be parsed through JSON.
|
8
|
+
#
|
9
|
+
def initialize(params = {})
|
10
|
+
@params = (params.is_a?(String) ? JSON.parse(params) : params) || {}
|
11
|
+
@defaults = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
#
|
15
|
+
# Return the value for the given key
|
16
|
+
#
|
17
|
+
# @param key [String] the key to lookup
|
18
|
+
# @return [Object] the value
|
19
|
+
#
|
20
|
+
def _value_for(key)
|
21
|
+
# Get the value from the params and defaults
|
22
|
+
value = (@params[key.to_s] || @defaults[key.to_s])
|
23
|
+
# Ensure that empty strings are actually nil.
|
24
|
+
value = nil if value.is_a?(String) && value.length == 0
|
25
|
+
# Return the value
|
26
|
+
value
|
27
|
+
end
|
28
|
+
|
29
|
+
alias_method :[], :_value_for
|
30
|
+
alias_method :method_missing, :_value_for
|
31
|
+
|
32
|
+
#
|
33
|
+
# Set the defaults for the param set
|
34
|
+
#
|
35
|
+
# @param defaults [Hash]
|
36
|
+
# @return [void]
|
37
|
+
def _defaults=(defaults)
|
38
|
+
if defaults.is_a?(Hash)
|
39
|
+
@defaults = defaults
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Moonrope
|
2
|
+
class RackMiddleware
|
3
|
+
|
4
|
+
#
|
5
|
+
# Initialize a new Moonrope::Rack server
|
6
|
+
#
|
7
|
+
# @param app [Object] the next Rack application in the stack
|
8
|
+
# @param base [Moonrope::Base] the base API to serve
|
9
|
+
# @param options [Hash] a hash of options
|
10
|
+
#
|
11
|
+
#
|
12
|
+
def initialize(app, base, options = {})
|
13
|
+
@app = app
|
14
|
+
@base = base
|
15
|
+
@options = options
|
16
|
+
end
|
17
|
+
|
18
|
+
#
|
19
|
+
# Make a new request
|
20
|
+
#
|
21
|
+
# @param env [Hash] a rack environment hash
|
22
|
+
# @return [Array] a rack triplet
|
23
|
+
#
|
24
|
+
def call(env)
|
25
|
+
if env['PATH_INFO'] =~ Moonrope::Request::PATH_REGEX
|
26
|
+
|
27
|
+
if @options[:reload_on_each_request]
|
28
|
+
@base.load
|
29
|
+
end
|
30
|
+
|
31
|
+
#
|
32
|
+
# Create a new request object
|
33
|
+
#
|
34
|
+
request = @base.request(env, $1)
|
35
|
+
|
36
|
+
#
|
37
|
+
# Check the request is valid
|
38
|
+
#
|
39
|
+
unless request.valid?
|
40
|
+
return [400, {}, ["Invalid API Request. Must provide a version, controller & action as /api/v1/controller/action."]]
|
41
|
+
end
|
42
|
+
|
43
|
+
global_headers = {}
|
44
|
+
global_headers['Content-Type'] = 'application/json'
|
45
|
+
|
46
|
+
#
|
47
|
+
# Execute the request
|
48
|
+
#
|
49
|
+
begin
|
50
|
+
result = request.execute
|
51
|
+
json = result.to_json
|
52
|
+
global_headers['Content-Length'] = json.bytesize.to_s
|
53
|
+
[200, global_headers.merge(result.headers), [result.to_json]]
|
54
|
+
rescue JSON::ParserError => e
|
55
|
+
[400, global_headers, [{:status => 'invalid-json', :details => e.message}.to_json]]
|
56
|
+
rescue => e
|
57
|
+
Moonrope.logger.info e.class
|
58
|
+
Moonrope.logger.info e.message
|
59
|
+
Moonrope.logger.info e.backtrace.join("\n")
|
60
|
+
[500, global_headers, [{:status => 'internal-server-error'}.to_json]]
|
61
|
+
end
|
62
|
+
|
63
|
+
else
|
64
|
+
if @app && @app.respond_to?(:call)
|
65
|
+
@app.call(env)
|
66
|
+
else
|
67
|
+
[404, {}, ["Non-API request"]]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Moonrope
|
2
|
+
class Railtie < Rails::Railtie
|
3
|
+
|
4
|
+
initializer 'moonrope.initialize' do |app|
|
5
|
+
|
6
|
+
# Initialize a new moonrope base from the API defined in
|
7
|
+
# $RAILS_ROOT/app/api directory.
|
8
|
+
moonrope_directory = Rails.root.join('api')
|
9
|
+
if File.directory?(moonrope_directory)
|
10
|
+
app.config.moonrope = Moonrope::Base.load(moonrope_directory)
|
11
|
+
else
|
12
|
+
$stderr.puts "Moonrope is installed but there is no API directory at ROOT/app/api."
|
13
|
+
next
|
14
|
+
end
|
15
|
+
|
16
|
+
# Set the logger
|
17
|
+
Moonrope.logger = Rails.logger
|
18
|
+
|
19
|
+
# Insert the Moonrope middleware into the application's middleware
|
20
|
+
# stack (at the bottom).
|
21
|
+
app.middleware.use(
|
22
|
+
Moonrope::RackMiddleware,
|
23
|
+
app.config.moonrope,
|
24
|
+
:reload_on_each_request => !app.config.cache_classes
|
25
|
+
)
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
module Moonrope
|
2
|
+
class Request
|
3
|
+
|
4
|
+
# The path which will be intercepted by the Rack middleware
|
5
|
+
# and that all reuqest will arrive on.
|
6
|
+
PATH_REGEX = /\A\/api\/([\w\/\-\.]+)?/
|
7
|
+
|
8
|
+
# @return [Hash] the rack environment
|
9
|
+
attr_reader :env
|
10
|
+
# @return [String] the name of the controller which was requested
|
11
|
+
attr_reader :controller_name
|
12
|
+
# @return [String] the name of the action which was requested
|
13
|
+
attr_reader :action_name
|
14
|
+
# @return [Object] the authenticated user
|
15
|
+
attr_reader :authenticated_user
|
16
|
+
|
17
|
+
#
|
18
|
+
# Initialize a new Moonrope::Request
|
19
|
+
#
|
20
|
+
# @param base [Moonrope::Base]
|
21
|
+
# @param env [Hash] a rack environment has
|
22
|
+
# @param path [String] the reqested path (after the /api/ prefix)
|
23
|
+
#
|
24
|
+
def initialize(base, env, path = nil)
|
25
|
+
@base = base
|
26
|
+
@env = env
|
27
|
+
if path.nil? && env['PATH_INFO'] =~ PATH_REGEX
|
28
|
+
path = $1
|
29
|
+
end
|
30
|
+
@version, @controller_name, @action_name = path ? path.split("/") : [nil, nil, nil]
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# Return the requested API version from the request
|
35
|
+
#
|
36
|
+
# @return [Integer]
|
37
|
+
#
|
38
|
+
def version
|
39
|
+
version = @version.to_s.gsub(/[^0-9]/, '').to_i
|
40
|
+
version = 1 if version == 0
|
41
|
+
version
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# Return whether or not this request is valid and can continue?
|
46
|
+
#
|
47
|
+
# @return [Boolean]
|
48
|
+
#
|
49
|
+
def valid?
|
50
|
+
!!(version > 0 && [controller_name, action_name].all? { |c| c =~ /\A[\w\-\.]+\z/} && controller && action)
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# Return the controller object for the request
|
55
|
+
#
|
56
|
+
# @return [Moonrope::Controller]
|
57
|
+
#
|
58
|
+
def controller
|
59
|
+
@controller ||= @base.controller(controller_name.to_sym)
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
# Return the action object for the request
|
64
|
+
#
|
65
|
+
# return [Moonrope::Action]
|
66
|
+
#
|
67
|
+
def action
|
68
|
+
@action ||= controller.actions[action_name.to_sym]
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
# Execute the appropriate action for the request after running
|
73
|
+
# the various authentication checks.
|
74
|
+
#
|
75
|
+
# @return [Moonrope::ActionResult]
|
76
|
+
#
|
77
|
+
def execute
|
78
|
+
eval_env = EvalEnvironment.new(@base, self)
|
79
|
+
if @base.authenticator
|
80
|
+
begin
|
81
|
+
@authenticated_user = eval_env.instance_eval(&@base.authenticator)
|
82
|
+
# If we are authenticated, check whether the action permits access to
|
83
|
+
# this user, if not raise an error.
|
84
|
+
if authenticated?
|
85
|
+
unless action.check_access(eval_env) == true
|
86
|
+
raise Moonrope::Errors::AccessDenied, "Access to #{controller.name}/#{action.name} is not permitted."
|
87
|
+
end
|
88
|
+
end
|
89
|
+
rescue Moonrope::Errors::RequestError => e
|
90
|
+
@authenticated_user ||= false # set authenticated user to false if they don't exist
|
91
|
+
result = Moonrope::ActionResult.new(self)
|
92
|
+
result.status = e.status
|
93
|
+
result.data = e.data
|
94
|
+
return result
|
95
|
+
end
|
96
|
+
end
|
97
|
+
action.execute(eval_env)
|
98
|
+
end
|
99
|
+
|
100
|
+
#
|
101
|
+
# Return all user supplier parameters
|
102
|
+
#
|
103
|
+
# @return [Moonrope::ParamSet]
|
104
|
+
#
|
105
|
+
def params
|
106
|
+
@params ||= Moonrope::ParamSet.new(rack_request.params['params'])
|
107
|
+
end
|
108
|
+
|
109
|
+
#
|
110
|
+
# Return all HTTP headers from the request
|
111
|
+
#
|
112
|
+
# @return [Rack::Utils::HeaderHash]
|
113
|
+
#
|
114
|
+
def headers
|
115
|
+
@headers ||= self.class.extract_http_request_headers(@env)
|
116
|
+
end
|
117
|
+
|
118
|
+
#
|
119
|
+
# Is this request to the API anonymous?
|
120
|
+
#
|
121
|
+
# @return [Boolean]
|
122
|
+
#
|
123
|
+
def anonymous?
|
124
|
+
authenticated_user.nil?
|
125
|
+
end
|
126
|
+
|
127
|
+
#
|
128
|
+
# Is this request to the API authenticated?
|
129
|
+
#
|
130
|
+
# @return [Boolean]
|
131
|
+
#
|
132
|
+
def authenticated?
|
133
|
+
!(authenticated_user.nil? || authenticated_user == false)
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
#
|
139
|
+
# Return/create a rack request object for use internally
|
140
|
+
#
|
141
|
+
# @return [Rack::Request]
|
142
|
+
#
|
143
|
+
def rack_request
|
144
|
+
@rack_request ||= ::Rack::Request.new(@env)
|
145
|
+
end
|
146
|
+
|
147
|
+
#
|
148
|
+
# Extract headers from the rack env
|
149
|
+
#
|
150
|
+
# @return [Rack::Utils::HeaderHash]
|
151
|
+
#
|
152
|
+
def self.extract_http_request_headers(env)
|
153
|
+
env.reject do |k, v|
|
154
|
+
!(/^HTTP_[A-Z_]+$/ === k) || v.nil?
|
155
|
+
end.map do |k, v|
|
156
|
+
[k.sub(/^HTTP_/, "").gsub("_", "-"), v]
|
157
|
+
end.inject(::Rack::Utils::HeaderHash.new) do |hash, k_v|
|
158
|
+
k, v = k_v
|
159
|
+
hash[k] = v
|
160
|
+
hash
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Moonrope
|
2
|
+
class Structure
|
3
|
+
|
4
|
+
# @return [Symbol] the name of the structure
|
5
|
+
attr_accessor :name
|
6
|
+
|
7
|
+
# @return [Proc] the basic data block
|
8
|
+
attr_accessor :basic
|
9
|
+
|
10
|
+
# @return [Proc] the full data block
|
11
|
+
attr_accessor :full
|
12
|
+
|
13
|
+
# @return [Moonrope::DSL::StructureDSL] the DSL
|
14
|
+
attr_reader :dsl
|
15
|
+
|
16
|
+
# @return [Hash] all expansions for the structure
|
17
|
+
attr_reader :expansions
|
18
|
+
|
19
|
+
# @return [Array] all restrictions for the structure
|
20
|
+
attr_reader :restrictions
|
21
|
+
|
22
|
+
# @return [Moonrope::Base] the base API
|
23
|
+
attr_reader :base
|
24
|
+
|
25
|
+
#
|
26
|
+
# Initialize a new structure
|
27
|
+
#
|
28
|
+
# @param base [Moonrope::Base]
|
29
|
+
# @param name [Symbol]
|
30
|
+
# @yield instance evals the contents within the structure DSL
|
31
|
+
def initialize(base, name, &block)
|
32
|
+
@base = base
|
33
|
+
@name = name
|
34
|
+
@expansions = {}
|
35
|
+
@restrictions = []
|
36
|
+
@dsl = Moonrope::DSL::StructureDSL.new(self)
|
37
|
+
@dsl.instance_eval(&block) if block_given?
|
38
|
+
end
|
39
|
+
|
40
|
+
#
|
41
|
+
# Return a hash for this struture
|
42
|
+
#
|
43
|
+
# @param object [Object] the object
|
44
|
+
# @param options [Hash] additional options
|
45
|
+
# @return [Hash]
|
46
|
+
#
|
47
|
+
def hash(object, options = {})
|
48
|
+
# Set up an environment for
|
49
|
+
environment = EvalEnvironment.new(base, options[:request], :o => object)
|
50
|
+
|
51
|
+
# Always get a basic hash to work from
|
52
|
+
hash = environment.instance_eval(&self.basic)
|
53
|
+
|
54
|
+
# Enhance with the full hash if requested
|
55
|
+
if options[:full]
|
56
|
+
if self.full.is_a?(Proc)
|
57
|
+
full_hash = environment.instance_eval(&self.full)
|
58
|
+
hash.merge!(full_hash)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Add restrictions
|
62
|
+
if environment.auth
|
63
|
+
@restrictions.each do |restriction|
|
64
|
+
next unless environment.instance_eval(&restriction.condition) == true
|
65
|
+
hash.merge!(environment.instance_eval(&restriction.data))
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Add expansions
|
71
|
+
if options[:expansions]
|
72
|
+
expansions.each do |name, expansion|
|
73
|
+
next if options[:expansions].is_a?(Array) && !options[:expansions].include?(name.to_sym)
|
74
|
+
hash.merge!(name => environment.instance_eval(&expansion))
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Return the hash
|
79
|
+
hash
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|