The Perl and Raku Conference 2025: Greenville, South Carolina - June 27-29 Learn more

=head1 NAME
CallBackery::Model::ConfigJsonSchema - get parse configuration file for CallBackery
=head1 SYNOPSIS
use CallBackery::Model::ConfigYAML;
my $cfg = CallBackery::Config->new(file=>$file);
my $hash_ref = $cfg->cfgHash();
my $pod = $cfg->pod();
=head1 DESCRIPTION
CallBackery gets much of its configuration from this config file.
=cut
use Mojo::Base 'CallBackery::Config', -signatures;
use Mojo::Util qw(dumper);
use Mojo::Loader qw(data_section);
use YAML::XS qw(LoadFile Load);
use Mojo::JSON qw(true false);
$YAML::XS::Boolean = 'JSON::PP';
=head2 cfgHash
a hash containing the data from the config file
=cut
my $walker;
$walker = sub ($data, $cb, $path='' ) {
if (ref $data eq 'HASH') {
for my $key (keys %$data) {
$data->{$key} = $walker->($data->{$key},$cb,$path.'/'.$key);
}
}
elsif (ref $data eq 'ARRAY') {
my $i = 0;
for my $item (@$data) {
$item = $walker->($item, $cb, $path.'/'.$i);
$i++;
}
}
else {
return $cb->($path,$data);
}
return $data;
};
=head2 cfgHash
a hash containing the data from the config file. If the environment
variable CM_CB_OVERRIDE_... is set, the value from the config file is
overridden with the value from the environment. All config key characters
outside of [0-9a-zA-Z] are replaced with _. Note that you can only
override settings that exist in the config file, you can not create new settings.
Example config file:
BACKEND:
cfg-db: 'dbi:SQLite:dbname=/opt/running/cb.db'
LIST:
- hello
- world
Example environment override:
export CB_CFG_OVERRIDE_BACKEND_cfg_db='dbi:SQLite:dbname=/tmp/cb.db'
export CB_CFG_OVERRIDE_LIST_0='goodbye'
=cut
has cfgHash => sub ($self) {
my $cfgRaw = eval { LoadFile($self->file) };
if ($@) {
Mojo::Exception->throw("Loading ".$self->file.": $@");
}
my $cfg = $walker->($cfgRaw, sub ($path,$data) {
my $env = 'CB_CFG_OVERRIDE'.$path;
$env =~ s/[^0-9a-zA-Z]/_/g;
if (exists $ENV{$env} and my $override = $ENV{$env}) {
$self->log->debug("overriding cfg $path ($data) with ENV \$$env ($override)");
return $override;
}
return $data;
});
if (my @errors = $self->validator->validate($cfg)){
Mojo::Exception->throw("Validating ".$self->file.":\n".join "\n",@errors);
}
return $cfg;
};
=head2 pod
returns a pod documenting the config file
=cut
has pod => sub ($self) {
return "pod output for YAML is not supported";
};
has grammar => sub ($self) {
die "The grammar is now described in the 'schema' attribute. Please upgrade!";
};
has schema => sub ($self) {
my $yaml = data_section __PACKAGE__, 'config-schema.yaml';
my $schema = eval {
Load($yaml);
};
if ($@) {
Mojo::Exception->throw("$@");
}
return $schema;
};
sub validator ($self) {
my $validator = JSON::Validator->new();
$validator->schema($self->schema);
return $validator;
}
=head2 postProcessCfg
Post process the configuration data into a format that is easily used
by the application.
=cut
sub postProcessCfg ($self) {
my $cfg = $self->cfgHash;
my $schema = $self->schema;
my @items;
my %pluginMap;
my @fullPluginList;
my @pluginList;
for my $prop (keys %{$schema->{properties}{BACKEND}{properties}}) {
my $default = $schema->{properties}{BACKEND}{properties}{$prop}{default};
$cfg->{BACKEND}{$prop} //= $default if $default;
}
for my $prop (keys %{$schema->{properties}{FRONTEND}{properties}}) {
my $default = $schema->{properties}{FRONTEND}{properties}{$prop}{default};
$cfg->{FRONTEND}{$prop} //= $default if $default;
}
for my $item (@{$cfg->{PLUGIN}}) {
my ($name) = keys %$item;
push @fullPluginList, $name;
push @pluginList, $name
if not $item->{$name}{unlisted};
Mojo::Exception->throw("Duplicated plugin instance $name")
if exists $pluginMap{$name};
my $obj = eval {
$self->loadAndNewPlugin($item->{$name}{module});
};
if ($@){
warn "Failed to load Plugin $name: $@";
next;
}
$obj->config($item->{$name});
$obj->name($name);
$obj->app($self->app);
$pluginMap{$name} = $obj;
push @items, {
type => 'object',
properties => {
$name => $obj->schema,
},
required => [ $name ],
additionalProperties => false,
};
}
$schema->{properties}{PLUGIN}{items} = {
anyOf => \@items
};
$schema->{properties}{FRONTEND}{properties}{$_.'_popup'}
{properties}{plugin}{enum} = \@fullPluginList
for qw(registration passwordreset);
# warn YAML::XS::Dump $schema;
# second pass with data structures from plugins integrated
if (my @errors = $self->validator->validate($cfg)){
Mojo::Exception->throw(join "\n",@errors);
}
$cfg->{PLUGIN} = {
prototype => \%pluginMap,
list => \@pluginList
};
for (qw(registration passwordreset)) {
if (my $popup = $cfg->{FRONTEND}{$_.'_popup'}){
my $plugin = delete $popup->{plugin};
my $obj = $pluginMap{$plugin};
$cfg->{FRONTEND}{$_.'_popup'} = {
popupTitle => $obj->config->{'tab-name'},
name => $plugin,
%$popup,
};
}
}
#warn YAML::XS::Dump($cfg->{FRONTEND})
for my $name (@fullPluginList) {
$pluginMap{$name}->massageConfig($cfg);
}
$cfg->{FRONTEND}{TRANSLATIONS} = $self->getTranslations();
return $cfg;
}
1;
__DATA__
@@ config-schema.yaml
definitions:
plugin:
type: object
additionalProperties: false
required:
- plugin
properties:
plugin:
type: string
set:
type: object
type: object
additionalProperties: false
required:
- BACKEND
- FRONTEND
- PLUGIN
properties:
BACKEND:
type: object
additionalProperties: false
required:
- cfg_db
properties:
log_file:
type: string
cfg_db:
type: string
sesame_user:
type: string
sesame_pass:
type: string
FRONTEND:
type: object
additionalProperties: false
properties:
logo:
type: string
format: uri
description: url for the logo brand the login screen
company_name:
type: string
description: who created the app
company_url:
type: string
format: uri
description: link to the company homepage
max_width:
type: integer
description: maximum content width
company_support:
type: string
format: uri
description: company support eMail
logo_small:
description: for the small logo brand the UI
type: string
format: uri
logo_noscale:
type: boolean
description: don't scale the login window logo
default: false
spinner:
type: string
format: uri
description: url for the busy animation spinner gif
title:
type: string
description: title string for the application
initial_plugin:
type: string
description: which tab should be active upon login ?
hide_password:
type: boolean
description: hide password field on login screen
hide_password_icon:
type: boolean
description: hide password dialog icon
default: false
hide_release:
type: boolean
description: hide release string on login screen
default: false
hide_company:
type: boolean
description: hide company string on login screen
default: false
registration_popup:
$ref: "#/definitions/plugin"
passwordreset_popup:
$ref: "#/definitions/plugin"
PLUGIN:
type: array
items:
type: object
maxProperties: 1
minProperties: 1
patternProperty:
^\S+$:
type: object
required:
- module
properties:
module:
type: string
unlisted:
description: do not add this plugin to the plugin list.
type: boolean
__END__
=head1 COPYRIGHT
Copyright (c) 2020 by OETIKER+PARTNER AG. All rights reserved.
=head1 AUTHOR
S<Tobias Oetiker E<lt>tobi@oetiker.chE<gt>>
=head1 HISTORY
2020-02-18 to 1.0 first version
2020-11-20 fz 1.1 call postProcessCfg from CallBackery.pm
=cut
# Emacs Configuration
#
# Local Variables:
# mode: cperl
# eval: (cperl-set-style "PerlStyle")
# mode: flyspell
# mode: flyspell-prog
# End:
#
# vi: sw=4 et