# frozen_string_literal: true require 'roda' require 'roda/endpoints/endpoint/collection' require 'roda/endpoints/endpoint/item' require 'rom/struct/to_json' class Roda # Module containing {Roda} plugins. module RodaPlugins # Endpoints plugin for {Roda} module Endpoints # @param [Class(Roda)] app def self.load_dependencies(app, **_opts) app.plugin :all_verbs app.plugin :head app.plugin :caching app.plugin :monads app.plugin :symbol_status app.plugin :symbol_matchers app.plugin :slash_path_empty app.plugin :json_parser app.plugin :indifferent_params app.plugin :json, classes: [Array, Hash, ROM::Struct] app.plugin :flow end # @param [Class(Roda)] app # @param [Hash] opts def self.configure(app, container: app, **opts) opts = (app.opts[:endpoints] || {}).merge(opts) unless container.respond_to? :resolve require 'dry-container' container.extend Dry::Container::Mixin end app.opts[:endpoints] = opts.merge(container: container) Roda::Endpoints.roda_class ||= app end # {ClassMethods#register `Roda.register`} && # {ClassMethods#merge `Roda.merge`} module ClassMethods # @param [String, Symbol] name # @param [Array] args # @param [Proc] block def register(name, *args, &block) opts[:container].register(name, *args, &block) end # @param [String, Symbol] name # @param [Array] args # @param [Proc] block def merge(name, *args, &block) opts[:container].merge(name, *args, &block) end end # `Roda::RodaRequest` instant extensions. module RequestMethods # Implements collection endpoint using given args # # @param name [Symbol] # @param item [{Symbol=>Object}] # @param kwargs [{Symbol=>Object}] # @param type [Class(Roda::Endpoints::Endpoint::Collection)] # @param on [String, Symbol] # @param (see Endpoint::Collection#initialize) # @see Endpoint::Collection.defaults # @yieldparam endpoint [Collection] # @yieldreturn [#to_json] # @return [Endpoint] # # @example # r.collection :articles, item: { only: %i(get delete) } # # # is equivalent to # # r.on 'articles', item: { only: %i(get delete) } do # articles = Endpoint.new( # name: :articles, # container: container, # ) # # r.last_modified articles.last_modified # # r.get do # articles.call(:get, r.params) # end # # r.post do # articles.call(:post, r.params) # end # # r.child :id, only: %i(get delete) # end def collection(name, item: { on: :id }, type: Roda::Endpoints::Endpoint::Collection, on: name.to_s, parent: root_endpoint, **kwargs) endpoint name: name, item: item, type: type, on: on, parent: parent, **kwargs do |endpoint| yield endpoint if block_given? end end # rubocop:enable Metrics/MethodLength, Metrics/AbcSize # @param on [Symbol] # @param kwargs [Hash] # # @example # r.collection :articles do |articles| # r.item :id # end # # # is equivalent to # # r.collection :articles do |articles| # r.on :id do |id| # # def article # @article ||= articles.repository.fetch(id) # end # # r.get do # article # end # end # end def item(on: :id, type: Roda::Endpoints::Endpoint::Item, **kwargs) unless current_endpoint.is_a?(Roda::Endpoints::Endpoint::Collection) raise ArgumentError, "#{self.class}#item called not within a collection endpoint" end # @route /{collection.name}/{id} endpoint( on: on, name: current_endpoint.item_name, type: type, **kwargs ) do |endpoint| yield endpoint if block_given? end end # @overload singleton(name, entity, type:, **kwargs) # @overload singleton(name:, entity:, type:, **kwargs) def singleton(name, *args, entity: args.first, type: Roda::Endpoints::Endpoint::Singleton, **kwargs) endpoint( name: name, entity: entity, type: type, on: name.to_s, **kwargs ) do |endpoint| yield endpoint if block_given? end end private # @param [Class(Roda::Endpoints::Endpoint::Singleton)] type # @param [Dry::Container::Mixin, #register, #resolve, #merge] container # @param [Roda::Endpoints::Endpoint] parent # @param [Hash] kwargs def endpoint(name:, type:, container: roda_class.opts[:endpoints][:container], parent: current_endpoint, on: name, **kwargs) parent ||= root_endpoint on on do |*captures| with_current_endpoint parent.child( name: name, type: type, container: container, parent: parent, on: on, captures: captures, **kwargs ) do |endpoint| yield endpoint if block_given? instance_exec(self, endpoint, &endpoint.route) end end end # @param [Symbol] verb def match_transaction(verb) resolve current_endpoint.transactions.key_for(verb) do |transaction| transaction.call(params) do |m| statuses = current_endpoint.class.statuses[verb] m.success do |result| response.status = statuses[:success] result end m.failure do |result| if result.is_a?(Array) && result.size == 2 response.status, value = result value else response.status = statuses[:failure] result end end end end end # @return [Endpoint] def current_endpoint endpoints.last end # @return [] def endpoints @endpoints ||= [root_endpoint] end def root_endpoint @root_endpoint ||= Roda::Endpoints::Endpoint.new( name: :root, ns: nil, container: roda_class.opts[:endpoints][:container] ) end def with_current_endpoint(endpoint) endpoints.push endpoint yield endpoint endpoints.pop end end # rubocop:enable Metrics/ModuleLength end register_plugin :endpoints, Endpoints end end