App-Test-Generator
view release on metacpan or search on metacpan
lib/App/Test/Generator.pm view on Meta::CPAN
}
if(defined($schema_file)) {
$schema = _load_schema($schema_file);
}
$test_file = $params->{'output_file'};
} else {
# Legacy API
($schema_file, $test_file) = @_;
if(defined($schema_file)) {
$schema = _load_schema($schema_file);
} else {
croak 'Usage: generate(schema_file [, outfile])';
}
}
# Parse the schema file and load into our structures
my %input = %{_load_schema_section($schema, 'input', $schema_file)};
my %output = %{_load_schema_section($schema, 'output', $schema_file)};
my %transforms = %{_load_schema_section($schema, 'transforms', $schema_file)};
my %accessor = %{_load_schema_section($schema, 'accessor', $schema_file)};
my %cases = %{$schema->{cases}} if(exists($schema->{cases}));
my %edge_cases = %{$schema->{edge_cases}} if(exists($schema->{edge_cases}));
my %type_edge_cases = %{$schema->{type_edge_cases}} if(exists($schema->{type_edge_cases}));
$module = $schema->{module} if(exists($schema->{module}) && length($schema->{module}));
$function = $schema->{function} if(exists($schema->{function}));
if(exists($schema->{new})) {
$new = defined($schema->{'new'}) ? $schema->{new} : '_UNDEF';
}
$yaml_cases = $schema->{yaml_cases} if(exists($schema->{yaml_cases}));
$seed = $schema->{seed} if(exists($schema->{seed}));
$iterations = $schema->{iterations} if(exists($schema->{iterations}));
my @edge_case_array = @{$schema->{edge_case_array}} if(exists($schema->{edge_case_array}));
_validate_config($schema);
my %config = %{$schema->{config}} if(exists($schema->{config}));
_normalize_config(\%config);
# Guess module name from config file if not set
if(!$module) {
if($schema_file) {
($module = basename($schema_file)) =~ s/\.(conf|pl|pm|yml|yaml)$//;
$module =~ s/-/::/g;
# Guard against Perl builtin function names being mistaken
# for module names â builtins have no module to load
if(_is_perl_builtin($module)) {
undef $module;
}
}
} elsif($module eq $MODULE_BUILTIN) {
undef $module;
}
if($module && length($module) && ($module ne 'builtin')) {
_validate_module($module, $schema_file);
}
# $module/$function are spliced unescaped into generated test
# source below (use_ok, new_ok, ->$function, $module::$function)
# â reject anything that isn't identifier-shaped before that happens.
_assert_identifier($module, 'module', package => 1) if defined($module) && length($module);
# sensible defaults
$function ||= 'run';
# package => 1: fully-qualified sub names (e.g. DB::DB, a debugger
# hook installed into the DB:: package regardless of its source
# package) are legitimate function names, not just bare identifiers
_assert_identifier($function, 'function', package => 1);
$iterations ||= DEFAULT_ITERATIONS; # default fuzz runs if not specified
$seed = undef if defined $seed && $seed eq ''; # treat empty as undef
# --- YAML corpus support (yaml_cases is filename string) ---
my %yaml_corpus_data;
if (defined $yaml_cases) {
croak("$yaml_cases: $!") if(!-f $yaml_cases);
my $yaml_data = LoadFile(Encode::decode('utf8', $yaml_cases));
if ($yaml_data && ref($yaml_data) eq 'HASH') {
# Validate that the corpus inputs are arrayrefs
# e.g: "FooBar": ["foo_bar"]
# Skip only invalid entries:
for my $expected (keys %{$yaml_data}) {
my $outputs = $yaml_data->{$expected};
unless($outputs && (ref $outputs eq 'ARRAY')) {
carp("$yaml_cases: $expected does not point to an array ref, ignoring");
next;
}
$yaml_corpus_data{$expected} = $outputs;
}
}
}
# Merge Perl %cases and YAML corpus safely
# my %all_cases = (%cases, %yaml_corpus_data);
my %all_cases = (%yaml_corpus_data, %cases);
for my $k (keys %yaml_corpus_data) {
if (exists $cases{$k} && ref($cases{$k}) eq 'ARRAY' && ref($yaml_corpus_data{$k}) eq 'ARRAY') {
$all_cases{$k} = [ @{$yaml_corpus_data{$k}}, @{$cases{$k}} ];
}
}
if(my $hints = delete $schema->{_yamltest_hints}) {
if(my $boundaries = $hints->{boundary_values}) {
push @edge_case_array, @{$boundaries};
}
if(my $invalid = $hints->{invalid}) {
carp('TODO: handle yamltest_hints->invalid');
}
}
# If the schema says the type is numeric, normalize
if ($schema->{type} && $schema->{type} =~ /^(integer|number|float)$/) {
for (@edge_case_array) {
next unless defined $_;
$_ += 0 if Scalar::Util::looks_like_number($_);
}
}
lib/App/Test/Generator.pm view on Meta::CPAN
}
# Ensure properties is always a hashref â if absent or set to
# a non-hash value, replace with a disabled default so that
# downstream code can safely dereference it without checking ref()
$config->{$CONFIG_PROPERTIES_KEY} = { enable => 0 } unless ref($config->{$CONFIG_PROPERTIES_KEY}) eq 'HASH';
}
# --------------------------------------------------
# _valid_type
#
# Determine whether a string is a
# recognised schema field type accepted
# by the generator.
#
# Entry: $type - the type string to validate.
# May be undef.
#
# Exit: Returns 1 if the type is known,
# 0 if the type is unknown or undef.
#
# Notes: The lookup hash is declared with
# 'state' so it is built only once per
# process rather than on every call â
# important since _valid_type is called
# in a loop over all input parameters.
#
# 'int' and 'bool' are accepted as
# aliases for 'integer' and 'boolean'
# respectively, for compatibility with
# schemas generated by external tools
# that use the shorter forms.
# --------------------------------------------------
sub _valid_type {
my $type = $_[0];
# Undef is never a valid type
return 0 unless defined($type);
# Build the lookup table once and cache it for
# the lifetime of the process via 'state'
state %VALID = map { $_ => 1 } qw(
string boolean integer number float
hashref arrayref object int bool any
);
return($VALID{$type} // 0);
}
# --------------------------------------------------
# _assert_identifier
#
# Purpose: Validate that a string is shaped like a
# plain Perl identifier (or, with
# package => 1, a "::"-separated package
# name) before it is spliced into generated
# test source as a bareword, package name,
# method name, or variable name rather than
# a quoted string literal. Schema-derived
# names (module, function, transform names)
# are spliced unescaped at the call sites
# that use this guard, so an unvalidated
# name could otherwise break out of the
# generated source and inject arbitrary
# Perl into a file that L<prove> will run.
#
# Entry: $name - the string to validate.
# $what - short label for the value, used
# only in the croak message.
# %opts - package => 1 allows "::"
# separators in $name.
#
# Exit: Returns $name unchanged on success.
# Croaks if $name is not identifier-shaped.
# --------------------------------------------------
sub _assert_identifier {
my ($name, $what, %opts) = @_;
croak(__PACKAGE__, ": $what is missing or empty")
unless defined($name) && length($name);
my $re = $opts{package}
? qr/^[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*\z/
: qr/^[A-Za-z_]\w*\z/;
croak(__PACKAGE__, ": $what '$name' is not a valid Perl identifier")
unless $name =~ $re;
return $name;
}
# --------------------------------------------------
# _validate_module
#
# Purpose: Check whether the module named in a
# schema can be found in @INC during
# test generation. Optionally also
# attempts to load it if the
# GENERATOR_VALIDATE_LOAD environment
# variable is set.
#
# Entry: $module - the module name to
# check. If undef or
# empty, returns 1
# immediately (builtin
# functions need no
# module).
# $schema_file - path to the schema
# file, used in warning
# messages only.
#
# Exit: Returns 1 if the module was found
# (and loaded, if validation was
# requested).
# Returns 0 if the module was not
# found or failed to load â this is
# non-fatal; generation continues.
# Returns 1 immediately for undef or
# empty $module.
#
# Side effects: Prints to STDERR when TEST_VERBOSE
lib/App/Test/Generator.pm view on Meta::CPAN
# Skip non-arrayref values â mixed hashes are allowed by callers
next unless ref($aref) eq 'ARRAY';
# Render each array element via perl_quote so strings are
# properly quoted and numbers are left unquoted
my $vals = join(', ', map { perl_quote($_) } @{$aref});
# Use "\t" rather than a literal tab for clarity
push @entries, "\t" . perl_quote($k) . " => [ $vals ]";
}
return join(",\n", @entries);
}
# --------------------------------------------------
# _has_positions
#
# Purpose: Determine whether any field in an input
# spec hashref declares a positional argument
# via the 'position' key.
#
# Entry: $input_spec - the input section of a parsed
# schema, expected to be a hashref whose values
# are themselves hashrefs containing field specs.
# May be undef or a non-hash ref.
#
# Exit: Returns 1 if any field has a defined
# 'position' key, 0 otherwise.
#
# Notes: Returns 0 immediately for undef or non-hash
# input rather than throwing â callers use the
# return value as a boolean and do not expect
# exceptions from this function.
# --------------------------------------------------
sub _has_positions {
my $input_spec = $_[0];
# Guard against undef or non-hash input â keys %$undef would throw
return 0 unless defined($input_spec) && ref($input_spec) eq 'HASH';
for my $field (keys %{$input_spec}) {
# Only examine fields whose spec is a hashref â scalar specs
# (e.g. input: { type: string }) cannot have positions
next unless ref($input_spec->{$field}) eq 'HASH';
# Return immediately on first match â no need to scan further
return 1 if defined $input_spec->{$field}{position};
}
# No positional arguments found in any field
return 0;
}
# --------------------------------------------------
# q_wrap
#
# Purpose: Wrap a string in the most readable
# q{} form that does not require escaping,
# falling back to single-quoted form with
# escaped apostrophes if no delimiter is
# available.
#
# Entry: $s - the string to wrap. May be undef.
# Exit: Returns a Perl source-code fragment that
# evaluates to the original string value,
# or the string 'undef' if $s is undef.
#
# Notes: index() returns -1 when not found and
# any value >= 0 when found, including 0
# for a delimiter at the start of the
# string. We compare against $INDEX_NOT_FOUND
# to make this boundary explicit and to
# prevent off-by-one mutation survivors.
# See GitHub issue #1.
# --------------------------------------------------
sub q_wrap {
my $s = $_[0];
croak('q_wrap: argument must be a plain string, not a reference') if ref($s);
# Return empty string for undef â this function is a low-level
# string quoter only. Callers that need the Perl literal 'undef'
# for undefined values should use perl_quote() instead, which
# handles the undef -> 'undef' semantic conversion correctly.
# Returning '' here preserves the original behaviour and avoids
# injecting the bare word 'undef' into contexts that expect a
# quoted string value.
return "''" unless defined $s;
# Try bracket-form q{} delimiters first â most readable
for my $p (@Q_BRACKET_PAIRS) {
my ($l, $r) = @{$p};
# Only use this bracket pair if neither bracket
# appears in the string â both must be checked
return "q$l$s$r" unless $s =~ /\Q$l\E|\Q$r\E/;
}
# Try single-character delimiters in preference order
for my $d (@Q_SINGLE_DELIMITERS) {
# index() returns $INDEX_NOT_FOUND (-1) when not found.
# Must use != $INDEX_NOT_FOUND rather than > 0 since
# the delimiter may legitimately appear at position 0
return "q$d$s$d" if index($s, $d) == $INDEX_NOT_FOUND;
}
# Last resort â single-quoted string with escaped apostrophes
(my $esc = $s) =~ s/'/\\'/g;
return "'$esc'";
}
# --------------------------------------------------
# perl_sq
#
# Purpose: Escape a string for safe inclusion
# inside a single-quoted Perl string
# literal in generated test code.
#
# Entry: $s - the string to escape.
# Exit: Returns the escaped string, or an
# empty string if $s is undef.
#
# Notes: NUL byte replacement produces the
# two-character sequence \0 which is
# only correct when the result is used
# inside a double-quoted string context
# in the generated test.
#
# The \b substitution (backspace) is
# intentionally omitted â in Perl regex
# context \b means word boundary, not
# backspace, so substituting it here
# would corrupt strings containing word
# boundaries.
# --------------------------------------------------
sub perl_sq {
my $s = $_[0];
croak('perl_sq: argument must be a plain string, not a reference') if ref($s);
# Return empty string for undef â callers that need
# 'undef' literal should use perl_quote instead
return '' unless defined $s;
# Escape backslashes first so later substitutions
# don't double-escape already-escaped sequences
$s =~ s/\\/\\\\/g;
# Escape apostrophes so they don't terminate the
# surrounding single-quoted string literal
$s =~ s/'/\\'/g;
# Escape common control characters to their
# printable two-character escape sequences
$s =~ s/\n/\\n/g;
$s =~ s/\r/\\r/g;
$s =~ s/\t/\\t/g;
$s =~ s/\f/\\f/g;
# Replace NUL bytes with \0 â valid only in
# double-quoted string context in generated code
$s =~ s/\0/\\0/g;
return $s;
}
=head2 perl_quote
Convert any Perl value into a source-code fragment that reproduces that value
when evaluated in a generated test file.
=head3 Arguments
=over 4
=item * C<$v>
Any Perl value. May be undef, a scalar, an arrayref, a Regexp, or a blessed
object. All types are handled â undef becomes C<'undef'>, the strings
C<'true'>/C<'false'> become the Perl boolean constants C<!!1>/C<!!0>,
numbers are unquoted, other strings are single-quoted, arrayrefs recurse,
Regexps become C<qr{...}>, and anything else (including hashrefs and
blessed objects) falls through to C<render_fallback>.
=back
=head3 API specification
=head4 input
{ v => { type => 'any', optional => 1 } }
=head4 output
{ type => 'string' }
=cut
sub perl_quote {
my ($v) = @_;
return _perl_quote($v, 0);
}
sub _perl_quote {
my ($v, $depth) = @_;
no warnings 'recursion'; ## no critic (TestingAndDebugging::ProhibitNoWarnings)
croak('perl_quote: structure too deeply nested (circular reference?)') if $depth > 100;
# Undef produces the Perl literal 'undef'
return 'undef' unless defined $v;
# Convert YAML boolean string literals to Perl
# boolean constants so they survive round-tripping
return '!!1' if $v eq 'true';
lib/App/Test/Generator.pm view on Meta::CPAN
# Guard: skip transforms with no input or with the
# YAML scalar 'undef' as their input â these have no
# generator and cannot produce meaningful properties
if(!defined($input_spec) ||
(!ref($input_spec) && $input_spec eq 'undef')) {
next;
}
# Guard: skip transforms whose input is not a hashref â
# must come before the helper calls below so we never
# pass a non-hash to _detect_transform_properties or
# _process_custom_properties
next unless ref($input_spec) eq 'HASH';
# Default output spec to empty hash so _STATUS lookups
# below are always safe regardless of schema content
my $output_spec = $transform->{output} // {};
# Detect automatic properties from the transform spec
# (range constraints, type preservation, definedness)
my @detected_props = _detect_transform_properties(
$transform_name,
$input_spec,
$output_spec
);
# Process any custom properties defined in the schema
my @custom_props = ();
if(exists($transform->{properties}) &&
ref($transform->{properties}) eq 'ARRAY') {
@custom_props = _process_custom_properties(
$transform->{properties},
$function,
$module,
$input_spec,
$output_spec,
$new
);
}
# Combine auto-detected and custom properties into one list
my @all_props = (@detected_props, @custom_props);
# Skip this transform if no properties were produced â
# nothing useful to render into the generated test
next unless @all_props;
# Build the LectroTest generator specification string,
# one entry per input field that has a generator
my @generators;
my @var_names;
for my $field (sort keys %{$input_spec}) {
my $spec = $input_spec->{$field};
# Skip non-hashref field specs â scalar types
# like 'string' have no generator sub-structure
next unless ref($spec) eq 'HASH';
# $field is spliced unescaped into the generated
# LectroTest generator spec by
# _schema_to_lectrotest_generator() â reject anything
# that isn't identifier-shaped first.
_assert_identifier($field, 'input field name');
my $gen = _schema_to_lectrotest_generator($field, $spec);
if(defined($gen) && length($gen)) {
push @generators, $gen;
push @var_names, $field;
}
}
my $gen_spec = join(', ', @generators);
# Build the call expression for the function under test.
# Note: property tests always construct a fresh object
# via new_ok() with no constructor arguments, regardless
# of what $new holds in the caller â the intent here is
# to test the method in isolation, not with specific
# construction state.
my $call_code;
if($module && defined($new)) {
# OO mode â construct a fresh object for each trial
$call_code = "my \$obj = new_ok('$module');";
$call_code .= "\$obj->$function";
} elsif($module && $module ne $MODULE_BUILTIN) {
# Functional mode with a named module
$call_code = "$module\::$function";
} else {
# Builtin or unqualified function call
$call_code = $function;
}
# Build the argument list, respecting positional order
# if the input spec declares positions
my @args;
if(_has_positions($input_spec)) {
# Sort fields by declared position so the generated
# call passes arguments in the correct order
my @sorted = sort {
$input_spec->{$a}{position} <=>
$input_spec->{$b}{position}
} keys %{$input_spec};
@args = map { "\$$_" } @sorted;
} else {
# No positions â use alphabetical order from @var_names
@args = map { "\$$_" } @var_names;
}
my $args_str = join(', ', @args);
# Concatenate all property check expressions with &&
# so the generated property block passes only when
# every check holds
my @checks = map { $_->{code} } @all_props;
my $property_checks = join(" &&\n\t", @checks);
# Determine expected behaviour from output _STATUS.
# Note: the schema convention uses 'WARNS' not 'WARN'
my $should_die = ($output_spec->{'_STATUS'} // '') eq 'DIES';
lib/App/Test/Generator.pm view on Meta::CPAN
my $min = $spec->{'min'};
my $max = $spec->{'max'};
if(!defined($min) && !defined($max)) {
# Unconstrained â symmetric range around zero
return "$field_name <- Float(sized => sub { rand($DEFAULT_GENERATOR_RANGE) - $DEFAULT_GENERATOR_RANGE / 2 })";
} elsif(!defined($min)) {
# Only max defined â choose range based on sign of max
if($max == $ZERO_BOUNDARY) {
# max=0: negative numbers only
return "$field_name <- Float(sized => sub { -rand($DEFAULT_GENERATOR_RANGE) })";
} elsif($max > $ZERO_BOUNDARY) {
# Positive max: generate 0 to max
return "$field_name <- Float(sized => sub { rand($max) })";
} else {
# Negative max: generate from (max - range) to max
return "$field_name <- Float(sized => sub { ($max - $DEFAULT_GENERATOR_RANGE) + rand($DEFAULT_GENERATOR_RANGE + $max) })";
}
} elsif(!defined($max)) {
# Only min defined â choose range based on sign of min
if($min == $ZERO_BOUNDARY) {
# min=0: positive numbers only
return "$field_name <- Float(sized => sub { rand($DEFAULT_GENERATOR_RANGE) })";
} elsif($min > $ZERO_BOUNDARY) {
# Positive min: generate min to min + range
return "$field_name <- Float(sized => sub { $min + rand($DEFAULT_GENERATOR_RANGE) })";
} else {
# Negative min: generate from min to min + range
return "$field_name <- Float(sized => sub { $min + rand(-$min + $DEFAULT_GENERATOR_RANGE) })";
}
} else {
# Both min and max defined â validate then generate
my $range = $max - $min;
if($range <= $ZERO_BOUNDARY) {
carp "Invalid range for '$field_name': min=$min, max=$max";
# Return undef rather than emitting a degenerate
# generator that would silently produce wrong values
return;
}
return "$field_name <- Float(sized => sub { $min + rand($range) })";
}
}
# --------------------------------------------------
# String generator
# --------------------------------------------------
if($type eq 'string') {
my $min_len = $spec->{'min'} // 0;
my $max_len = $spec->{'max'} // $DEFAULT_MAX_STRING_LEN;
# If a regex pattern is declared, delegate to
# Data::Random::String::Matches for pattern-aware generation
if(defined($spec->{'matches'})) {
my $pattern = $spec->{'matches'};
# Compile the pattern safely rather than splicing the raw
# string into qr/$pattern/ â the raw form lets a pattern
# containing an unescaped '/' break out of the qr//
# delimiter and inject arbitrary Perl into the generated
# test. regexp_pattern() decomposes the already-compiled
# Regexp object back into pattern text that is guaranteed
# to be a self-contained regex body, safe to re-embed.
my $compiled = ref($pattern) eq 'Regexp' ? $pattern : eval { qr/$pattern/ };
if($@ || !defined($compiled)) {
carp "Invalid matches pattern '$pattern' for field '$field_name': $@";
return "$field_name <- String(length => [$min_len, $max_len])";
}
my ($pat, $mods) = regexp_pattern($compiled);
my $safe_re = "qr{$pat}" . ($mods // '');
if(defined($spec->{'max'})) {
return "$field_name <- Gen { Data::Random::String::Matches->create_random_string({ regex => $safe_re, length => $spec->{'max'} }) }";
} elsif(defined($spec->{'min'})) {
return "$field_name <- Gen { Data::Random::String::Matches->create_random_string({ regex => $safe_re, length => $spec->{'min'} }) }";
} else {
return "$field_name <- Gen { Data::Random::String::Matches->create_random_string({ regex => $safe_re }) }";
}
}
return "$field_name <- String(length => [$min_len, $max_len])";
}
# --------------------------------------------------
# Boolean generator
# --------------------------------------------------
if($type eq 'boolean') {
return "$field_name <- Bool";
}
# --------------------------------------------------
# Arrayref generator
# --------------------------------------------------
if($type eq 'arrayref') {
my $min_size = $spec->{'min'} // 0;
my $max_size = $spec->{'max'} // $DEFAULT_MAX_COLLECTION_SIZE;
return "$field_name <- List(Int, length => [$min_size, $max_size])";
}
# --------------------------------------------------
# Hashref generator
# LectroTest has no built-in Hash generator so we
# use Elements over a pre-built list of hashrefs
# --------------------------------------------------
if($type eq 'hashref') {
my $min_keys = $spec->{'min'} // 0;
my $max_keys = $spec->{'max'} // $DEFAULT_MAX_COLLECTION_SIZE;
return "$field_name <- Elements(map { my \%h; for (1..\$_) { \$h{'key'.\$_} = \$_ }; \\\%h } $min_keys..$max_keys)";
}
# --------------------------------------------------
# Unknown type â fall back to String with a warning
# --------------------------------------------------
carp "Unknown type '$type' for '$field_name' LectroTest generator, using String";
return "$field_name <- String";
}
# --------------------------------------------------
# _is_numeric_transform
lib/App/Test/Generator.pm view on Meta::CPAN
}
if(defined($output_spec->{'max'})) {
my $max = $output_spec->{'max'};
push @properties, {
name => 'max_constraint',
code => "defined(\$result) && looks_like_number(\$result) && \$result <= $max",
};
}
# Heuristic: transforms named 'positive' (case-insensitive)
# imply a non-negative result constraint
if($transform_name =~ /$TRANSFORM_POSITIVE_PATTERN/i) {
push @properties, {
name => 'non_negative',
code => "defined(\$result) && looks_like_number(\$result) && \$result >= 0",
};
}
}
# --------------------------------------------------
# Property 2: Specific value output
# --------------------------------------------------
if(defined($output_spec->{'value'})) {
my $expected = $output_spec->{'value'};
# Numeric refs use == for comparison; scalars use eq
# via perl_quote to produce the correct quoted literal
push @properties, {
name => 'exact_value',
code => ref($expected)
? "\$result == $expected"
: "\$result eq " . perl_quote($expected),
};
}
# --------------------------------------------------
# Property 3: String length constraints
# --------------------------------------------------
if(_is_string_transform($input_spec, $output_spec)) {
if(defined($output_spec->{'min'})) {
push @properties, {
name => 'min_length',
code => "length(\$result) >= $output_spec->{'min'}",
};
}
if(defined($output_spec->{'max'})) {
push @properties, {
name => 'max_length',
code => "length(\$result) <= $output_spec->{'max'}",
};
}
if(defined($output_spec->{'matches'})) {
my $pattern = $output_spec->{'matches'};
# See the matching comment in _schema_to_lectrotest_generator â
# compile first and re-embed via regexp_pattern() rather than
# splicing the raw string into qr/$pattern/, which would let
# an unescaped '/' break out of the delimiter.
my $compiled = ref($pattern) eq 'Regexp' ? $pattern : eval { qr/$pattern/ };
if($@ || !defined($compiled)) {
carp "Invalid matches pattern '$pattern' for transform '$transform_name': $@";
} else {
my ($pat, $mods) = regexp_pattern($compiled);
my $safe_re = "qr{$pat}" . ($mods // '');
push @properties, {
name => 'pattern_match',
code => "\$result =~ $safe_re",
};
}
}
}
# --------------------------------------------------
# Property 4: Type preservation
# --------------------------------------------------
if(_same_type($input_spec, $output_spec)) {
my $type = _get_dominant_type($output_spec);
# Only emit a numeric_type check for numeric types â
# string and other types have no equivalent simple check
if($type eq 'number' || $type eq 'integer' || $type eq 'float') {
push @properties, {
name => 'numeric_type',
code => 'looks_like_number($result)',
};
}
}
# --------------------------------------------------
# Property 5: Definedness
# --------------------------------------------------
# Emit a defined() check for all transforms except those
# whose output type is explicitly 'undef' â those are
# expected to return nothing
unless(($output_spec->{'type'} // '') eq 'undef') {
push @properties, {
name => 'defined',
code => 'defined($result)',
};
}
return @properties;
}
# --------------------------------------------------
# _process_custom_properties
#
# Purpose: Process the 'properties' array from a
# transform definition, resolving each
# entry to either a named builtin property
# (looked up from _get_builtin_properties)
# or a custom property with inline code.
#
# Entry: $properties_spec - arrayref of property
# definitions from the
# schema. Each element
# is either a string
# (builtin name) or a
( run in 1.141 second using v1.01-cache-2.11-cpan-788537b7465 )