Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integer Validator improvement to support OpenAPI & AsyncAPI specs #830

Merged
merged 8 commits into from
Mar 19, 2024

Conversation

ankitk-me
Copy link
Contributor

@ankitk-me ankitk-me commented Mar 5, 2024

Description

  • integer validation (text & binary)
    • int32, int64
    • max and min value
    • multiple of N

Comment on lines 27 to 30
private boolean number;
private boolean sign;
int decimalPlaces;
private int value;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we use enum as a strategy object, the implementation needs to be stateless because the same instance of the enum constant is reused by all callers, so any state stored on the instance of the enum constant is also shared by all callers, causing a collision either across threads or due to overlapping fragmented validate calls even on the same thread.

Suggest moving the validation to a different class that can have state, and keeping just the remaining necessary behavioral parts here in the enum that can perform the checks needed by the stateful validator.

Comment on lines 27 to 31
max: 2147483647
min: -2147483648
exclusiveMax: false
exclusiveMin: false
multiple: 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these need to be explicitly specified, or are they the defaults?
If they are the defaults then they should be omitted, agree?

Comment on lines 94 to 96
int max = this.max != 0 ? this.max : Integer.MAX_VALUE;
int min = this.min != 0 ? this.min : Integer.MIN_VALUE;
int multiple = this.multiple != 0 ? this.multiple : DEFAULT_MULTIPLE;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps simpler to either assign defaults to fields initially (i think we do this elsewhere), or use boxed Integer and Boolean types for fields (but still pass primitives via methods) so that you can tell the difference between unassigned (null, method never called) vs assigned explicitly to a value matching the default value.

}

return valid ? length : -1;
return handler.validate(FLAGS_COMPLETE, data, index, length, next) ? length : -1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This -1 return value seems like a magic value - should be defined as a constant on ConverterHandler instead perhaps.

Comment on lines 41 to 45
this.max = config.max;
this.min = config.min;
this.exclusiveMax = config.exclusiveMax;
this.exclusiveMin = config.exclusiveMin;
this.multiple = config.multiple;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of storing all these fields separately, we can simplify to a single IntPredicate called check that verifies the conditions, so later instead of calling

conditions(value, max, min, exclusiveMax, exclusiveMin, multiple);

we can instead call

check.test(value)

so something like this in the constructor to assign check field

int max = config.max;
int min = config.min;
int multiple = config.multiple;
IntPredicate checkMax = config.exclusiveMax ? v -> v < max : v <= max;
IntPredicate checkMin = config.exclusiveMin ? v -> v > min : v >= min;
IntPredicate checkMultiple = v -> v % multiple == 0;
this.check = checkMax.and(checkMin).and(checkMultiple);

