Mojolicious-Plugin-Fondation-OpenAPI
view release on metacpan or search on metacpan
lib/Mojolicious/Plugin/Fondation/OpenAPI/Command/openapi.pm view on Meta::CPAN
unless ($schema->await($rs_gp->find({ group_id => $admin->id, perm_id => $perm->id }))) {
eval { $schema->await($rs_gp->create({ group_id => $admin->id, perm_id => $perm->id })) };
if ($@) {
warn "[sync-permissions] Failed to assign '$name' to admin: $@\n";
} else {
$assigned++;
}
}
}
warn "[sync-permissions] Assigned $assigned permission(s) to 'admin' group.\n";
}
# ---------------------------------------------------------------------------
# Build client-side validators.js from the OpenAPI spec
# ---------------------------------------------------------------------------
sub _build_validators_js ($self, $spec) {
my $schemas = $spec->{components}{schemas};
my $schemas_js = '';
for my $name (sort keys %$schemas) {
my $schema = $schemas->{$name};
my $props = $schema->{properties} // {};
$schemas_js .= "FondationSchemas['$name'] = {\n";
$schemas_js .= " properties: {\n";
for my $prop (sort keys %$props) {
my $def = $props->{$prop};
my @rules;
push @rules, "required: true"
if grep { $_ eq $prop } @{ $schema->{required} // [] };
push @rules, "type: '" . $def->{type} . "'"
if $def->{type};
push @rules, "minLength: " . $def->{minLength}
if $def->{minLength};
push @rules, "maxLength: " . $def->{maxLength}
if $def->{maxLength};
push @rules, "format: '" . $def->{format} . "'"
if $def->{format};
push @rules, "nullable: true"
if $def->{nullable};
push @rules, "readOnly: true"
if $def->{readOnly};
push @rules, "writeOnly: true"
if $def->{writeOnly};
$schemas_js .= " '$prop': { " . join(', ', @rules) . " },\n";
}
$schemas_js .= " }\n";
$schemas_js .= "};\n\n";
}
my $validators = <<'VALIDATORS';
var FondationSchemas = {};
SCHEMAS_PLACEHOLDER
window.FondationValidators = {
validate: function(schemaName, data) {
var schema = FondationSchemas[schemaName];
if (!schema) return { valid: false, errors: ['Schema not found: ' + schemaName] };
var errors = [];
for (var prop in schema.properties) {
var rules = schema.properties[prop];
var val = data[prop];
// Skip readOnly fields (server-managed, not in forms)
if (rules.readOnly) continue;
// Required check (skip readOnly -- server-managed)
if (rules.required && !rules.readOnly) {
if (val === undefined || val === null || val === '') {
errors.push(prop + ' is required');
continue;
}
}
// Skip further checks if value is empty and not required
if (val === undefined || val === null || val === '') {
if (rules.nullable) continue;
continue;
}
// Type check
if (rules.type === 'integer') {
var n = Number(val);
if (isNaN(n) || !Number.isInteger(n)) {
errors.push(prop + ' must be an integer');
continue;
}
}
// Format check
if (rules.format === 'email') {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)) {
errors.push(prop + ' must be a valid email');
}
}
// Min length
if (rules.minLength && typeof val === 'string' && val.length < rules.minLength) {
errors.push(prop + ' must be at least ' + rules.minLength + ' characters');
}
// Max length
if (rules.maxLength && typeof val === 'string' && val.length > rules.maxLength) {
errors.push(prop + ' must be at most ' + rules.maxLength + ' characters');
}
// Enum check
if (rules.enum) {
var match = false;
for (var i = 0; i < rules.enum.length; i++) {
if (rules.type === 'integer') {
if (Number(val) === rules.enum[i]) { match = true; break; }
} else {
if (val === String(rules.enum[i])) { match = true; break; }
lib/Mojolicious/Plugin/Fondation/OpenAPI/Command/openapi.pm view on Meta::CPAN
=item * Added back into C<create> and C<update> projection properties
=back
This means GET responses never contain writeOnly fields and the
OpenAPI validator does not expect them.
=head2 Config override
Any column property can be overridden via the plugin configuration
without modifying DBIx classes:
'Fondation::OpenAPI' => {
backend => 'main',
schemas => {
User => {
columns => {
name => {
maxLength => 100, # override DBIx size
},
password => {
writeOnly => 1,
create => { required => 1 },
update => { required => 0 },
},
},
},
},
},
Config keys follow the same flat + contextual structure as
C<extra-E<gt>{openapi}> and take the highest priority in the cascade:
1. Structure DBIx (implicit)
2. extra->{openapi} flat keys (Result class)
3. Config flat keys (myapp.conf)
4. extra->{openapi} contextual (Result class)
5. Config contextual (myapp.conf)
=head1 SUBCOMMANDS
=head2 generate
myapp.pl openapi generate
myapp.pl openapi generate -y
myapp.pl openapi generate --output custom.json
Iterates all DBIx sources (monikers), builds the API Base for each,
applies contextual projections, and writes two files:
=over
=item C<share/openapi.json>
OpenAPI 3.0.3 specification with schemas and CRUD paths. Loaded at
runtime by L<Mojolicious::Plugin::OpenAPI> for request validation.
=item C<public/js/validators.js>
Client-side validation (C<FondationValidators.validate()>) consumed
by L<Fondation::Asset> bundles.
=back
Options:
--output FILE Output path relative to $app->home (default: share/openapi.json)
-y Overwrite without confirmation prompt
=head1 CRUD PATHS
Each source generates five endpoints with C<x-mojo-to> routing and
automatic C<x-auth> permission annotations:
GET /{moniker} -> {Moniker}#list x-auth: {moniker_lc}_list
POST /{moniker} -> {Moniker}#create x-auth: {moniker_lc}_create
GET /{moniker}/{id} -> {Moniker}#read x-auth: {moniker_lc}_read
PUT /{moniker}/{id} -> {Moniker}#update x-auth: {moniker_lc}_update
DELETE /{moniker}/{id} -> {Moniker}#delete x-auth: {moniker_lc}_delete
The C<x-auth> default can be overridden via the plugin config
(C<schemas.{Source}.x_auth.{operation}>). See L<Mojolicious::Plugin::Fondation::OpenAPI>
for details. Enforcement is handled at runtime by
L<Mojolicious::Plugin::Fondation::OpenAPI::Security>.
=head1 SEE ALSO
L<Mojolicious::Plugin::Fondation::OpenAPI>,
L<Fondation::Model::DBIx::Async>,
L<Mojolicious::Plugin::OpenAPI>
=head1 AUTHOR
Daniel Brosseau <dab@cpan.org>
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Daniel Brosseau.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut
( run in 0.925 second using v1.01-cache-2.11-cpan-f4a522933cf )