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