Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions guides/parameters/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,26 @@ arguments = parameters.parse!(media_type, input)
user.update(arguments["user"])
```

## Constrain Enumerations

Use an enumeration to accept an exact set of values:

``` ruby
parameters = Protocol::Content::Parameters.build do
field "status", enumeration("draft", "published")
end
```

The hash form maps accepted input values to corresponding output values:

``` ruby
parameters = Protocol::Content::Parameters.build do
field "enabled", enumeration("true" => true, "false" => false)
end
```

Enumeration matching is exact. Values not present in the enumeration produce an `invalid_type` error.

## Convert Fields

Built-in types match `String` values exactly and convert compatible values to `Integer` and `Float`. A custom converter can be supplied as any object responding to `#call`:
Expand Down
1 change: 1 addition & 0 deletions lib/protocol/content/parameters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

require_relative "default"
require_relative "parameters/type"
require_relative "parameters/enumeration"
require_relative "parameters/error"
require_relative "parameters/result"
require_relative "parameters/value"
Expand Down
8 changes: 8 additions & 0 deletions lib/protocol/content/parameters/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ def upload(name, required: false, multiple: false)
return add(UploadField.new(name, required:, multiple:))
end

# Construct an enumeration converter from accepted values or an input-to-output mapping.
# @parameter values [Array(Object)] The accepted values.
# @parameter options [Hash] Additional input-to-output mappings.
# @returns [Enumeration] The enumeration converter.
def enumeration(*values, **options)
return Enumeration.build(*values, **options)
end

# Declare an array of scalar values or nested argument hierarchies.
# @parameter name [String] The array field name.
# @parameter type [Module | #call | Nil] The expected element type or converter.
Expand Down
44 changes: 44 additions & 0 deletions lib/protocol/content/parameters/enumeration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2026, by Samuel Williams.

module Protocol
module Content
module Parameters
# Converts an exact set of input values to corresponding output values.
class Enumeration
# Construct an enumeration from accepted values or an input-to-output mapping.
# @parameter values [Array(Object)] The accepted values.
# @parameter options [Hash] Additional input-to-output mappings.
# @returns [Enumeration] The enumeration converter.
def self.build(*values, **options)
mapping = values.to_h{|value| [value, value]}
mapping.update(options)
return new(mapping)
end

# Initialize an enumeration from an input-to-output mapping.
# @parameter mapping [Hash] The accepted inputs and corresponding outputs.
def initialize(mapping)
@mapping = mapping.freeze
end

# The accepted input values and corresponding output values.
attr :mapping

# Convert an accepted input value.
# @parameter value [Object] The input value.
# @returns [Object] The corresponding output value.
# @raises [ArgumentError] If the input value is not accepted.
def call(value)
if @mapping.key?(value)
return @mapping[value]
end

raise ArgumentError, "Invalid enumeration value: #{value.inspect}!"
end
end
end
end
end
1 change: 1 addition & 0 deletions releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Unreleased

- Add declarative content parameter filtering, conversion, validation, and upload handling.
- Add exact enumeration validation and input mapping for content parameters.

## v0.1.0

Expand Down
46 changes: 46 additions & 0 deletions test/protocol/content/parameters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,52 @@ def multipart_body(*parts)
expect(result["value"]).to be_a(type)
end

it "accepts enumerated values" do
statuses = ["draft", "published"]
enumeration = subject::Enumeration.build(*statuses)
parameters = subject.build do
field "status", enumeration
field "coordinates", enumeration([1, 2])
end

valid = parse_json(parameters, '{"status":"published","coordinates":[1,2]}')
invalid = parse_json(parameters, '{"status":"deleted"}')

expect(valid.value).to be == {"status" => "published", "coordinates" => [1, 2]}
expect(invalid.value).to be == {}
expect(invalid.errors.map(&:path)).to be == [["status"]]
expect(invalid.errors.map(&:code)).to be == [:invalid_type]
end

it "maps enumerated input values" do
parameters = subject.build do
enumeration = enumeration("pending", "yes" => true, "no" => false, "none" => nil)
field "enabled", enumeration
field "disabled", enumeration
field "unspecified", enumeration
field "status", enumeration
end

result = parse_json(parameters, '{"enabled":"yes","disabled":"no","unspecified":"none","status":"pending"}')

expect(result).to be(:valid?)
expect(result.value).to be == {
"enabled" => true,
"disabled" => false,
"unspecified" => nil,
"status" => "pending",
}
end

it "owns its enumeration mapping" do
mapping = {"draft" => "unpublished"}
enumeration = subject::Enumeration.build(**mapping)
mapping["published"] = "published"

expect(enumeration.mapping).to be(:frozen?)
expect(enumeration.mapping).to be == {"draft" => "unpublished"}
end

it "collects custom converter failures" do
converter = ->(_value){raise ArgumentError}
parameters = subject.build do
Expand Down