Comment on lines 49 to 105
@Override
public boolean validate(
int flags,
DirectBuffer data,
int index,
int length,
ValueConsumer next)
{
int position = 0;
boolean valid = true;
if ((flags & FLAGS_INIT) != 0x00)
{
value = 0;
progress = 0;
sign = false;
number = false;
byte digit = data.getByte(index);
if (format.negative(digit))
{
position += BitUtil.SIZE_OF_BYTE;
sign = true;
}
}

for (int numByteIndex = index + position; numByteIndex < index + length; numByteIndex++)
{
byte digit = data.getByte(numByteIndex);
if (format.digit(digit))
{
value = format.decode(value, digit);
number = true;
}
else
{
valid = false;
break;
}
progress += BitUtil.SIZE_OF_BYTE;
}

if (valid)
{
if ((flags & FLAGS_FIN) != 0x00 && format.validateFin(progress, number))
{
if (sign)
{
value *= -1;
}
valid = conditions(value, max, min, exclusiveMax, exclusiveMin, multiple);
}
else
{
valid = format.validateContinue(progress);
}
}
return valid;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we use MutableInteger, then enum can modify state without inadvertently sharing state across validations or workers.

Using decoded field to store incrementally decoded result across multiple calls to format.decode(...).
Using processed field to store total processed bytes across multiple calls to format.decode(...).

Suggested change
@Override
public boolean validate(
int flags,
DirectBuffer data,
int index,
int length,
ValueConsumer next)
{
int position = 0;
boolean valid = true;
if ((flags & FLAGS_INIT) != 0x00)
{
value = 0;
progress = 0;
sign = false;
number = false;
byte digit = data.getByte(index);
if (format.negative(digit))
{
position += BitUtil.SIZE_OF_BYTE;
sign = true;
}
}
for (int numByteIndex = index + position; numByteIndex < index + length; numByteIndex++)
{
byte digit = data.getByte(numByteIndex);
if (format.digit(digit))
{
value = format.decode(value, digit);
number = true;
}
else
{
valid = false;
break;
}
progress += BitUtil.SIZE_OF_BYTE;
}
if (valid)
{
if ((flags & FLAGS_FIN) != 0x00 && format.validateFin(progress, number))
{
if (sign)
{
value *= -1;
}
valid = conditions(value, max, min, exclusiveMax, exclusiveMin, multiple);
}
else
{
valid = format.validateContinue(progress);
}
}
return valid;
}
@Override
public boolean validate(
int flags,
DirectBuffer data,
int index,
int length,
ValueConsumer next)
{
if ((flags & FLAGS_INIT) != 0x00)
{
decoded.value = 0;
processed.value = 0;
}
int progress = format.decode(decoded, processed, data, index, length);
boolean valid = progress != IntegerFormat.INVALID_INDEX;
if ((flags & FLAGS_FIN) != 0x00)
{
valid &= format.valid(decoded, processed);
valid &= check.test(value);
}
return valid;
}

where the enum becomes something like...

public enum IntegerFormat
{
    public static final int INVALID_INDEX = -1;

    TEXT
    {
        @Override
        public int decode(
            MutableInteger decoded,
            MutableInteger processed,
            DirectBuffer data,
            int index,
            int length)
        {
            int progress = index;
            int limit = index + length;

            decode:
            for (; progress < limit; progress++)
            {
                int digit = data.getByte(progress);

                if (digit < '0' || '9' < digit)
                {
                    if (processed.value == 0))
                    {
                        switch (digit)
                        {
                        case '-':
                            decoded.value = Integer.MIN_VALUE;
                            processed.value++;
                            continue decode;
                        case '+':
                            decoded.value = Integer.MAX_VALUE;
                            processed.value++;
                            continue decode;
                        default:
                            break;
                        }
                    }

                    progress = INVALID_INDEX;
                    break decode;
                }

                int multipler = 10;

                if (processed.value == 1))
                {
                    switch (decoded.value)
                    {
                    case Integer.MIN_VALUE:
                        decoded.value = -1;
                        multipler = 1;
                        break;
                    case Integer.MAX_VALUE:
                        decoded.value = 1;
                        multipler = 1;
                        break;
                    default:
                        break;
                    }
                }

                decoded.value = decoded.value * multipler + (digit - '0');
                processed.value++;
            }
        }

        @Override
        public boolean valid(
            MutableInteger decoded,
            MutableInteger processed)
        {
            return processed.value > 1 || decoded.value != Integer.MIN_VALUE || decoded.value != Integer.MAX_VALUE;
        }
    },

    BINARY
    {
        private static final int INT32_SIZE = 4;

        @Override
        public int decode(
            MutableInteger decoded,
            MutableInteger processed,
            DirectBuffer data,
            int index,
            int length)
        {
            int progress = index;
            int limit = index + length;

            decode:
            for (; progress < limit; progress++)
            {
                int digit = data.getByte(progress);

                if (processed.value >= INT32_SIZE)
                {
                    progress = INVALID_INDEX;
                    break decode;
                }

                decoded.value <<= 8;
                decoded.value |= digit & 0xFF;
                processed.value++;
            }

            return progress;
        }

        @Override
        public boolean valid(
            MutableInteger decoded,
            MutableInteger processed)
        {
            return processed.value == INT32_SIZE;
        }
    }

