Module: Spikard::Introspection
- Defined in:
- lib/spikard/introspection.rb
Overview
Handler introspection via Method#parameters.
Defined Under Namespace
Constant Summary collapse
- BODYLESS_METHODS =
Set.new(%w[GET HEAD OPTIONS TRACE CONNECT]).freeze
- SPECIAL_PARAM_NAMES =
Set.new(%w[self]).freeze
Class Method Summary collapse
-
.derive_json_schema(body_type) ⇒ Object
Derive JSON Schema from a Dry::Struct class.
-
.introspect(handler, method, path, body_type) ⇒ Object
Analyse handler and produce a RouteSpec describing how to bind requests.
-
.json_schema_type_for(dry_type) ⇒ Object
Map a Dry::Types type to a JSON Schema type string.
-
.path_param_names(path) ⇒ Object
Return the set of path-parameter names in path (handles id and id:int).
Class Method Details
.derive_json_schema(body_type) ⇒ Object
Derive JSON Schema from a Dry::Struct class.
Returns nil if body_type is not a Dry::Struct. Otherwise builds a JSON Schema object by iterating the struct's schema attributes.
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/spikard/introspection.rb', line 45 def self.derive_json_schema(body_type) return nil unless body_type.is_a?(Class) && body_type < Dry::Struct properties = {} required = [] body_type.schema.each do |key| attr_name = key.name.to_s attr_type = key.type attr_required = key.required? # Map Dry type to JSON Schema type json_type = json_schema_type_for(attr_type) properties[attr_name] = { "type" => json_type } required << attr_name if attr_required end { "type" => "object", "properties" => properties, "required" => required } end |
.introspect(handler, method, path, body_type) ⇒ Object
Analyse handler and produce a RouteSpec describing how to bind requests.
The handler receives the hydrated body DTO (if body_type provided) as its FIRST POSITIONAL parameter, and remaining keyword-arg bindings by name.
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/spikard/introspection.rb', line 104 def self.introspect(handler, method, path, body_type) path_names = path_param_names(path) bindings = [] body_param_name = nil # The first positional parameter (if any) becomes the body param when # body_type is provided. All other parameters become keyword-arg bindings. positional_idx = 0 # Use handler.parameters (the block's own params), NOT # handler.method(:call).parameters, which for a Proc reflects Proc#call's # signature ([[:rest]]) rather than the block's declared params. handler.parameters.each do |param_type, param_name| next if SPECIAL_PARAM_NAMES.include?(param_name.to_s) next if param_name.nil? # Skip *args and **kwargs next if param_type == :rest param_name_str = param_name.to_s # First required/optional positional parameter becomes the body param # (only if body_type was explicitly provided) if body_type && (param_type == :req || param_type == :opt) && positional_idx == 0 body_param_name = param_name_str positional_idx += 1 next end # Keyword parameters become bindings (query, path, header, cookie) if param_type == :key || param_type == :keyreq # Determine source by name (path > query) source = if path_names.include?(param_name_str) "path" else "query" end bindings << Binding.new( param_name_str, source, String, nil ) end end RouteSpec.new(handler, bindings, body_param_name, body_type) end |
.json_schema_type_for(dry_type) ⇒ Object
Map a Dry::Types type to a JSON Schema type string.
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/spikard/introspection.rb', line 74 def self.json_schema_type_for(dry_type) # Get the primitive class from the Dry type. primitive = dry_type.respond_to?(:primitive) ? dry_type.primitive : nil # Match on the class NAME, not `case primitive when Integer`: `===` on a # class (`Integer === Integer`) is false because the class object is not # an instance of itself, so a `when` clause would never fire. case primitive.to_s when "String", "Symbol" "string" when "Integer" "integer" when "Float", "Numeric", "BigDecimal" "number" when "TrueClass", "FalseClass" "boolean" when "Array" "array" when "Hash" "object" else # Default fallback "string" end end |
.path_param_names(path) ⇒ Object
Return the set of path-parameter names in path (handles id and id:int).
157 158 159 160 |
# File 'lib/spikard/introspection.rb', line 157 def self.path_param_names(path) # Match {name} and typed {name:converter} path segments path.scan(/\{(\w+)(?::[^{}]+)?\}/).map(&:first).to_set end |