class Model

An immutable model for parsing, filtering, and validating parameters.

Definitions

def initialize(parser, fields, strict: true)

Initialize a parameter model.

Signature

parameter parser Parser

The content parser.

parameter fields Hash

The parameter fields.

parameter strict Boolean

Whether unknown fields should produce validation errors.

Implementation

def initialize(parser, fields, strict: true)
	@parser = parser
	@fields = fields
	@strict = strict
end

attr :fields

The fields in this model, indexed by name.

def parse(media_type, input, &upload_handler)

Parse, filter, and validate content parameters.

Signature

parameter media_type String | Protocol::Media::Type | Nil

The content media type.

parameter input Object

The readable content input.

yields {|name, upload| ...}

Each streaming upload. Its return value is inserted into the parsed value.

returns Result

The parsed value and validation errors.

Implementation

def parse(media_type, input, &upload_handler)
	# Replace ephemeral multipart uploads with outcomes which can survive until validation:
	value = @parser.parse(media_type, input) do |name, item|
		if item.is_a?(Protocol::Multipart::FormData::Upload)
			path = Protocol::URL::Encoding.split(name)
			
			# Only process uploads accepted by an explicit field:
			if upload_handler && field = upload_field(path)
				field.process(name, item, &upload_handler)
			else
				Value::OMITTED
			end
		else
			item
		end
	end
	
	# Apply the model after parsing so ordinary values and upload outcomes follow the same hierarchy:
	errors = []
	value = apply(value, errors)
	return Result.new(value, errors)
end

def parse!(media_type, input, &block)

Parse content parameters, raising when validation fails.

Signature

parameter media_type String | Protocol::Media::Type | Nil

The content media type.

parameter input Object

The readable content input.

yields {|name, upload| ...}

Each streaming upload. Its return value is inserted into the parsed value.

returns Hash

The valid value.

raises ValidationError

If validation fails.

Implementation

def parse!(media_type, input, &block)
	result = parse(media_type, input, &block)
	
	if result.valid?
		return result.value
	end
	
	raise ValidationError, result
end

def apply(value, errors, path = [])

Apply this model to an existing argument hierarchy.

Signature

parameter value Object

The parameter hierarchy.

parameter errors Array(Error)

The validation error destination.

parameter path Array(String | Integer)

The current argument path.

returns Hash

The filtered and converted value.

Implementation

def apply(value, errors, path = [])
	# Parameter models always apply to a key/value hierarchy:
	unless value.is_a?(Hash)
		errors << Error.new(path, :invalid_type, expected: Hash, value: value)
		return {}
	end
	
	# Copy the input so declared fields can be removed without modifying caller-owned data:
	input = value.dup
	output = {}
	
	# Apply declared values and collect missing required parameters:
	@fields.each do |name, field|
		item_path = path + [name]
		
		if input.key?(name)
			item = input.delete(name)
			
			if item.equal?(Value::OMITTED)
				if field.required?
					errors << Error.new(item_path, :required)
				end
			else
				field.apply(item, output, errors, item_path)
			end
		elsif field.required?
			errors << Error.new(item_path, :required)
		end
	end
	
	# Reject remaining undeclared values when strict validation is enabled:
	if @strict
		input.each_key do |name|
			errors << Error.new(path + [name], :unknown)
		end
	end
	
	return output
end

def accepts_upload?(path)

Whether an upload path is explicitly accepted by this model.

Signature

parameter path Array(String)

The decoded upload path.

returns Boolean

Whether the upload is accepted.

Implementation

def accepts_upload?(path)
	return !!upload_field(path)
end

def upload_field(path)

Find the upload field which accepts the given decoded path.

Signature

parameter path Array(String)

The decoded upload path.

returns UploadField | Nil

The accepting upload field.

Implementation

def upload_field(path)
	# Walk fields using the decoded components of the form name:
	name, *remaining = path
	
	unless field = @fields[name]
		return nil
	end
	
	return field.upload_field(remaining)
end

def freeze

Freeze this model and its fields.

Signature

returns self

The frozen model.

Implementation

def freeze
	return self if self.frozen?
	
	@parser.freeze
	@fields.each_value(&:freeze)
	@fields.freeze
	super
end