——————package
CallBackery::Model::ConfigJsonSchema;
=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
JSON::Validator;
use
Mojo::Exception;
$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