ml-archive/submissions

How to use Integer-valued text fields with Submissions 2.0?

Opened this issue · 6 comments

I'm trying to display a text field where the user could enter arbitrary integer values. For this, I currently have

	struct Submission: Decodable, Reflectable, FieldsRepresentable {
		let integerField: Int?
		
		static func makeFields(for instance: Submission?) throws -> [Field] {
			return try [
				Field(keyPath: \.integerField, instance: instance, label: "Integer Field")
			]
		}
	}

Submitting works fine as long as the text entered into the field actually parses as an integer. When entering a non-integer value, however, I get the following error message on the console:

URLEncodedFormError.fwi: Could not convert to `Int32`: str("entered_text") (NIOServer.swift:104)
  • I understand that entering a string into an integer field would be an error. However, what would be the recommended approach to surfacing this to the user and still internally use the integer value as conveniently as possible?
  • Should I instead declare integerField as String? and, before using it, parse it via e.g. submission.integerField.flatMap { Int($0) }? Is there a way to avoid the aforementioned parsing?
  • And/or should I add an "IntegerValidator" type to my fields list? If so, is one available already, or would I have to build my own, or shall I use RangeValidator?

Those are good questions!
The way I see it there is no way around using a String since that models the possible inputs the best. One way to get the integer out after validation is as you suggest to flatMap but that requires a force unwrap. An alternative is to model the input in 2 ways, once for validation and once for the creation of the final model. Note that this only works for URLEncoded data (like from a form), not for JSON encoded data.
Submissions doesn't come with an IntegerValidator so you'd have to create your own.

A solution that uses the Creatable protocol from Sugar could look something like this:

struct IntegerWrapper: Decodable, Submittable, Creatable {
    typealias Create = IntegerWrapper
    let integerField: Int

    struct Submission: SubmissionType {
        let integerField: String?

        static func makeFields(for instance: Submission?) throws -> [Field] {
            return try [
                Field(
                    keyPath: \.integerField,
                    instance: instance,
                    label: "Integer Field",
                    validators: [Validator("") {
                        if Int($0) == nil {
                            throw BasicValidationError("\($0) is not a valid number.")
                        }
                    }]
                )
            ]
        }
    }
}

You could then use it as simply as this:

func f(request: Request) throws -> Future<IntegerWrapper> {
    return IntegerWrapper.create(on: request)
}

This would perform the validation using the Submission type before decoding the IntegerWrapper itself from the same request.

I just realized that you’ll need to conform to SelfCreatable because what I wrote depends on an upcoming small improvement to nodes-vapor/sugar

I updated Sugar to support my example

@siemensikkema thank you! This is what I ended up using as my validator:

import Validation

public struct LosslesslyConvertibleFromStringValidator<T: LosslessStringConvertible>: ValidatorType {
	public typealias ValidationData = String
	
	public var validatorReadable = "convertible to \(T.self)"
	
	public func validate(_ data: String) throws {
		guard let parsedValue = T(data),
			parsedValue.description == data
			else { throw BasicValidationError("can not be converted to \(T.self)") }
	}
}

extension Validator where T == String {
	public static func losslesslyConvertible<T: LosslessStringConvertible>(to: T.Type) -> Validator<String> {
		return LosslesslyConvertibleFromStringValidator<T>().validator()
	}
}

For now I am simply doing a if let integerField = (submission.integerField.flatMap { Int32($0) }) { in the func update(_ submission: Submission) throws { of that type's editor controller. Let me know if I'm missing out on something by not using the IntegerWrapper you suggested.

@MrMage The IntegerWrapper was just an example name for the model. See it as the model that you ideally would like to decode from the input if you would know that the input was valid.
The validator look 👍
Can you share the controller code? I'm curious to see how you're using it 🙂

@siemensikkema sure; would you mind DMing me on Twitter?