Class: Steppe::Endpoint
- Inherits:
-
Plumb::Pipeline
- Object
- Plumb::Pipeline
- Steppe::Endpoint
- Defined in:
- lib/steppe/endpoint.rb
Overview
Endpoint represents a single API endpoint with request validation, processing, and response handling.
Inherits from Plumb::Pipeline to provide composable request processing through steps. Each endpoint defines an HTTP verb, URL path pattern, input validation schemas, processing logic, and response serialization strategies.
Defined Under Namespace
Classes: BodyParser, DefaultEntitySerializer, HeaderValidator, PayloadValidator, QueryValidator, SecurityStep
Constant Summary collapse
- MatchContentType =
These types are used in the respond method pattern matching.
Types::String[ContentType::MIME_TYPE] | Types::Symbol
- MatchStatus =
Types::Integer | Types::Any[Range]
- FALLBACK_RESPONDER =
Fallback responder used when no matching responder is found for a status/content-type combination. Returns a JSON error message indicating the missing responder configuration.
Responder.new(statuses: (100..599), accepts: 'application/json') do |r| r.serialize do attribute :message, String def = "no responder registered for response status: #{result.response.status}" end end
- DefaultHTMLSerializer =
-> (conn) { html5 { head { title "Default #{conn.response.status}" } body { h1 "Default view" dl { dt "Response status:" dd conn.response.status.to_s dt "Parameters:" dd conn.params.inspect dt "Errors:" dd conn.errors.inspect } } } }
Instance Attribute Summary collapse
-
#description ⇒ Object
Returns the value of attribute description.
-
#path ⇒ Object
Returns the value of attribute path.
-
#payload_schemas ⇒ Object
readonly
Returns the value of attribute payload_schemas.
-
#registered_security_schemes ⇒ Object
readonly
Returns the value of attribute registered_security_schemes.
-
#rel_name ⇒ Object
readonly
Returns the value of attribute rel_name.
-
#responders ⇒ Object
readonly
Returns the value of attribute responders.
-
#tags ⇒ Object
Returns the value of attribute tags.
Instance Method Summary collapse
-
#call(conn) ⇒ Result
Main processing method that runs the endpoint pipeline and handles response.
-
#debug! ⇒ void
Adds a debugging breakpoint step to the endpoint pipeline.
-
#header_schema(sc = nil) ⇒ Object
Defines or returns the HTTP header validation schema.
-
#html(statuses = (200...300), view = nil) { ... } ⇒ self
Convenience method to define an HTML responder.
-
#initialize(service, rel_name, verb, path: '/') {|endpoint| ... } ⇒ Endpoint
constructor
Creates a new endpoint instance.
- #inspect ⇒ Object
-
#json(statuses = (200...300), serializer = nil) {|serializer| ... } ⇒ self
Convenience method to define a JSON responder.
- #no_spec! ⇒ Object
-
#node_name ⇒ Object
Node name for OpenAPI documentation.
-
#payload_schema(*args) ⇒ Object
Defines request body validation schema for a specific content type.
-
#query_schema(sc = nil) ⇒ Object
Defines or returns the query parameter validation schema.
-
#respond(*args) ⇒ self
Define how the endpoint responds to specific HTTP status codes and content types.
-
#run(request) ⇒ Result
Executes the endpoint pipeline for a given request.
-
#security(scheme_name, scopes = []) ⇒ void
Apply a security scheme to this endpoint with required scopes.
- #specced? ⇒ Boolean
-
#to_rack ⇒ Proc
Rack-compatible application callable.
-
#verb(vrb = nil) ⇒ Object
Gets or sets the HTTP verb for this endpoint.
Constructor Details
#initialize(service, rel_name, verb, path: '/') {|endpoint| ... } ⇒ Endpoint
Creates a new endpoint instance.
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 |
# File 'lib/steppe/endpoint.rb', line 251 def initialize(service, rel_name, verb, path: '/', &) # Do not setup with block yet super(freeze_after: false, &nil) @service = service @rel_name = rel_name @verb = verb @responders = ResponderRegistry.new @query_schema = Types::Hash @header_schema = Types::Hash @payload_schemas = {} @body_parsers = {} @registered_security_schemes = {} @description = 'An endpoint' @specced = true @tags = [] # This registers security schemes declared in the service # which may declare their own header, query or payload schemas service.registered_security_schemes.each do |name, scopes| security name, scopes end # This registers a query_schema # and a QueryValidator step self.path = path configure(&) if block_given? # Register default responders for common status codes respond 204, :json respond 304, :json respond 200..299, :json, DefaultEntitySerializer # TODO: match any content type # respond 304, '*/*' respond 401..422, :json, DefaultEntitySerializer respond 401..422, :html, DefaultHTMLSerializer freeze end |
Instance Attribute Details
#description ⇒ Object
Returns the value of attribute description.
237 238 239 |
# File 'lib/steppe/endpoint.rb', line 237 def description @description end |
#path ⇒ Object
Returns the value of attribute path.
236 237 238 |
# File 'lib/steppe/endpoint.rb', line 236 def path @path end |
#payload_schemas ⇒ Object (readonly)
Returns the value of attribute payload_schemas.
236 237 238 |
# File 'lib/steppe/endpoint.rb', line 236 def payload_schemas @payload_schemas end |
#registered_security_schemes ⇒ Object (readonly)
Returns the value of attribute registered_security_schemes.
236 237 238 |
# File 'lib/steppe/endpoint.rb', line 236 def registered_security_schemes @registered_security_schemes end |
#rel_name ⇒ Object (readonly)
Returns the value of attribute rel_name.
236 237 238 |
# File 'lib/steppe/endpoint.rb', line 236 def rel_name @rel_name end |
#responders ⇒ Object (readonly)
Returns the value of attribute responders.
236 237 238 |
# File 'lib/steppe/endpoint.rb', line 236 def responders @responders end |
#tags ⇒ Object
Returns the value of attribute tags.
237 238 239 |
# File 'lib/steppe/endpoint.rb', line 237 def @tags end |
Instance Method Details
#call(conn) ⇒ Result
Main processing method that runs the endpoint pipeline and handles response.
Flow:
-
Runs all registered steps (query validation, payload validation, business logic)
-
Resolves appropriate responder based on status code and Accept header
-
Runs responder pipeline to serialize and format response
668 669 670 671 672 673 674 675 676 677 678 679 |
# File 'lib/steppe/endpoint.rb', line 668 def call(conn) known_query_names = query_schema._schema.keys.map(&:to_sym) known_query = conn.request.steppe_url_params.slice(*known_query_names) conn.request.set_url_params!(known_query) conn = super(conn) accepts = conn.request.get_header('HTTP_ACCEPT') || ContentTypes::JSON responder = responders.resolve(conn.response.status, accepts) || FALLBACK_RESPONDER # Conn might be a Halt now, because a step halted processing. # We set it back to Continue so that the responder pipeline # can process it through its steps. responder.call(conn.valid) end |
#debug! ⇒ void
This method returns an undefined value.
Adds a debugging breakpoint step to the endpoint pipeline. Useful for development and troubleshooting.
641 642 643 644 645 646 |
# File 'lib/steppe/endpoint.rb', line 641 def debug! step do |conn| debugger conn end end |
#header_schema(schema) ⇒ void #header_schema ⇒ Plumb::Composable
HTTP header names in Rack env use the format ‘HTTP_*’ (e.g., ‘HTTP_AUTHORIZATION’)
Optional headers can be specified with a ‘?’ suffix (e.g., ‘HTTP_X_CUSTOM?’)
Security schemes automatically add their header requirements via SecurityStep
Defines or returns the HTTP header validation schema.
When called with a schema argument, registers a HeaderValidator step to validate HTTP headers. When called without arguments, returns the current header schema. Header schemas are automatically merged from security schemes and other composable steps.
404 405 406 407 408 409 410 |
# File 'lib/steppe/endpoint.rb', line 404 def header_schema(sc = nil) if sc step(HeaderValidator.new(sc)) else @header_schema end end |
#html(statuses = (200...300), view = nil) { ... } ⇒ self
Convenience method to define an HTML responder.
528 529 530 531 532 |
# File 'lib/steppe/endpoint.rb', line 528 def html(statuses = (200...300), view = nil, &block) respond(statuses, :html, view || block) self end |
#inspect ⇒ Object
290 291 292 |
# File 'lib/steppe/endpoint.rb', line 290 def inspect %(<#{self.class}##{object_id} [#{rel_name}] #{verb.to_s.upcase} #{path}>) end |
#json(statuses = (200...300), serializer = nil) {|serializer| ... } ⇒ self
Convenience method to define a JSON responder.
510 511 512 513 514 515 516 517 |
# File 'lib/steppe/endpoint.rb', line 510 def json(statuses = (200...300), serializer = nil, &block) respond(statuses:, accepts: :json) do |r| r.description = "Response for status #{statuses}" r.serialize serializer || block end self end |
#no_spec! ⇒ Object
300 |
# File 'lib/steppe/endpoint.rb', line 300 def no_spec! = @specced = false |
#node_name ⇒ Object
Node name for OpenAPI documentation
303 |
# File 'lib/steppe/endpoint.rb', line 303 def node_name = :endpoint |
#payload_schema(schema) ⇒ Object #payload_schema(content_type, schema) ⇒ Object
Defines request body validation schema for a specific content type.
Automatically registers a BodyParser step for the content type if not already registered, then registers a PayloadValidator step to validate the parsed body.
458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 |
# File 'lib/steppe/endpoint.rb', line 458 def payload_schema(*args) ctype, stp = case args in [Hash => sc] [ContentTypes::JSON, sc] in [Plumb::Composable => sc] [ContentTypes::JSON, sc] in [MatchContentType => content_type, Hash => sc] [content_type, sc] in [MatchContentType => content_type, Plumb::Composable => sc] [content_type, sc] else raise ArgumentError, "Invalid arguments: #{args.inspect}. Expects [Hash] or [Plumb::Composable], and an optional content type." end content_type = ContentType.parse(ctype) unless @body_parsers[content_type] step BodyParser.build(content_type) @body_parsers[ctype] = true end step PayloadValidator.new(content_type, stp) end |
#query_schema(schema) ⇒ void #query_schema ⇒ Plumb::Composable
Defines or returns the query parameter validation schema.
When called with a schema argument, registers a QueryValidator step to validate query parameters. When called without arguments, returns the current query schema.
428 429 430 431 432 433 434 |
# File 'lib/steppe/endpoint.rb', line 428 def query_schema(sc = nil) if sc step(QueryValidator.new(sc)) else @query_schema end end |
#respond(status) {|responder| ... } ⇒ self #respond(status, accepts) {|responder| ... } ⇒ self #respond(status, accepts, serializer) {|responder| ... } ⇒ self #respond(status_range, accepts, serializer) {|responder| ... } ⇒ self #respond(responder) ⇒ self #respond(**options) {|responder| ... } ⇒ self
Responders are resolved by ResponderRegistry using status code and Accept header
When ranges overlap, first registered responder wins
Default accept type is :json (application/json) when not specified
Define how the endpoint responds to specific HTTP status codes and content types.
Responders are registered in order and when ranges overlap, the first registered responder wins. This allows you to define specific handlers first, then fallback handlers for broader ranges.
613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 |
# File 'lib/steppe/endpoint.rb', line 613 def respond(*args, &) case args in [Responder => responder] @responders << responder in [MatchStatus => statuses] @responders << Responder.new(statuses:, &) in [MatchStatus => statuses, MatchContentType => accepts] @responders << Responder.new(statuses:, accepts:, &) in [MatchStatus => statuses, MatchContentType => accepts, Object => serializer] @responders << Responder.new(statuses:, accepts:, serializer:, &) in [Hash => kargs] @responders << Responder.new(**kargs, &) else raise ArgumentError, "Invalid arguments: #{args.inspect}" end self end |
#run(request) ⇒ Result
Executes the endpoint pipeline for a given request.
Creates an initial Continue result and runs it through the pipeline.
654 655 656 657 |
# File 'lib/steppe/endpoint.rb', line 654 def run(request) result = Result::Continue.new(nil, request:) call(result) end |
#security(scheme_name, scopes = []) ⇒ void
If authentication fails, returns 401 Unauthorized
If authorization fails (missing required scopes), returns 403 Forbidden
This method returns an undefined value.
Apply a security scheme to this endpoint with required scopes. The security scheme must be registered in the parent Service using #security_scheme, #bearer_auth, or #basic_auth. This adds a processing step that validates authentication/authorization before other endpoint logic runs.
366 367 368 369 370 371 |
# File 'lib/steppe/endpoint.rb', line 366 def security(scheme_name, scopes = []) scheme = service.security_schemes.fetch(scheme_name) scheme_step = SecurityStep.new(scheme, scopes:) @registered_security_schemes[scheme.name] = scopes step scheme_step end |
#specced? ⇒ Boolean
299 |
# File 'lib/steppe/endpoint.rb', line 299 def specced? = @specced |
#to_rack ⇒ Proc
Returns Rack-compatible application callable.
295 296 297 |
# File 'lib/steppe/endpoint.rb', line 295 def to_rack proc { |env| run(Steppe::Request.new(env)).response } end |
#verb(verb) ⇒ Symbol #verb ⇒ Symbol
Gets or sets the HTTP verb for this endpoint.
490 491 492 493 |
# File 'lib/steppe/endpoint.rb', line 490 def verb(vrb = nil) @verb = vrb if vrb @verb end |