Comment on lines 28 to 29
private final int max;
private final int min;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These be removed now, right?

Comment on lines 38 to 39
this.max = config.max;
this.min = config.min;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These can be local variables, agree?

}
else
{
assert false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be an exception instead of an assert, we need to know for sure things are working or not at runtime.

JsonValue value)
{
Int32ModelConfig result = null;
if (value instanceof JsonString)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check the value type instead of instanceof?

@ankitk-me ankitk-me marked this pull request as ready for review March 15, 2024 17:32
Comment on lines 27 to 28
MutableInteger decoded,
MutableInteger processed,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest we add a separate Int32State class and use instead of these two mutable integers.

public final class Int32State
{
    public int decoded;
    public int processed;
}

then pass an instance here as a parameter called state, and refer to the fields as needed below.


if (digit < '0' || '9' < digit)
{
if (processed.value == 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (processed.value == 0)
if (state.processed == 0)

Comment on lines 47 to 48
decoded.value = Integer.MIN_VALUE;
processed.value++;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
decoded.value = Integer.MIN_VALUE;
processed.value++;
state.decoded = Integer.MIN_VALUE;
state.processed++;

Comment on lines 51 to 52
decoded.value = Integer.MAX_VALUE;
processed.value++;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
decoded.value = Integer.MAX_VALUE;
processed.value++;
state.decoded = Integer.MAX_VALUE;
state.processed++;


int multipler = 10;

if (processed.value == 1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (processed.value == 1)
if (state.processed == 1)

Comment on lines 67 to 78
switch (decoded.value)
{
case Integer.MIN_VALUE:
decoded.value = -1 * (digit - '0');
break;
case Integer.MAX_VALUE:
decoded.value = digit - '0';
break;
default:
decoded.value = decoded.value * multipler + (digit - '0');
break;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
switch (decoded.value)
{
case Integer.MIN_VALUE:
decoded.value = -1 * (digit - '0');
break;
case Integer.MAX_VALUE:
decoded.value = digit - '0';
break;
default:
decoded.value = decoded.value * multipler + (digit - '0');
break;
}
switch (state.decoded)
{
case Integer.MIN_VALUE:
state.decoded = -1 * (digit - '0');
break;
case Integer.MAX_VALUE:
state.decoded = digit - '0';
break;
default:
state.decoded = state.decoded * multipler + (digit - '0');
break;
}

Comment on lines 82 to 84
decoded.value = decoded.value < 0
? decoded.value * multipler - (digit - '0')
: decoded.value * multipler + (digit - '0');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
decoded.value = decoded.value < 0
? decoded.value * multipler - (digit - '0')
: decoded.value * multipler + (digit - '0');
state.decoded = state.decoded < 0
? state.decoded * multipler - (digit - '0')
: state.decoded * multipler + (digit - '0');

? decoded.value * multipler - (digit - '0')
: decoded.value * multipler + (digit - '0');
}
processed.value++;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
processed.value++;
state.processed++;

Comment on lines 93 to 98
public boolean valid(
MutableInteger decoded,
MutableInteger processed)
{
return processed.value > 1 || decoded.value != Integer.MIN_VALUE || decoded.value != Integer.MAX_VALUE;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public boolean valid(
MutableInteger decoded,
MutableInteger processed)
{
return processed.value > 1 || decoded.value != Integer.MIN_VALUE || decoded.value != Integer.MAX_VALUE;
}
public boolean valid(
Int32State state)
{
return state.processed > 1 || state.decoded != Integer.MIN_VALUE || state.decoded != Integer.MAX_VALUE;
}

@ankitk-me ankitk-me changed the title Number Validator improvement to support OpenAPI & AsyncAPI specs Integer Validator improvement to support OpenAPI & AsyncAPI specs Mar 19, 2024
@jfallows jfallows merged commit 0f9950d into aklivity:develop Mar 19, 2024
5 checks passed
@jfallows jfallows linked an issue Apr 5, 2024 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support OpenAPI and AsyncAPI validation cases
2 participants