Net-Z3950-FOLIO

 view release on metacpan or  search on metacpan

lib/Net/Z3950/FOLIO/Config.pm  view on Meta::CPAN

package Net::Z3950::FOLIO::Config;

use 5.008000;
use strict;
use warnings;

use IO::File;
use Cpanel::JSON::XS qw(decode_json);


# Possible values of $missingAction
sub MISSING_ERROR { 0 }
sub MISSING_TENANT { 1 }
sub MISSING_FILTER { 2 }


sub new {
    my $class = shift();
    my($cfgbase, @extras) = @_;

    my $cfg = _compileConfig($cfgbase, @extras);
    return bless $cfg, $class;
}


sub _compileConfig {
    my($cfgbase, @extras) = @_;

    my $cfg = _compileConfigFile($cfgbase, undef, MISSING_ERROR);

    my $isFirst = 1;
    while (@extras) {
	my $extra = shift @extras;
	my $overlay = _compileConfigFile($cfgbase, $extra, $isFirst ? MISSING_TENANT : MISSING_FILTER);
	$isFirst = 0;
	_mergeConfig($cfg, $overlay);
    }

    my $path = $cfgbase;
    if ($path =~ /\//) {
	$path =~ s/(.*)?\/.*/$1/;
    } else {
	$path = '.';
    }

    my $gqlfile = $cfg->{graphqlQuery}
        or die "$0: no GraphQL query file defined";
    my $fh = new IO::File("<$path/$gqlfile")
	or die "$0: can't open GraphQL query file '$path/$gqlfile': $!";
    { local $/; $cfg->{graphql} = <$fh> };
    $fh->close();

    my $esets = $cfg->{xmlElementSets};
    if ($esets) {
	foreach my $setname (keys %$esets) {
	    my $filename = $esets->{$setname};
	    my $fh = new IO::File("<$path/$filename")
		or Net::Z3950::FOLIO::_throw(1, "can't open XSLT stylesheet file '$path/$filename': $!");
	    { local $/; $esets->{$setname} = <$fh> };
	    $fh->close();
	}
    }

    return $cfg;
}


sub _compileConfigFile {
    my($cfgbase, $cfgsub, $missingAction) = @_;

    my $cfgname = $cfgbase . ($cfgsub ? ".$cfgsub" : '') . '.json';
    my $fh = new IO::File("<$cfgname");
    if (!$fh) {
	if ($! == 2 && $missingAction == MISSING_TENANT) {
	    Net::Z3950::FOLIO::_throw(109, "$cfgsub");
	} elsif ($! == 2 && $missingAction == MISSING_FILTER) {
	    Net::Z3950::FOLIO::_throw(1, "filter not configured: $cfgsub");
	}
	die "$0: can't open config file '$cfgbase.json': $!"
    }

    my $json; { local $/; $json = <$fh> };
    $fh->close();

    my $cfg = decode_json($json);
    _expandVariableReferences($cfg);
    return $cfg;
}


sub _expandVariableReferences {
    my($obj) = @_;

    foreach my $key (sort keys %$obj) {
	$obj->{$key} = _expandSingleVariableReference($key, $obj->{$key});
    }

    return $obj;
}

sub _expandSingleVariableReference {
    my($key, $val) = @_;

    if (ref($val) eq 'HASH') {
	return _expandVariableReferences($val);
    } elsif (ref($val) eq 'ARRAY') {
	return [ map { _expandSingleVariableReference($key, $_) } @$val ];
    } elsif (ref($val) eq 'JSON::PP::Boolean') {
	return $val;
    } elsif (!ref($val)) {
	return _expandScalarVariableReference($key, $val);
    } else {
	use Data::Dumper;
	die "non-hash, non-array, non-boolean, non-scalar configuration key '$key' = ", Dumper($val);
    }
}

sub _expandScalarVariableReference {
    my ($key, $val) = @_;

    my $orig = $val;
    while ($val =~ /(.*?)\$\{(.*?)}(.*)/) {
	my($pre, $inclusion, $post) = ($1, $2, $3);

	my($name, $default);
	if ($inclusion =~ /(.*?)-(.*)/) {
	    $name = $1;
	    $default = $2;
	} else {
	    $name = $inclusion;
	    $default = undef;
	}

	my $env = $ENV{$name} || $default;
	if (!defined $env) {
	    warn "environment variable '$2' not defined for '$key'";
	    $env = '';
	}
	$val = "$pre$env$post";
    }

    return $val;
}


sub _mergeConfig {
    my($base, $overlay) = @_;

    my %complex_keys = (
	okapi => 1,
	login => 1,
	indexMap => 1,
	marcHoldings => 1,
	fieldDefinitions => {
	    holding => 1,
	    circulation => 1,
	},
    );

    foreach my $key (keys (%complex_keys)) {
	if (defined $overlay->{$key}) {
	    if (ref $base->{$key} eq 'HASH') {
		_mergeHash($base->{$key}, $overlay->{$key}, $complex_keys{$key});
	    } else {
		$base->{$key} = $overlay->{$key};
	    }
	}
    }

    foreach my $key (sort keys %$overlay) {
	if (!$complex_keys{$key}) {
	    $base->{$key} = $overlay->{$key};
	}
    }
}


sub _mergeHash {
    my($base, $overlay, $keysOfsubStructures) = @_;

    foreach my $key (sort keys %$overlay) {
	if (ref $keysOfsubStructures eq 'HASH' && $keysOfsubStructures->{$key}) {
	    _mergeHash($base->{$key}, $overlay->{$key}, $keysOfsubStructures->{$key});
	} else {
	    $base->{$key} = $overlay->{$key};
	}
    }
}

=encoding utf-8

=head1 NAME

Net::Z3950::FOLIO::Config - configuration file for the FOLIO Z39.50 gateway

=head1 SYNOPSIS

  {
    "okapi": {
      "url": "https://folio-snapshot-okapi.dev.folio.org",
      "tenant": "${OKAPI_TENANT-indexdata}"
    },
    "login": {
      "username": "diku_admin",
      "password": "${OKAPI_PASSWORD}"
    },
    "indexMap": {
      "1": "author",
      "7": "identifiers/@value/@identifierTypeId=\"8261054f-be78-422d-bd51-4ed9f33c3422\"",
      "4": "title",
      "12": {
        "cql": "hrid",
        "relation": "==",
        "omitSortIndexModifiers": [ "missing", "case" ]
      },
      "21": "subject",
      "1016": "author,title,hrid,subject"
    },
    "queryFilter": "source=marc",
    "graphqlQuery": "instances.graphql-query",
    "chunkSize": 5,
    "fieldDefinitions": {
      "circulation": {
	"availableThru": "permanentLoanType"
      }
    },
    "marcHoldings": {
      "restrictToItem": 0,
      "field": "952",
      "indicators": [" ", " "],
      "holdingsElements": {
        "t": "copyNumber"
      },
      "itemElements": {
        "b": "itemId",
        "k": "_callNumberPrefix",
        "h": "_callNumber",
        "m": "_callNumberSuffix",
        "v": "_volume",
        "e": "_enumeration",
        "y": "_yearCaption",
        "c": "_chronology"
      }
    },
    "postProcessing": {



( run in 2.476 seconds using v1.01-cache-2.11-cpan-39bf76dae61 )