lib/restspec/endpoints/dsl.rb in restspec-0.0.4 vs lib/restspec/endpoints/dsl.rb in restspec-0.1
- old
+ new
@@ -2,140 +2,373 @@
module Restspec
module Endpoints
HTTP_METHODS = [:get, :post, :put, :patch, :delete, :head]
+ # The Endpoints DSL is what should be used inside the `endpoints.rb` file.
+ # This class is related to the top-level namespace of the DSL.
class DSL
+ # Generates a new namespace that is just an entity that
+ # groups a couple of endpoints for whatever reason.
+ #
+ # @example with only a name
+ # namespace :books do
+ # end
+ #
+ # @example with a `base_path`
+ # namespace :books, base_path: '/publications' do
+ # end
+ #
+ # @param name [String] the name of the namespace.
+ # @param base_path [String, nil] the base_path property of the namespace.
+ # @param block [:call] a block to yield to a newly created {NamespaceDSL}.
def namespace(name, base_path: nil, &block)
namespace = Namespace.create(name.to_s)
namespace.base_path = base_path
namespace_dsl = NamespaceDSL.new(namespace)
namespace_dsl.instance_eval(&block)
end
- def resource(name, options = {}, &block)
- namespace name, base_path: (options[:base_path] || "/#{name}") do
- if self.namespace.schema_name.blank?
- schema_name = name.to_s.singularize
- schema(schema_name.to_sym)
- end
+ # This is actually a kind of namespace factory, that creates
+ # a namespace that have the next conventions:
+ # - The name is a pluralization of another name. (products, books, etc)
+ # - The base_path is, when not defined, the name of the namespace prepended with a "/"
+ # - Attaches a schema with the name of the namespace singularized to the namespace.
+ # (product, book, etc)
+ #
+ # In this way, it can express REST resources that groups a couple of endpoints related
+ # to them.
+ #
+ # @example
+ # resource :books do
+ # namespace.schema_for(:response).name # book
+ # namespace.base_path # /books
+ # end
+ #
+ # @param (see #namespace)
+ #
+ def resource(name, base_path: nil, &block)
+ resource_name = name.to_s.singularize.to_sym
+ namespace name, base_path: (base_path || "/#{name}") do
+ schema resource_name
instance_eval(&block)
end
end
end
+ # The Namespace DSL is what should be used inside a namespace or resource block.
+ # Its major responsability is to add endpoints to the dsl.
class NamespaceDSL
- attr_accessor :namespace, :endpoint_base_path, :resource_endpoint_base_path, :common_endpoints_config_block
+ attr_accessor :namespace, :endpoint_config_blocks
def initialize(namespace)
self.namespace = namespace
- self.resource_endpoint_base_path = ''
+ self.endpoint_config_blocks = []
end
+ # Defines a new endpoint with a name and a block
+ # that has a context equals to a {EndpointDSL} instance
+ # and saves the endpoint into the namespace.
+ #
+ # @example
+ # namespace :books do
+ # endpoint :get do
+ # end
+ # end
+ #
+ # @param name [Symbol] the endpoint name.
+ # @param block [block] the block to call within an {EndpointDSL} instance.
def endpoint(name, &block)
endpoint = Endpoint.new(name)
endpoint_dsl = EndpointDSL.new(endpoint)
namespace.add_endpoint(endpoint)
endpoint_dsl.instance_eval(&block)
- endpoint_dsl.instance_eval(&common_endpoints_config_block)
+ endpoint_config_blocks.each do |config_block|
+ endpoint_dsl.instance_eval(&config_block)
+ end
+
Restspec::EndpointStore.store(endpoint)
end
- HTTP_METHODS.each do |http_method|
- define_method(http_method) do |name, path = '', &block|
- endpoint(name) do
- public_send(http_method, path)
- instance_eval(&block) if block.present?
- end
- end
+ # @!macro http_method_endpoint
+ # Defines an endpoint with a **$0** http method.
+ #
+ # @example
+ # namespace :books do
+ # $0 :endpoint_name, '/path' # this endpoint will have the $0 http method attached
+ # end
+ #
+ # @param name [String] the name of the endpoint.
+ # @param path [String] the optional path of the endpoint.
+ # @param block [block] the block to call with the {EndpointDSL} instance.
+
+ # @macro http_method_endpoint
+ def get(name, path = '', &block)
+ setup_endpoint_from_http_method(:get, name, path, &block)
end
+ # @macro http_method_endpoint
+ def post(name, path = '', &block)
+ setup_endpoint_from_http_method(:post, name, path, &block)
+ end
+
+ # @macro http_method_endpoint
+ def put(name, path = '', &block)
+ setup_endpoint_from_http_method(:put, name, path, &block)
+ end
+
+ # @macro http_method_endpoint
+ def patch(name, path = '', &block)
+ setup_endpoint_from_http_method(:patch, name, path, &block)
+ end
+
+ # @macro http_method_endpoint
+ def delete(name, path = '', &block)
+ setup_endpoint_from_http_method(:delete, name, path, &block)
+ end
+
+ # @macro http_method_endpoint
+ def head(name, path = '', &block)
+ setup_endpoint_from_http_method(:head, name, path, &block)
+ end
+
+ # This defines an anonymous namespace with a base_path equals
+ # to '/:id' that will affect all his internals endpoints
+ # with the base_path of the parent namespace. Esentially:
+ #
+ # @example
+ # resource :books do
+ # member do
+ # get :show # /books/:id
+ # patch :update # /books/:id
+ # get :chapters, '/chapters' # /books/:id/chapters
+ # end
+ # end
+ #
+ # @param base_path [String, nil] override of the base_pat
+ # @param identifier_name [String, :id] override of the key :id
+ # @param block [block] block that will be called with a {NamespaceDSL instance},
+ # typically for create endpoints inside.
def member(base_path: nil, identifier_name: 'id', &block)
member_namespace = namespace.add_anonymous_children_namespace
member_namespace.base_path = base_path || "/:#{identifier_name}"
- NamespaceDSL.new(member_namespace).instance_eval(&block)
+ member_dsl = NamespaceDSL.new(member_namespace)
+ member_dsl.endpoint_config_blocks += endpoint_config_blocks
+ member_dsl.instance_eval(&block)
end
+ # This defines an anonymous namespace with a base_path equals
+ # to the empty string that will affect all his internals endpoints
+ # with the base_path of the parent namespace. This should be used
+ # to group collection related endpoints.
+ #
+ # @example
+ # resource :books do
+ # collection do
+ # get :index # /books
+ # post :create # /books
+ # end
+ # end
+ #
+ # @param base_path [String, nil] override of the base_pat
+ # @param block [block] block that will be called with a {NamespaceDSL instance},
+ # typically for create endpoints inside.
def collection(base_path: nil, &block)
collection_namespace = namespace.add_anonymous_children_namespace
collection_namespace.base_path = base_path
- NamespaceDSL.new(collection_namespace).instance_eval(&block)
+ collection_dsl = NamespaceDSL.new(collection_namespace)
+ collection_dsl.endpoint_config_blocks += endpoint_config_blocks
+ collection_dsl.instance_eval(&block)
end
- def schema(name, schema_extensions = {})
- namespace.schema_name = name
- namespace.schema_extensions = schema_extensions
+ # It attaches a schema to the namespace. This schema name will be
+ # used by the endpoints inside the namespace too.
+ #
+ # @example
+ # namespace :products do
+ # schema :product
+ # end
+ #
+ # @param name [Symbol] the schema name.
+ # @param options [Hash] A set of options to pass to {Endpoint#add_schema}.
+ def schema(name, options = {})
+ namespace.add_schema(name, options)
+ all { schema(name, options) }
end
+ # Defines a block that will be executed in every endpoint inside this namespace.
+ # It should only be one for namespace.
+ #
+ # @example
+ # resource :products, base_path: '/:country_id/products' do
+ # member do
+ # all do
+ # url_params(:country_id) { 'us' }
+ # end
+ #
+ # get :show # /us/products/:id
+ # put :update # /us/products/us/:id
+ # end
+ # end
+ #
+ # @param endpoints_config [block] block that will be called in the context of
+ # an {EndpointDSL} instance.
def all(&endpoints_config)
- self.common_endpoints_config_block = endpoints_config
+ self.endpoint_config_blocks << endpoints_config
end
+ # It calls {#all} with the {EndpointDSL#url_param} method.
+ # Please refer to the {EndpointDSL#url_param} method.
def url_param(param, &value_or_example_block)
all do
url_param(param, &value_or_example_block)
end
end
private
- def common_endpoints_config_block
- @common_endpoints_config_block ||= (Proc.new {})
+ def setup_endpoint_from_http_method(http_method, endpoint_name, path, &block)
+ endpoint(endpoint_name) do
+ self.method http_method
+ self.path path
+ instance_eval(&block) if block.present?
+ end
end
end
+ # The Endpoint DSL is what should be used inside an endpoint block.
+ # Its major responsability is to define endpoint behavior.
class EndpointDSL < Struct.new(:endpoint)
+ # Defines what the HTTP method will be.
+ #
+ # @example
+ # endpoint :show do
+ # method :get
+ # end
+ #
+ # @param method [Symbol] the http method name
def method(method)
endpoint.method = method
end
+ # Defines what the path will be. It will be append
+ # to the namespace's `base_path` and to the Restspec
+ # config's `base_url`.
+ #
+ # @example
+ # endpoint :monkeys do
+ # path '/monkeys'
+ # end
+ #
+ # @param path [String] the path of the endpoint
def path(path)
endpoint.path = path
end
- def schema(name, schema_extensions = {})
- endpoint.schema_name = name
- endpoint.schema_extensions = schema_extensions
+ # Defines what the schema will be.
+ #
+ # @example
+ # endpoint :monkeys do
+ # schema :monkey
+ # end
+ #
+ # @param name [Symbol] The schema's name.
+ # @param options [Hash] A set of options to pass to {Endpoint#add_schema}.
+ def schema(name, options = {})
+ endpoint.add_schema(name, options)
end
+ # Remove the schema of the current endpoint. Some endpoints
+ # doesn't require schemas for payload or for responses. This
+ # is the typical case for DELETE requests.
+ #
+ # @example
+ # resource :game do
+ # delete(:destroy) { no_schema }
+ # end
+ #
+ def no_schema
+ endpoint.remove_schemas
+ end
+
+ # A mutable hash containing the headers for the endpoint
+ #
+ # @example
+ # endpoint :monkeys do
+ # headers['Content-Type'] = 'application/json'
+ # end
def headers
endpoint.headers
end
+ # This set url parameters for the endpoint. It receives a key and a block
+ # returning the value. If the value is a type, an example will be generated.
+ #
+ # @example with just a value:
+ # endpoint :monkeys, path: '/:id' do
+ # url_param(:id) { '1' }
+ # end # /1
+ #
+ # @example with a type:
+ # endpoint :monkeys, path: '/:id' do
+ # url_param(:id) { string } # a random string
+ # end # /{the random string}
+ #
+ # @param param [Symbol] the parameter name.
+ # @param value_or_type_block [block] the block returning the value.
def url_param(param, &value_or_type_block)
endpoint.add_url_param_block(param) do
- value_or_type_context = ValueOrTypeContext.new
- value_or_type = value_or_type_context.instance_eval(&value_or_type_block)
+ ExampleOrValue.new(endpoint, param, value_or_type_block).value
+ end
+ end
- if value_or_type.is_a?(Restspec::Schema::Types::BasicType)
- attribute = if endpoint.schema && endpoint.schema.attributes[param]
- endpoint.schema.attributes[param]
- else
- Restspec::Schema::Attribute.new(param, value_or_type_context.integer)
- end
+ # A special class to get a type example or a simple
+ # value from a block.
+ #
+ # If the block returns a type, the result should
+ # be one example of that type.
+ class ExampleOrValue
+ def initialize(endpoint, attribute_name, callable)
+ self.attribute_name = attribute_name
+ self.endpoint = endpoint
+ self.callable = callable
+ end
- value_or_type.example_for(attribute)
+ def value
+ if example?
+ raw_value.example_for(get_attribute)
else
- value_or_type
+ raw_value
end
end
- end
- HTTP_METHODS.each do |http_method|
- define_method(http_method) do |path|
- self.method http_method
- self.path path
+ private
+
+ attr_accessor :attribute_name, :callable, :endpoint
+
+ def example?
+ raw_value.is_a?(Restspec::Schema::Types::BasicType)
end
- end
- end
- class ValueOrTypeContext
- Restspec::Schema::Types::ALL.each do |type_name, type_class|
- define_method(type_name) do |options = {}|
- type_class.new(options)
+ def get_attribute
+ if (schema = endpoint.schema_for(:payload)).present? && schema.attributes[attribute_name]
+ schema.attributes[attribute_name]
+ else
+ Restspec::Schema::Attribute.new(attribute_name, context.integer)
+ end
+ end
+
+ def raw_value
+ @raw_value ||= context.instance_eval(&callable)
+ end
+
+ def context
+ @context ||= Object.new.tap do |context|
+ context.extend(Restspec::Schema::Types::TypeMethods)
+ end
end
end
end
end
end