lib/jets/router/route.rb in jets-4.0.12 vs lib/jets/router/route.rb in jets-5.0.0.beta1

- old
+ new

@@ -1,189 +1,157 @@ # route = Jets::Router::Route.new( # path: "posts", -# method: :get, +# http_method: :get, # to: "posts#index", # ) -class Jets::Router +module Jets::Router class Route - include Util + extend Memoist + include Compat + include AfterInitialize + include As include Authorizer + include Path + include Util CAPTURE_REGEX = "([^/]*)" # as string - attr_reader :to, :as + attr_reader :options, :scope, :info, :defaults + attr_accessor :original_engine def initialize(options, scope=Scope.new) - @options, @scope = options, scope - @path = compute_path - @to = compute_to - @as = compute_as - # Pretty tricky. The @options[:mount_class] is a class that is mounted. - # For Grape apps, calling ActiveSupport to_json on a Grape class causes an infinite loop. - # Can reproduce with `GrapeApp.to_json` - # There's some type of collision between Grape and ActiveSupport to_json. - # Coerce mount_class option into a string so that when the route is serialized to JSON - # it is a string it won't cause an infinite loop. This allows the apigw routes to be - # saved to s3 and loaded back up at the end of a deploy. - # Related PR: smarter apigw routes paging calculation #635 - # https://github.com/boltops-tools/jets/pull/635 - # Debugging notes: https://gist.github.com/tongueroo/c9baa7e98d5ad68bbdd770fde4651963 - @options[:mount_class] = @options[:mount_class].to_s if @options[:mount_class] + @options = options + @scope = scope + @info = Info.new(@options, @scope) # @info.action and @info.controller + after_initialize + @path_names = {} end - # Constantize back to the original class - def mount_class - @options[:mount_class].constantize + def to + engine || "#{@info.controller}##{@info.action}" # IE: posts#index end - def compute_path - # Note: The @options[:prefix] is missing prefix and is not support via direct create_route. - # This is because it can be added directly to the path. IE: - # - # get "myprefix/posts", to: "posts#index" - # - # Also, this helps to keep the method creator logic more simple. - # - prefix = @scope.full_prefix - prefix = account_scope(prefix) - prefix = account_on(prefix) - - path = [prefix, @options[:path]].compact.join('/') - path = path[1..-1] if path.starts_with?('/') # be more forgiving if / accidentally included - path + def engine + @options[:engine] end + alias rack_app engine - def account_scope(prefix) - return unless prefix - return prefix unless @options[:from_scope] - - if @options[:singular_resource] - prefix.split('/')[0..-2].join('/') - else - prefix.split('/')[0..-3].join('/') - end + def engine? + !!engine end - def account_on(prefix) - # Tricky @scope.from == :resources since the account_scope already has accounted for it - if @options[:on] == :collection && @scope.from == :resources - prefix = prefix.split('/')[0..-2].join('/') - end - prefix == '' ? nil : prefix + def endpoint + engine.to_s if engine end - def compute_to - controller, action = get_controller_action(@options) - mod = @options[:module] || @scope.full_module - controller = [mod, controller].compact.join('/') # add module - "#{controller}##{action}" + def resolved_defaults + defaults = @options[:defaults] || {} + @scope.resolved_defaults.merge(defaults) end - def compute_as - return nil if @options[:as] == :disabled - return unless @options[:method] == :get || @options[:root] - - controller, action = get_controller_action(@options) - klass = if @options[:root] - Jets::Router::MethodCreator::Root - elsif %w[index edit show new].include?(action.to_s) - class_name = "Jets::Router::MethodCreator::#{action.camelize}" - class_name.constantize # Index, Show, Edit, New - else - Jets::Router::MethodCreator::Generic - end - - klass.new(@options, @scope, controller).full_meth_name(nil) + def http_method + @options[:http_method].to_s.upcase end - # IE: standard: posts/:id/edit - # api_gateway: posts/{id}/edit - def path(format=:jets) - case format - when :api_gateway - api_gateway_format(@path) - when :raw - @path - else # jets format - ensure_jets_format(@path) - end + def constraints + @options[:constraints] || @scope.resolved_constraints end - def method - @options[:method].to_s.upcase - end - def internal? !!@options[:internal] end def homepage? - path == '' + path == '/' end # IE: PostsController + # IE: index + delegate :action, :controller, :is_collection?, :is_member?, to: :@info + alias action_name action + + # IE: PostsController + # Different from @info.action def controller_name - to.sub(/#.*/,'').camelize + "Controller" + "#{controller.camelize}Controller" if controller end - # IE: index - def action_name - to.sub(/.*#/,'') - end - # Checks to see if the corresponding controller exists. Useful to validate routes # before deploying to CloudFormation and then rolling back. def valid? controller_class = begin controller_name.constantize - rescue NameError + rescue NameError => e return false end controller_class.lambda_functions.include?(action_name.to_sym) end + # For Jets.config.cfn.build.routes == "one_apigw_method_for_all_routes" + # Need to build the pathParameters for the API Gateway event. + def rebuild_path_parameters(event) + extracted = extract_parameters(event["path"]) + if extracted + params = event["pathParameters"] || {} + params.merge(extracted) + else + event["pathParameters"] # pass through + end + end + # Extracts the path parameters from the actual path # Only supports extracting 1 parameter. So: # - # actual_path: posts/tung/edit + # request_path: posts/tung/edit # route.path: posts/:id/edit # # Returns: # { id: "tung" } - def extract_parameters(actual_path) + def extract_parameters(request_path) + request_path = "/#{request_path}" unless request_path.starts_with?('/') # be more forgiving if / accidentally not included + request_path = remove_engine_mount_at_path(request_path) if path.include?(':') - extract_parameters_capture(actual_path) + extract_parameters_capture(request_path) elsif path.include?('*') - extract_parameters_proxy(actual_path) + extract_parameters_proxy(request_path) else # Lambda AWS_PROXY sets null to the input request when there are no path parmeters nil end end - def extract_parameters_proxy(actual_path) + def remove_engine_mount_at_path(request_path) + return request_path unless original_engine + + mount = Jets::Router::EngineMount.find_by(engine: original_engine) + return request_path unless mount + + request_path.sub(mount.at, '') + end + + def extract_parameters_proxy(request_path) # changes path to a string used for a regexp # others/*proxy => others\/(.*) # nested/others/*proxy => nested/others\/(.*) if path.include?('/') leading_path = path.split('/')[0..-2].join('/') # drop last segment # leading_path: nested/others # capture everything after the leading_path as the value regexp = Regexp.new("#{leading_path}/(.*)") - value = actual_path.match(regexp)[1] + value = request_path.match(regexp)[1] else - value = actual_path + value = request_path end # the last segment without the '*' is the key proxy_segment = path.split('/').last # last segment is the proxy segment # proxy_segment: *proxy key = proxy_segment.sub('*','') { key => value } end - def extract_parameters_capture(actual_path) + def extract_parameters_capture(request_path) # changes path to a string used for a regexp # posts/:id/edit => posts\/(.*)\/edit labels = [] regexp_string = path.split('/').map do |s| if s.start_with?(':') @@ -195,17 +163,52 @@ end.join('\/') # make sure beginning and end of the string matches regexp_string = "^#{regexp_string}$" regexp = Regexp.new(regexp_string) - values = regexp.match(actual_path).captures + values = regexp.match(request_path).captures labels.map do |next_label| [next_label, values.delete_at(0)] end.to_h end - def to_h - JSON.load(to_json) + # Prevents infinite loop when calling route.to_json for state.save("routes", ...) + def as_json(options= nil) + data = { + path: path, + http_method: http_method, + to: to, + } + data[:engine] = engine if engine + data[:internal] = internal if internal + data + end + + # To keep "self #{self}" more concise and helpful + # Use "self #{self.inspect}" more verbose info + def to_s + "#<Jets::Router::Route:#{object_id} @options=#{@options}>" + end + + # Old notes: + # For Grape apps, calling ActiveSupport to_json on a Grape class used to cause an infinite loop. + # Believe Grape fixed this issue. A GrapeApp.to_json now produces a string. + # No longer need to coerce to a string and back to a class. + # + # This is important because Sprocket::Environment.new cannot be coerced into a string or mounting wont work. + # This is used in sprockets-jets/lib/sprockets/jets/engine.rb + # + # Related PR: smarter apigw routes paging calculation #635 + # https://github.com/boltops-tools/jets/pull/635 + # Debugging notes: https://gist.github.com/tongueroo/c9baa7e98d5ad68bbdd770fde4651963 + def mount_class + @options[:mount_class] + end + + # For jets routes help table of routes + def mount_class_name + return unless mount_class + mount_class.class == Class ? mount_class : "#{mount_class.class}.new" end private def ensure_jets_format(path) path.split('/').map do |s|