# frozen_string_literal: true require 'sinatra/base' require 'mustermann' module Sinatra # = Sinatra::Namespace # # Sinatra::Namespace is an extension that adds namespaces to an # application. This namespaces will allow you to share a path prefix for the # routes within the namespace, and define filters, conditions and error # handlers exclusively for them. Besides that, you can also register helpers # and extensions that will be used only within the namespace. # # == Usage # # Once you have loaded the extension (see below), you can use the +namespace+ # method to define namespaces in your application. # # You can define a namespace by a path prefix: # # namespace '/blog' do # get { haml :blog } # get '/:entry_permalink' do # @entry = Entry.find_by_permalink!(params[:entry_permalink]) # haml :entry # end # # # More blog routes... # end # # by a condition: # # namespace :host_name => 'localhost' do # get('/admin/dashboard') { haml :dashboard } # get('/admin/login') { haml :login } # # # More admin routes... # end # # or both: # # namespace '/admin', :host_name => 'localhost' do # get('/dashboard') { haml :dashboard } # get('/login') { haml :login } # post('/login') { login_user } # # # More admin routes... # end # # Regex is also accepted: # # namespace /\/posts\/([^\/&?]+)\// do # get { haml :blog } # # # More blog routes... # end # # When you define a filter or an error handler, or register an extension or a # set of helpers within a namespace, they only affect the routes defined in # it. For instance, lets define a before filter to prevent the access of # unauthorized users to the admin section of the application: # # namespace '/admin' do # helpers AdminHelpers # before { authenticate unless request.path_info == '/admin/login' } # # get '/dashboard' do # # Only authenticated users can access here... # haml :dashboard # end # # # More admin routes... # end # # get '/' do # # Any user can access here... # haml :index # end # # Well, they actually also affect the nested namespaces: # # namespace '/admin' do # helpers AdminHelpers # before { authenticate unless request.path_info == '/admin/login' } # # namespace '/users' do # get do # # Only authenticated users can access here... # @users = User.all # haml :users # end # # # More user admin routes... # end # # # More admin routes... # end # # Redirecting within the namespace can be done using redirect_to: # # namespace '/admin' do # get '/foo' do # redirect_to '/bar' # Redirects to /admin/bar # end # # get '/foo' do # redirect '/bar' # Redirects to /bar # end # end # # === Classic Application Setup # # To be able to use namespaces in a classic application all you need to do is # require the extension: # # require "sinatra" # require "sinatra/namespace" # # namespace '/users' do # end # # === Modular Application Setup # # To be able to use namespaces in a modular application all you need to do is # require the extension, and then, register it: # # require "sinatra/base" # require "sinatra/namespace" # # class MyApp < Sinatra::Base # register Sinatra::Namespace # # namespace '/users' do # end # end # # === Within an extension # # To be able to use namespaces within an extension, you need to first create # an extension. This includes defining the `registered(app)` method in the # module. # # require 'sinatra/base' # For creating Sinatra extensions # require 'sinatra/namespace' # To create namespaces # # module Zomg # Keep everything under "Zomg" namespace for sanity # module Routes # Define a new "Routes" module # # def self.registered(app) # # First, register the Namespace extension # app.register Sinatra::Namespace # # # This defines an `/api` namespace on the application # app.namespace '/api' do # get '/users' do # # do something with `GET "/api/users"` # end # end # # end # end # # # Lastly, register the extension to use in our app # Sinatra.register Routes # end # # In order to use this extension, is the same as any other Sinatra extension: # # module Zomg # # Define our app class, we use modular app for this example # class App < Sinatra::Base # # this gives us all the namespaces we defined earlier # register Routes # # get '/' do # "Welcome to my application!" # end # end # end # # Zomg::App.run! # Don't forget to start your app ;) # # Phew! That was a mouthful. # # I hope that helps you use `Sinatra::Namespace` in every way imaginable! # module Namespace def self.new(base, pattern, conditions = {}, &block) Module.new do # quelch uninitialized variable warnings, since these get used by compile method. @pattern = nil @conditions = nil extend NamespacedMethods include InstanceMethods @base = base @extensions = [] @errors = {} @pattern, @conditions = compile(pattern, conditions) @templates = Hash.new { |_h, k| @base.templates[k] } namespace = self before { extend(@namespace = namespace) } class_eval(&block) end end module InstanceMethods def settings @namespace end def template_cache super.fetch(:nested, @namespace) { TemplateCache.new } end def redirect_to(uri, *args) redirect("#{@namespace.pattern}#{uri}", *args) end end module SharedMethods def namespace(pattern, conditions = {}, &block) Sinatra::Namespace.new(self, pattern, conditions, &block) end end module NamespacedMethods include SharedMethods attr_reader :base, :templates ALLOWED_ENGINES = %i[ erb erubi haml hamlit builder nokogiri sass scss liquid markdown rdoc asciidoc markaby rabl slim yajl ] def self.prefixed(*names) names.each { |n| define_method(n) { |*a, &b| prefixed(n, *a, &b) } } end prefixed :before, :after, :delete, :get, :head, :options, :patch, :post, :put def helpers(*extensions, &block) class_eval(&block) if block_given? include(*extensions) if extensions.any? end def register(*extensions, &block) extensions << Module.new(&block) if block_given? @extensions += extensions extensions.each do |extension| extend extension extension.registered(self) if extension.respond_to?(:registered) end end def invoke_hook(name, *args) @extensions.each { |e| e.send(name, *args) if e.respond_to?(name) } end def not_found(&block) error(Sinatra::NotFound, &block) end def errors base.errors.merge(namespace_errors) end def namespace_errors @errors end def error(*codes, &block) args = Sinatra::Base.send(:compile!, 'ERROR', /.*/, block) codes = codes.map { |c| Array(c) }.flatten codes << Exception if codes.empty? codes << Sinatra::NotFound if codes.include?(404) codes.each do |c| errors = @errors[c] ||= [] errors << args end end def respond_to(*args) return @conditions[:provides] || base.respond_to if args.empty? @conditions[:provides] = args end def set(key, value = self, &block) return key.each { |k, v| set(k, v) } if key.respond_to?(:each) && block.nil? && (value == self) raise ArgumentError, "may not set #{key}" unless ([:views] + ALLOWED_ENGINES).include?(key) block ||= proc { value } singleton_class.send(:define_method, key, &block) end def enable(*opts) opts.each { |key| set(key, true) } end def disable(*opts) opts.each { |key| set(key, false) } end def template(name, &block) first_location = caller_locations.first filename = first_location.path line = first_location.lineno templates[name] = [block, filename, line] end def layout(name = :layout, &block) template name, &block end def pattern @pattern end private def app base.respond_to?(:base) ? base.base : base end def compile(pattern, conditions, default_pattern = nil) if pattern.respond_to? :to_hash conditions = conditions.merge pattern.to_hash pattern = nil end base_pattern = @pattern base_conditions = @conditions pattern ||= default_pattern [prefixed_path(base_pattern, pattern), (base_conditions || {}).merge(conditions)] end def prefixed_path(a, b) return a || b || /.*/ unless a && b return Mustermann.new(b) if a == /.*/ Mustermann.new(a) + Mustermann.new(b) end def prefixed(method, pattern = nil, conditions = {}, &block) default = %r{(?:/.*)?} if (method == :before) || (method == :after) pattern, conditions = compile pattern, conditions, default result = base.send(method, pattern, **conditions, &block) invoke_hook :route_added, method.to_s.upcase, pattern, block result end def method_missing(method, *args, &block) base.send(method, *args, &block) end def respond_to?(method, include_private = false) super || base.respond_to?(method, include_private) end end module BaseMethods include SharedMethods end def self.extended(base) base.extend BaseMethods end end register Sinatra::Namespace Delegator.delegate :namespace end