Fix build

Fix build

import module namespace http-client = "http://zorba.io/modules/http-client";
import module namespace request = "http://www.28msec.com/modules/http-request";
import module namespace response = "http://www.28msec.com/modules/http-response";

declare %an:nondeterministic function local:test-concepts($expected as integer, $params as string) as atomic
{
    let $elements as object* := parse-json(http-client:get("http://" || request:server-name() || ":" || request:server-port() || "/v1/_queries/public/api/report-elements.jq?_method=POST" || $params).body.content).ReportElements[]
    let $actual as integer := count($elements)
    let $duplicates := for $element in $elements where $element.CIK instance of array return $element
    return
        if (exists($duplicates)) then
            "false [CIK " || $duplicates[1].CIK || " is not unique]"
        else if ($actual eq $expected) then
            true
        else
            "false [Actual="||$actual||", Expected="||$expected ||"]"
};

declare %an:sequential function local:check($o as object) as object
{
    if (not(every $k in (keys($o) ! $o.$$) satisfies ($k instance of boolean and $k)))
    then {
            response:status-code(500);
            $o
    } else
            $o
};

local:check({
    all: local:test-concepts(3, "&cik=4962&name=us-gaap:Assets"),
    onlyNames: local:test-concepts(16143, "&tag=DOW30&fiscalYear=2012&fiscalPeriod=FY&onlyNames=true"),
    disclosures: local:test-concepts(1855, "&ticker=ko&fiscalYear=2012&fiscalPeriod=FY&report=Disclosure&concept=disc:ConsolidationVariableInterestEntityPolicy")
})

import module namespace http-client = "http://zorba.io/modules/http-client";
import module namespace request = "http://www.28msec.com/modules/http-request";
import module namespace response = "http://www.28msec.com/modules/http-response";

declare %an:nondeterministic function local:test-concepts($expected as integer, $params as string) as atomic
{
    let $elements as object* := parse-json(http-client:get("http://" || request:server-name() || ":" || request:server-port() || "/v1/_queries/public/api/report-elements.jq?_method=POST" || $params).body.content).ReportElements[]
    let $actual as integer := count($elements)
    let $duplicate := for $element in $elements where $element.CIK instance of array return $element
    return
        if (exists($duplicate)) then
            "false [CIK " || $duplicates[1].CIK || " is not unique]"
        else if ($actual eq $expected) then
            true
        else
            "false [Actual="||$actual||", Expected="||$expected ||"]"
};

declare %an:sequential function local:check($o as object) as object
{
    if (not(every $k in (keys($o) ! $o.$$) satisfies ($k instance of boolean and $k)))
    then {
            response:status-code(500);
            $o
    } else
            $o
};

local:check({
    all: local:test-concepts(3, "&cik=4962&name=us-gaap:Assets"),
    onlyNames: local:test-concepts(16143, "&tag=DOW30&fiscalYear=2012&fiscalPeriod=FY&onlyNames=true"),
    disclosures: local:test-concepts(1855, "&ticker=ko&fiscalYear=2012&fiscalPeriod=FY&report=Disclosure&concept=disc:ConsolidationVariableInterestEntityPolicy")
})


Check token in facttable-for-component.jq

jsoniq version "1.0";

import module namespace components = "http://28.io/modules/xbrl/components";
import module namespace filings = "http://28.io/modules/xbrl/profiles/sec/filings";
import module namespace sec = "http://28.io/modules/xbrl/profiles/sec/core";
import module namespace entities = "http://28.io/modules/xbrl/entities";
import module namespace hypercubes = "http://28.io/modules/xbrl/hypercubes";
import module namespace conversion = "http://28.io/modules/xbrl/conversion";
import module namespace networks = "http://28.io/modules/xbrl/networks";
import module namespace concept-maps = "http://28.io/modules/xbrl/concept-maps";
import module namespace rules = "http://28.io/modules/xbrl/rules";
import module namespace labels = "http://28.io/modules/xbrl/labels";

import module namespace multiplexer = "http://28.io/modules/xbrl/profiles/multiplexer";
import module namespace fiscal-core = "http://28.io/modules/xbrl/profiles/sec/fiscal/core";

import module namespace config = "http://apps.28.io/config";
import module namespace session = "http://apps.28.io/session";
import module namespace api = "http://apps.28.io/api";

declare option rest:response "first-item";

(: Query parameters :)
declare  %rest:env                              variable $request-uri        as string  external;
declare  %rest:case-insensitive                 variable $format             as string? external;
declare  %rest:case-insensitive %rest:distinct  variable $cik                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $edinetcode         as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $tag                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $ticker             as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $sic                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $fiscalYear         as string* external := "ALL";
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriod       as string* external := "ALL";
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriodType   as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $filingKind         as string* external := ();
declare  %rest:case-insensitive %rest:distinct  variable $eid                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $aid                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $networkIdentifier  as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $cid                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $concept            as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $reportElement      as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $disclosure         as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $label              as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $rollup             as string* external;
declare  %rest:case-insensitive                 variable $map                as string? external;
declare  %rest:case-insensitive                 variable $validate           as boolean external := false;
declare  %rest:case-insensitive                 variable $labels             as boolean external := true;
declare  %rest:case-insensitive                 variable $merge              as boolean external := true;
declare  %rest:case-insensitive                 variable $additional-rules   as string? external;
declare  %rest:case-insensitive                 variable $profile-name       as string  external := $config:profile-name;
declare  %rest:case-insensitive %rest:distinct  variable $role               as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $hypercube          as string* external;
declare  %rest:case-insensitive                 variable $language           as string  external := "en-US";
declare  %rest:case-insensitive                 variable $count              as boolean external := false;
declare  %rest:case-insensitive                 variable $override           as boolean external := false;

(: Post-processing :)
let $format as string? := api:preprocess-format($format, $request-uri)
let $fiscalYear as integer* := api:preprocess-fiscal-years($fiscalYear)
let $fiscalPeriod as string* := api:preprocess-fiscal-periods($fiscalPeriod)
let $tag as string* := api:preprocess-tags($tag)
let $reportElement := ($reportElement, $concept)
let $networkIdentifier := distinct-values(($networkIdentifier, $role))

let $cik as string* :=
    switch($profile-name)
    case "sec" return $cik
    case "japan" return $edinetcode
    default return ()

(: Entity resolution :)
let $entities as object* := multiplexer:entities(
  $profile-name,
  $eid,
  $cik,
  $tag,
  $ticker,
  $sic,
  (),
  (),
  (),
  ())

let $archives as object* := multiplexer:filings(
  $profile-name,
  $entities,
  $fiscalPeriod,
  $fiscalYear,
  $filingKind,
  $aid)

let $archives-not-found as boolean :=
  exists(($entities, $fiscalPeriod, $fiscalYear, $filingKind, $aid)) and empty($archives)

let $entities as object* :=
    ($entities[entities:eid($$) = $archives.Entity],
    let $not-found := $archives.Entity[not entities:eid($entities) = $$]
    where exists($not-found)
    return entities:entities($not-found))

let $entities-not-found as boolean :=
  exists(($eid, $cik, $tag, $ticker, $sic)) and empty($entities)

let $component as object? :=
    multiplexer:component(
      $profile-name,
      $archives,
      $cid,
      $reportElement,
      $disclosure,
      $networkIdentifier,
      $label[$profile-name ne "sec"],
      $label[$profile-name eq "sec"],
      $hypercube,
      $merge
    )

let $component-not-found as boolean :=
  exists(($archives, $cid, $reportElement, $disclosure, $networkIdentifier, $label)) and empty($component)

let $selected-hypercube as string? := multiplexer:hypercube(
    $component,
    $hypercube)

let $hypercube-not-found as boolean := exists($hypercube) and empty($selected-hypercube)

let $cid as string? := components:cid($component)
let $rules as object* := if(exists($additional-rules)) then rules:rules($additional-rules) else ()

let $filter-override as object? :=
    if($profile-name eq "sec")
    then fiscal-core:filter-override(
        $entities,
        $fiscalYear,
        $fiscalPeriod,
        $fiscalPeriodType,
        $aid)
    else components:filter-override($entities, $aid)

(: Fact resolution :)
let $facts :=
    switch(true)
    case (exists($rollup) and $profile-name eq "sec")
         return
             let $calc-network := networks:networks-for-components-and-short-names($component, $networks:CALCULATION_NETWORK)
             let $hc := hypercubes:hypercubes-for-components($component)[][$$.Name = $selected-hypercube]
             let $hc := hypercubes:modify-hypercube($hc, {
                 "sec:FiscalYear" : { Type: "integer", Default: null },
                 "sec:FiscalPeriod" : { Type: "string", Default: null },
                 "sec:FiscalPeriodType" : { Type: "string", Default: null }
             })
             let $options as object? := if(exists($rules)) then { Rules: [ $rules ] } else ()
             let $p := hypercubes:populate-networks-with-facts($calc-network, $hc, $archives, $options)
             let $map := concept-maps:concept-maps($map)
             let $concepts :=
                if (not $map instance of null)
                then
                    for $d in $rollup[]
                    return
                        descendant-objects($p)[$$.Name = ($map.Trees[])[$$.Name eq $d].To[].Name][1].To[].Name
                else
                    for $d in $rollup[]
                    return ($d, keys(descendant-objects($p)[$$.Name eq $d].To))
             return sec:facts-for-archives-and-concepts($archives, $concepts, { Hypercube: $hc })
    case exists($selected-hypercube)
          return components:facts(
              $component,
              $selected-hypercube,
              {|
                  { FilterOverride: $filter-override }[$override],
                  { Validate: $validate },
                  { Rules : [ $rules ] }[exists($rules)]
              |}
          )
    default return ()

let $facts :=
    if(not $labels)
    then $facts
    else
        (: if labels are requested by the labels=true parameter then also add labels for concepts :)
        let $concepts as object* := $component.Concepts[]
        for $fact in $facts
        let $labels := labels:labels-for-facts(
          $fact,
          $labels:STANDARD_LABEL_ROLE,
          $language,
          $concepts,
          $entities,
          ()
        )
        return
            {|
                trim($fact, "Labels"),
                { Labels : $labels }
            |}
let $facts := api:normalize-facts($facts)

let $result :=
  switch($profile-name)
  case "sec" return {
    CIK : entities:eid($entities),
    EntityRegistrantName : $entities.Profiles.SEC.CompanyName,
    TableName : $selected-hypercube,
    Label : $component.Label,
    AccessionNumber : $component.Archive,
    FormType : $archives.Profiles.SEC.FormType,
    FiscalPeriod : $archives.Profiles.SEC.Fiscal.DocumentFiscalPeriodFocus,
    FiscalYear : $archives.Profiles.SEC.Fiscal.DocumentFiscalYearFocus,
    AcceptanceDatetime : ($archives ! filings:acceptance-dateTimes($$)),
    NetworkIdentifier: $component.Role,
    Disclosure : $component.Profiles.SEC.Disclosure,
    FactTable : [ if($count) then () else $facts ]
  } default return {
    Archive : $component.Archive,
    Role: $component.Role,
    TableName : $selected-hypercube,
    Label : $component.Label,
    FactTable : [ if($count) then () else $facts ]
  }

let $comment := {
    NumFacts : count($facts),
    TotalNumFacts: session:num-facts(),
    TotalNumArchives: session:num-archives(),
    TotalNumEntities: session:num-entities()
}

let $serializers := {
    to-xml : function($res as object) as node() {
        switch($profile-name)
        case "sec" return <FactTable entityRegistrantName="{$res.EntityRegistrantName}"
            cik="{$res.CIK}"
            tableName="{$res.TableName}"
            label="{$res.Label}"
            accessionNumber="{$res.AccessionNumber}"
            networkIdentifier="{$res.NetworkIdentifier}"
            formType="{$res.FormType}"
            fiscalPeriod="{$res.FiscalPeriod}"
            fiscalYear="{$res.FiscalYear}"
            acceptanceDatetime="{$res.AcceptanceDatetime}"
            disclosure="{$res.Disclosure}"
            >{
            conversion:facts-to-xml($res.FactTable[], { Caller: "Component" })
        }</FactTable>
        default return <FactTable
            tableName="{$res.TableName}"
            label="{$res.Label}"
            archive="{$res.Archive}"
            role="{$res.Role}"
            >{
            conversion:facts-to-xml($res.FactTable[], { Caller: "Component" })
        }</FactTable>
    },
    to-csv : function($res as object) as string {
        string-join(conversion:facts-to-csv($res.FactTable[], { Caller: "Component"}))
    }
}
let $results as item* :=
    api:serialize($result, $comment, $serializers, $format, "facttable-" || $cid)
return switch(true)
       case $entities-not-found return api:not-found("entity")
       case $archives-not-found return api:not-found("archive")
       case $component-not-found return api:not-found("component")
       case $hypercube-not-found return api:not-found("hypercube")
       default return api:check-and-return-results($token, $results, $format)

jsoniq version "1.0";

import module namespace components = "http://28.io/modules/xbrl/components";
import module namespace filings = "http://28.io/modules/xbrl/profiles/sec/filings";
import module namespace sec = "http://28.io/modules/xbrl/profiles/sec/core";
import module namespace entities = "http://28.io/modules/xbrl/entities";
import module namespace hypercubes = "http://28.io/modules/xbrl/hypercubes";
import module namespace conversion = "http://28.io/modules/xbrl/conversion";
import module namespace networks = "http://28.io/modules/xbrl/networks";
import module namespace concept-maps = "http://28.io/modules/xbrl/concept-maps";
import module namespace rules = "http://28.io/modules/xbrl/rules";
import module namespace labels = "http://28.io/modules/xbrl/labels";

import module namespace multiplexer = "http://28.io/modules/xbrl/profiles/multiplexer";
import module namespace fiscal-core = "http://28.io/modules/xbrl/profiles/sec/fiscal/core";

import module namespace config = "http://apps.28.io/config";
import module namespace session = "http://apps.28.io/session";
import module namespace api = "http://apps.28.io/api";

declare option rest:response "first-item";

(: Query parameters :)
declare  %rest:env                              variable $request-uri        as string  external;
declare  %rest:case-insensitive                 variable $format             as string? external;
declare  %rest:case-insensitive %rest:distinct  variable $cik                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $edinetcode         as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $tag                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $ticker             as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $sic                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $fiscalYear         as string* external := "ALL";
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriod       as string* external := "ALL";
declare  %rest:case-insensitive %rest:distinct  variable $fiscalPeriodType   as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $filingKind         as string* external := ();
declare  %rest:case-insensitive %rest:distinct  variable $eid                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $aid                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $networkIdentifier  as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $cid                as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $concept            as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $reportElement      as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $disclosure         as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $label              as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $rollup             as string* external;
declare  %rest:case-insensitive                 variable $map                as string? external;
declare  %rest:case-insensitive                 variable $validate           as boolean external := false;
declare  %rest:case-insensitive                 variable $labels             as boolean external := true;
declare  %rest:case-insensitive                 variable $merge              as boolean external := true;
declare  %rest:case-insensitive                 variable $additional-rules   as string? external;
declare  %rest:case-insensitive                 variable $profile-name       as string  external := $config:profile-name;
declare  %rest:case-insensitive %rest:distinct  variable $role               as string* external;
declare  %rest:case-insensitive %rest:distinct  variable $hypercube          as string* external;
declare  %rest:case-insensitive                 variable $language           as string  external := "en-US";
declare  %rest:case-insensitive                 variable $count              as boolean external := false;
declare  %rest:case-insensitive                 variable $override           as boolean external := false;

(: Post-processing :)
let $format as string? := api:preprocess-format($format, $request-uri)
let $fiscalYear as integer* := api:preprocess-fiscal-years($fiscalYear)
let $fiscalPeriod as string* := api:preprocess-fiscal-periods($fiscalPeriod)
let $tag as string* := api:preprocess-tags($tag)
let $reportElement := ($reportElement, $concept)
let $networkIdentifier := distinct-values(($networkIdentifier, $role))

let $cik as string* :=
    switch($profile-name)
    case "sec" return $cik
    case "japan" return $edinetcode
    default return ()

(: Entity resolution :)
let $entities as object* := multiplexer:entities(
  $profile-name,
  $eid,
  $cik,
  $tag,
  $ticker,
  $sic,
  (),
  (),
  (),
  ())

let $archives as object* := multiplexer:filings(
  $profile-name,
  $entities,
  $fiscalPeriod,
  $fiscalYear,
  $filingKind,
  $aid)

let $archives-not-found as boolean :=
  exists(($entities, $fiscalPeriod, $fiscalYear, $filingKind, $aid)) and empty($archives)

let $entities as object* :=
    ($entities[entities:eid($$) = $archives.Entity],
    let $not-found := $archives.Entity[not entities:eid($entities) = $$]
    where exists($not-found)
    return entities:entities($not-found))

let $entities-not-found as boolean :=
  exists(($eid, $cik, $tag, $ticker, $sic)) and empty($entities)

let $component as object? :=
    multiplexer:component(
      $profile-name,
      $archives,
      $cid,
      $reportElement,
      $disclosure,
      $networkIdentifier,
      $label[$profile-name ne "sec"],
      $label[$profile-name eq "sec"],
      $hypercube,
      $merge
    )

let $component-not-found as boolean :=
  exists(($archives, $cid, $reportElement, $disclosure, $networkIdentifier, $label)) and empty($component)

let $selected-hypercube as string? := multiplexer:hypercube(
    $component,
    $hypercube)

let $hypercube-not-found as boolean := exists($hypercube) and empty($selected-hypercube)

let $cid as string? := components:cid($component)
let $rules as object* := if(exists($additional-rules)) then rules:rules($additional-rules) else ()

let $filter-override as object? :=
    if($profile-name eq "sec")
    then fiscal-core:filter-override(
        $entities,
        $fiscalYear,
        $fiscalPeriod,
        $fiscalPeriodType,
        $aid)
    else components:filter-override($entities, $aid)

(: Fact resolution :)
let $facts :=
    switch(true)
    case (exists($rollup) and $profile-name eq "sec")
         return
             let $calc-network := networks:networks-for-components-and-short-names($component, $networks:CALCULATION_NETWORK)
             let $hc := hypercubes:hypercubes-for-components($component)[][$$.Name = $selected-hypercube]
             let $hc := hypercubes:modify-hypercube($hc, {
                 "sec:FiscalYear" : { Type: "integer", Default: null },
                 "sec:FiscalPeriod" : { Type: "string", Default: null },
                 "sec:FiscalPeriodType" : { Type: "string", Default: null }
             })
             let $options as object? := if(exists($rules)) then { Rules: [ $rules ] } else ()
             let $p := hypercubes:populate-networks-with-facts($calc-network, $hc, $archives, $options)
             let $map := concept-maps:concept-maps($map)
             let $concepts :=
                if (not $map instance of null)
                then
                    for $d in $rollup[]
                    return
                        descendant-objects($p)[$$.Name = ($map.Trees[])[$$.Name eq $d].To[].Name][1].To[].Name
                else
                    for $d in $rollup[]
                    return ($d, keys(descendant-objects($p)[$$.Name eq $d].To))
             return sec:facts-for-archives-and-concepts($archives, $concepts, { Hypercube: $hc })
    case exists($selected-hypercube)
          return components:facts(
              $component,
              $selected-hypercube,
              {|
                  { FilterOverride: $filter-override }[$override],
                  { Validate: $validate },
                  { Rules : [ $rules ] }[exists($rules)]
              |}
          )
    default return ()

let $facts :=
    if(not $labels)
    then $facts
    else
        (: if labels are requested by the labels=true parameter then also add labels for concepts :)
        let $concepts as object* := $component.Concepts[]
        for $fact in $facts
        let $labels := labels:labels-for-facts(
          $fact,
          $labels:STANDARD_LABEL_ROLE,
          $language,
          $concepts,
          $entities,
          ()
        )
        return
            {|
                trim($fact, "Labels"),
                { Labels : $labels }
            |}
let $facts := api:normalize-facts($facts)

let $result :=
  switch($profile-name)
  case "sec" return {
    CIK : entities:eid($entities),
    EntityRegistrantName : $entities.Profiles.SEC.CompanyName,
    TableName : $selected-hypercube,
    Label : $component.Label,
    AccessionNumber : $component.Archive,
    FormType : $archives.Profiles.SEC.FormType,
    FiscalPeriod : $archives.Profiles.SEC.Fiscal.DocumentFiscalPeriodFocus,
    FiscalYear : $archives.Profiles.SEC.Fiscal.DocumentFiscalYearFocus,
    AcceptanceDatetime : ($archives ! filings:acceptance-dateTimes($$)),
    NetworkIdentifier: $component.Role,
    Disclosure : $component.Profiles.SEC.Disclosure,
    FactTable : [ if($count) then () else $facts ]
  } default return {
    Archive : $component.Archive,
    Role: $component.Role,
    TableName : $selected-hypercube,
    Label : $component.Label,
    FactTable : [ if($count) then () else $facts ]
  }

let $comment := {
    NumFacts : count($facts),
    TotalNumFacts: session:num-facts(),
    TotalNumArchives: session:num-archives(),
    TotalNumEntities: session:num-entities()
}

let $serializers := {
    to-xml : function($res as object) as node() {
        switch($profile-name)
        case "sec" return <FactTable entityRegistrantName="{$res.EntityRegistrantName}"
            cik="{$res.CIK}"
            tableName="{$res.TableName}"
            label="{$res.Label}"
            accessionNumber="{$res.AccessionNumber}"
            networkIdentifier="{$res.NetworkIdentifier}"
            formType="{$res.FormType}"
            fiscalPeriod="{$res.FiscalPeriod}"
            fiscalYear="{$res.FiscalYear}"
            acceptanceDatetime="{$res.AcceptanceDatetime}"
            disclosure="{$res.Disclosure}"
            >{
            conversion:facts-to-xml($res.FactTable[], { Caller: "Component" })
        }</FactTable>
        default return <FactTable
            tableName="{$res.TableName}"
            label="{$res.Label}"
            archive="{$res.Archive}"
            role="{$res.Role}"
            >{
            conversion:facts-to-xml($res.FactTable[], { Caller: "Component" })
        }</FactTable>
    },
    to-csv : function($res as object) as string {
        string-join(conversion:facts-to-csv($res.FactTable[], { Caller: "Component"}))
    }
}
return switch(true)
       case $entities-not-found return api:not-found("entity")
       case $archives-not-found return api:not-found("archive")
       case $component-not-found return api:not-found("component")
       case $hypercube-not-found return api:not-found("hypercube")
       default return api:serialize($result, $comment, $serializers, $format, "facttable-" || $cid)


Misc fixes.

Misc fixes.

jsoniq version "1.0";

(:
 : Copyright 2006-2013 The FLWOR Foundation.
 :
 : Licensed under the Apache License, Version 2.0 (the "License");
 : you may not use this file except in compliance with the License.
 : You may obtain a copy of the License at
 :
 : http://www.apache.org/licenses/LICENSE-2.0
 :
 : Unless required by applicable law or agreed to in writing, software
 : distributed under the License is distributed on an "AS IS" BASIS,
 : WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 : See the License for the specific language governing permissions and
 : limitations under the License.
 :)

(:~
 : JSound simple validator
 : This is a JSONiq implemenation of the JSound (the schema for JSON) validator.
 :
 : @author Cezar Andrei
 :)

module namespace jsv = "http://jsound.io/modules/validate";

import module namespace jn = "http://jsoniq.org/functions";
import module namespace map = "http://jsound.io/modules/validate/map";

declare namespace an = "http://zorba.io/annotations";

declare %an:sequential function
jsv:jsd-valid($jsd as object, $name as string, $ns as string,
  $instance as item) as boolean
{
  variable $jstypes := { "foo" : 5 };

  if( $jsd instance of object )
  then
    if ( exists($jsd("$namespace")) and exists($jsd("$types")) )
    then
      if ( $jsd("$namespace") instance of string )
      then
        if ( $jsd("$namespace") eq $ns )
        then
          if ( $jsd("$types") instance of array )
          then
            jsv:check-types( $jstypes, $jsd("$types") );
          else
            fn:error(QName("jsv", "BadJSoundFormat"),
              "Not a valid JSound doc: value of '$types' must be an array.");
        else
          fn:error(QName("jsv", "Invalid"),
            "Requested type namespace not matching schema document namespace.");
      else
        fn:error(QName("jsv", "BadJSoundFormat"),
          "Not a valid JSound doc: value of '$namespace' must be a string.");
    else
      fn:error(QName("jsv", "BadJSoundFormat"),
        "Not a valid JSound doc: must contain '$types' and '$namespace' keys.");
  else
    fn:error(QName("jsv", "BadJSoundFormat"),
      "The specified JSON document is not a valid JSound schema: top item not an object.");

  (: todo: validate all type refs :)

  jsv:validate-instance($jstypes, $name, $instance)
};

(: The check-xxx functions just checks if the JSound schema definitions are valid. :)

declare %an:sequential function
jsv:check-types($jstypes as object, $types as array) as boolean
{
  every $i in
    for $type in jn:members($types)
    return
      jsv:check-type($jstypes, $type )
  satisfies $i eq fn:true()
};

declare %an:sequential function
jsv:check-type($jstypes as object, $type as item) as boolean
{
  if( $type instance of object )
  then
    if ( exists($type("$kind")) and $type("$kind") instance of string )
    then
      switch( $type("$kind") )
      case "atomic" return jsv:check-atomic-type($jstypes, $type)
      case "object" return jsv:check-object-type($jstypes, $type)
      case "array"  return jsv:check-array-type ($jstypes, $type)
      case "union"  return jsv:check-union-type ($jstypes, $type)

      default return
        fn:error(QName("jsv", "BadJSoundFormat"),
          "Not a valid JSound doc: type $kind unknown:" | $type("$kind") )

    else
      fn:error(QName("jsv", "BadJSoundFormat"),
        "Not a valid JSound doc: type definition must contain '$kind' key with string value.")
  else
    if( $type instance of string )
    then
      jsv:check-ref-type($jstypes, $type)
    else
      fn:error(QName("jsv", "BadJSoundFormat"),
        "Not a valid JSound doc: type item not an object."  )
};

declare %an:sequential function
jsv:check-atomic-type($jstypes as object, $type as object) as boolean
{
  if( exists($type("$baseType")) and $type("$baseType") instance of string )
  then
    jsv:save-type($jstypes, $type)
  else
    fn:error(QName("jsv", "BadJSoundFormat"),
      "Not a valid JSound doc: atomic type definition must contain '$baseType' key with string value.")

  (: todo: add facets checks: length, min/max Inclusive/Exclusive, etc. :)
};

declare %an:sequential function
jsv:check-object-type($jstypes as object, $type as object) as boolean
{
  (: todo: check the reast of "object" type :)
  jsv:save-type($jstypes, $type);

  if( exists($type("$content")) )
  then
    if( $type("$content") instance of object )
    then
      every $i in
        for $k in jn:keys($type("$content"))
        return
          if( $type("$content")($k) instance of object )
          then
            if( exists( $type("$content")($k)("$type")) )
            then
              jsv:check-type($jstypes, $type("$content")($k)("$type") )
            else
              fn:error(QName("jsv", "BadJSoundFormat"),
                "Not a valid JSound doc: Key " | $k | " does not contain a $type definition.")
          else
            fn:error(QName("jsv", "BadJSoundFormat"),
              "Not a valid JSound doc: Key " | $k | " in $content must have an object value.")

      satisfies $i eq fn:true()
    else
      fn:error(QName("jsv", "BadJSoundFormat"),
              "Not a valid JSound doc: $content definion of object type must be an object.")
  else
    fn:error(QName("jsv", "BadJSoundFormat"),
             "Not a valid JSound doc: Object type does not contain $content.")
};

declare %an:sequential function
jsv:check-array-type($jstypes as object, $type as object) as boolean
{
  (: check all the contents :)
  (: let $t := fn:trace($type, "Array type:") :)
  let $content := $type("$content")
  return
    if( empty($content) )
    then
      fn:error(QName("jsv", "BadJSoundFormat"),
        "Not a valid JSound doc: $kind array must have a $content key.")
    else
      if( $content instance of array and jn:size($content) eq 1 )
      then
      {
        jsv:save-type($jstypes, $type);

        typeswitch( $content(1) )
        case string return
          fn:true()
        case object return
            jsv:check-type($jstypes, $content(1))
        default return
          fn:error(QName("jsv", "BadJSoundFormat"),
            "Not a valid JSound doc: for $kind array, $content[1] must be string or array.")
      }
      else
        fn:error(QName("jsv", "BadJSoundFormat"),
          "Not a valid JSound doc: for $kind array, $content must be an array of size 1.")

  (: Todo: check array facets :)
};

declare %an:sequential function
jsv:check-union-type($jstypes as object, $type as object) as boolean
{
  let $content := $type("$content")
  return
    if( empty($content) )
    then
      fn:error(QName("jsv", "BadJSoundFormat"),
        "Not a valid JSound doc: $kind union must have a $content key.")
    else
      if( $content instance of array )
      then
      {
        jsv:save-type($jstypes, $type);

        every $i in
          for $c in jn:members($content)
          return
            typeswitch( $c )
            case string return
              fn:true()
            case object return
                jsv:check-type($jstypes, $c)
            default return
              fn:error(QName("jsv", "BadJSoundFormat"),
                "Not a valid JSound doc: for $kind union, $content items must be strings or objects.")
        satisfies $i eq fn:true()
      }
      else
        fn:error(QName("jsv", "BadJSoundFormat"),
          "Not a valid JSound doc: for $kind union, $content must be an array.")
};

declare function
jsv:check-ref-type($jstypes as object, $type as string) as boolean
{
  switch( $type )
  case "string" return fn:true()
  case "integer" return fn:true()
  case "boolean" return fn:true()
  case "anyURI"  return fn:true()
  default return
  {
    (: fn:trace($type, "NYI: type ref not yet suported"); :)
       fn:error(QName("jsv", "NYI"), "Type ref: not yet implemented.")
  }

  (: todo: save all refs, check them at the end to see if they exist :)
  (: todo: apply resolution rules :)
};

declare %an:sequential function
jsv:save-type($jstypes as object, $type as item) as boolean
{
  if( fn:exists( $type("$name") ) )
  then
    if( $type("$name") instance of string )
    then
      map:set-if-empty($jstypes, $type("$name"), $type)
    else
      fn:error(QName("jsv", "BadJSoundFormat"),
          "Not a valid JSound doc: for $type, $name must be a string.")
  else
    ()
};

(: The validate-xxx functions actualy do the validation of the instance :)

declare function
jsv:validate-instance($jstypes as object, $name as string, $instance as item)
 as boolean
{
  let $type := map:get($jstypes, $name)
  return
    if( exists($type) )
    then
      jsv:validate-type($jstypes, $type, $instance)
    else
      fn:error(QName("jsv", "Invalid"),
             "Requested type name not present in schema: " | $name)
};

declare function
jsv:validate-type($jstypes as object, $type as item, $instance as item) as boolean
{
  switch( $type("$kind") )
  case "atomic" return jsv:validate-atomic-type($jstypes, $type, $instance)
  case "object" return jsv:validate-object-type($jstypes, $type, $instance)
  case "array"  return jsv:validate-array-type($jstypes, $type, $instance)
  case "union"  return jsv:validate-union-type($jstypes, $type, $instance)

  default return
    fn:error(QName("jsv", "BadJSoundFormat"),
      "Not a valid JSound doc: type $kind unknown:" | $type("$kind") )
};

declare function
jsv:validate-type-ref($jstypes as object, $type as string, $instance as item) as boolean
{
  (: todo: check first between the defined types in $jstypes and then the build-in ones, this is per spec :)

  switch( $type )
  case "string" return
    if ( $instance instance of string )
    then
      fn:true()
    else
      fn:error(QName("jsv", "Invalid"), "Expected string value.")
  case "integer" return
    if ( $instance instance of integer or
         $instance instance of int )
    then
      fn:true()
    else
      fn:error(QName("jsv", "Invalid"), "Expected integer value.")
  case "anyURI" return
    if ( $instance instance of anyURI )
    then
      fn:true()
    else
      fn:error(QName("jsv", "Invalid"), "Expected anyURI value.")
  case "boolean" return
    if ( $instance instance of boolean )
    then
      fn:true()
    else
      fn:error(QName("jsv", "Invalid"), "Expected boolean value.")

  default return
    fn:error(QName("jsv", "NYI"),
      "Type ref: not yet implemented: " | $type )
};

declare function
jsv:validate-atomic-type($jstypes as object, $type as object, $instance as item) as boolean
{
  (: todo: check first between the defined types in $jstypes and then the build-in ones, this is per spec :)

  if ( exists($type("$baseType")) and $type("$baseType") instance of string )
  then
    switch( $type("$baseType") )
    case "integer" return
      if ( $instance instance of integer or $instance instance of int )
      then
        fn:true() and jsv:validate-enumeration($jstypes, $type, $instance)
      else
        fn:error(QName("jsv", "Invalid"), "Expected integer or int value")

    case "string" return
      if ( $instance instance of string )
      then
        fn:true() and jsv:validate-enumeration($jstypes, $type, $instance)
      else
        fn:error( QName("jsv", "Invalid"), "Expected string value")

    case "anyURI" return
      if ( $instance instance of anyURI )
      then
        fn:true() and jsv:validate-enumeration($jstypes, $type, $instance)
      else
        fn:error(QName("jsv", "Invalid"), "Expected anyURI value")

    case "boolean" return
      if ( $instance instance of boolean )
      then
        fn:true() and jsv:validate-enumeration($jstypes, $type, $instance)
      else
        fn:error(QName("jsv", "Invalid"), "Expected boolean value")

    default return
      fn:error(QName("jsv", "NYI"),
        "$baseType: not yet implemented" | $type("$baseType") )
  else
    fn:error(QName("jsv", "BadJSoundFormat"),
      "Not a valid JSound doc: atomic type definition must contain '$baseType' key with string value.")

  (: todo: add facets checks: length, min/max Inclusive/Exclusive, etc. :)
};

declare function
jsv:validate-enumeration($jstypes as object, $type as object, $instance as item) as boolean
{
  variable $enum := $type("$enumeration");
  if( exists($enum) )
  then
    if( $enum instance of array )
    then
      if( some $v in jn:members($enum) satisfies $v eq $instance )
      then
        fn:true()
      else
        fn:error(QName("jsv", "Invalid"), "Instance value not in the enumeration list.")
    else
      fn:error(QName("jsv", "BadJSoundFormat"),
        "Not a valid JSound doc: the value of '$enumeration' must be an array.")
  else
    fn:true()
};

declare function
jsv:validate-object-type($jstypes as object, $type as object, $instance as item) as boolean
{
  variable $open := $type("$open");

  if( (not exists($open)) or (exists($open) and $open eq true()) )
  then
    (: object is open: i.e. extra keys alowed but all defined keys must be in the instance except the default ones :)
    (
      every $k in jn:keys($instance) satisfies
        let $ctype := $type("$content")($k)("$type")
        return
          typeswitch( $ctype )
          case string return
            jsv:validate-type-ref($jstypes, $ctype, $instance($k) )
          case object return
            jsv:validate-type($jstypes, $ctype, $instance($k) )
          default return
            fn:error(QName("jsv", "BadJSoundFormat"),
              "Not a valid JSound doc: for $kind object, $content $type must be string or array.")
    ) and (
      every $k in jn:keys($type("$content")) except jn:keys($instance) satisfies
        let $optional := $type("$content")($k)("$optional")
        return
          if( exists($optional) and $optional eq fn:true() )
          then fn:true()
          else
            fn:error(QName("jsv", "Invalid"),
             "Required key not present in instance:" | $k)
    )
  else
    (: object is closed: i.e. all defined keys must be in the instance except
       the default ones, no extra keys alowed :)
    (
      every $k in jn:keys($instance) satisfies
        let $ctype := $type("$content")($k)("$type")
        return
          typeswitch( $ctype )
          case string return
            jsv:validate-type-ref($jstypes, $ctype, $instance($k) )
          case object return
            jsv:validate-type($jstypes, $ctype, $instance($k) )
          default return
            fn:error(QName("jsv", "BadJSoundFormat"),
              "Not a valid JSound doc: for $kind object, $content $type must be string or array.")
    ) and (
      every $k in jn:keys($type("$content")) except jn:keys($instance) satisfies
        let $optional := $type("$content")($k)("$optional")
        return
          if( exists($optional) and $optional eq fn:true() )
          then fn:true()
          else
            fn:error(QName("jsv", "Invalid"),
             "Required key not present in instance:" | $k)
    )
};

declare function
jsv:validate-array-type($jstypes as object, $type as object, $instance as item) as boolean
{
  if( $instance instance of array )
  then
    (: check all the contents :)
    (: let $t := fn:trace($type, "Array type:") :)
    let $content := $type("$content")
    return
        typeswitch( $content(1) )
        case string return
          every $i in jn:members($instance) satisfies jsv:validate-type-ref($jstypes, $content(1), $i)
        case object return
          every $i in jn:members($instance) satisfies jsv:validate-type($jstypes, $content(1), $i)
        default return
          fn:error(QName("jsv", "BadJSoundFormat"),
            "Not a valid JSound doc: for $kind array, $content[1] must be string or array.")
  else
    fn:error(QName("jsv", "Invalid"), "Instance not an array." )

  (: check array facets :)
};

declare function
jsv:validate-union-type($jstypes as object, $type as object, $instance as item) as boolean
{
  let $content := $type("$content")
  return
    if(
      some $i in
        for $c in jn:members($content)
        return
          try
          {
            typeswitch( $c )
            case string return
              fn:true()
            case object return
                jsv:validate-type($jstypes, $c, $instance)
            default return
              fn:error(QName("jsv", "BadJSoundFormat"),
                "Not a valid JSound doc: for $kind union, $content items must be strings or objects.")
          }
          catch Invalid
          {
             fn:false()
          }
      satisfies $i eq fn:true()
    )
    then fn:true()
    else
      fn:error(QName("jsv", "Invalid"), "Instance not valid for union type." )
};

jsoniq version "1.0";

(:
 : Copyright 2006-2013 The FLWOR Foundation.
 :
 : Licensed under the Apache License, Version 2.0 (the "License");
 : you may not use this file except in compliance with the License.
 : You may obtain a copy of the License at
 :
 : http://www.apache.org/licenses/LICENSE-2.0
 :
 : Unless required by applicable law or agreed to in writing, software
 : distributed under the License is distributed on an "AS IS" BASIS,
 : WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 : See the License for the specific language governing permissions and
 : limitations under the License.
 :)

(:~
 : JSound simple validator
 : This is a JSONiq implemenation of the JSound (the schema for JSON) validator.
 :
 : @author Cezar Andrei
 :)

module namespace jsv = "http://jsound.io/modules/validate";

import module namespace jn = "http://jsoniq.org/functions";
import module namespace map = "http://jsound.io/modules/validate/map";

declare namespace an = "http://zorba.io/annotations";

declare %an:sequential function
jsv:jsd-valid($jsd as object, $name as string, $ns as string,
  $instance as item) as boolean
{
  variable $jstypes := { "foo" : 5 };

  if( $jsd instance of object )
  then
    if ( exists($jsd("$namespace")) and exists($jsd("$types")) )
    then
      if ( $jsd("$namespace") instance of string )
      then
        if ( $jsd("$namespace") eq $ns )
        then
          if ( $jsd("$types") instance of array )
          then
            jsv:check-types( $jstypes, $jsd("$types") );
          else
            fn:error(QName("jsv", "BadJSoundFormat"),
              "Not a valid JSound doc: value of '$types' must be an array.");
        else
          fn:error(QName("jsdJSound"),
            "Type namespace not matching document namespace.");
      else
        fn:error(QName("jsv", "BadJSoundFormat"),
          "Not a valid JSound doc: value of '$namespace' must be a string.");
    else
      fn:error(QName("jsv", "BadJSoundFormat"),
        "Not a valid JSound doc: must contain '$types' and '$namespace' keys.");
  else
    fn:error(QName("jsv", "BadJSoundFormat"),
      "The specified JSON document is not a valid JSound schema: top item not an object.");

  (: todo: validate all type refs :)

  jsv:validate-instance($jstypes, $name, $instance)
};

(: The check-xxx functions just checks if the JSound schema definitions are valid. :)

declare %an:sequential function
jsv:check-types($jstypes as object, $types as array) as boolean
{
  every $i in
    for $type in jn:members($types)
    return
      jsv:check-type($jstypes, $type)
  satisfies $i eq fn:true()
};

declare %an:sequential function
jsv:check-type($jstypes as object, $type as item) as boolean
{
  if( $type instance of object )
  then
    if ( exists($type("$kind")) and $type("$kind") instance of string )
    then
      switch( $type("$kind") )
      case "atomic" return jsv:check-atomic-type($jstypes, $type)
      case "object" return jsv:check-object-type($jstypes, $type)
      case "array"  return jsv:check-array-type ($jstypes, $type)
      case "union"  return jsv:check-union-type ($jstypes, $type)

      default return
        fn:error(QName("jsv", "BadJSoundFormat"),
          "Not a valid JSound doc: type $kind unknown:" | $type("$kind") )

    else
      fn:error(QName("jsv", "BadJSoundFormat"),
        "Not a valid JSound doc: type definition must contain '$kind' key with string value.")
  else
    if( $type instance of string )
    then
      jsv:check-ref-type($jstypes, $type)
    else
      fn:error(QName("jsv", "BadJSoundFormat"),
        "Not a valid JSound doc: type item not an object."  )
};

declare %an:sequential function
jsv:check-atomic-type($jstypes as object, $type as object) as boolean
{
  if( exists($type("$baseType")) and $type("$baseType") instance of string )
  then
    jsv:save-type($jstypes, $type)
  else
    fn:error(QName("jsv", "BadJSoundFormat"),
      "Not a valid JSound doc: atomic type definition must contain '$baseType' key with string value.")

  (: todo: add facets checks: length, min/max Inclusive/Exclusive, etc. :)
};

declare %an:sequential function
jsv:check-object-type($jstypes as object, $type as object) as boolean
{
  (: todo: check the reast of "object" type :)
  jsv:save-type($jstypes, $type);

  if( exists($type("$content")) )
  then
    if( $type("$content") instance of object )
    then
      every $i in
        for $k in jn:keys($type("$content"))
        return
          if( $type("$content")($k) instance of object )
          then
            if( exists($type("$content")($k)("$type")) )
            then
              jsv:check-type($jstypes, $type("$content")($k)("$type"))
            else
              fn:error(QName("jsv", "BadJSoundFormat"),
                "Not a valid JSound doc: Key " | $k | " does not contain a $type definition.")
          else
            fn:error(QName("jsv", "BadJSoundFormat"),
              "Not a valid JSound doc: Key " | $k | " in $content must have an object value.")

      satisfies $i eq fn:true()
    else
      fn:error(QName("jsv", "BadJSoundFormat"),
              "Not a valid JSound doc: $content definion of object type must be an object.")
  else
    fn:error(QName("jsv", "BadJSoundFormat"),
             "Not a valid JSound doc: Object type does not contain $content.")
};

declare %an:sequential function
jsv:check-array-type($jstypes as object, $type as object) as boolean
{
  (: check all the contents :)
  (: let $t := fn:trace($type, "Array type:") :)
  let $content := $type("$content")
  return
    if( empty($content) )
    then
      fn:error(QName("jsv", "BadJSoundFormat"),
        "Not a valid JSound doc: $kind array must have a $content key.")
    else
      if( $content instance of array and jn:size($content) eq 1 )
      then
      {
        jsv:save-type($jstypes, $type);

        typeswitch( $content(1) )
        case string return
          fn:true()
        case object return
            jsv:check-type($jstypes, $content(1))
        default return
          fn:error(QName("jsv", "BadJSoundFormat"),
            "Not a valid JSound doc: for $kind array, $content[1] must be string or array.")
      }
      else
        fn:error(QName("jsv", "BadJSoundFormat"),
          "Not a valid JSound doc: for $kind array, $content must be an array of size 1.")

  (: Todo: check array facets :)
};

declare %an:sequential function
jsv:check-union-type($jstypes as object, $type as object) as boolean
{
  let $content := $type("$content")
  return
    if( empty($content) )
    then
      fn:error(QName("jsv", "BadJSoundFormat"),
        "Not a valid JSound doc: $kind union must have a $content key.")
    else
      if( $content instance of array )
      then
      {
        jsv:save-type($jstypes, $type);

        every $i in
          for $c in jn:members($content)
          return
            typeswitch( $c )
            case string return
              fn:true()
            case object return
                jsv:check-type($jstypes, $c)
            default return
              fn:error(QName("jsv", "BadJSoundFormat"),
                "Not a valid JSound doc: for $kind union, $content items must be strings or objects.")
        satisfies $i eq fn:true()
      }
      else
        fn:error(QName("jsv", "BadJSoundFormat"),
          "Not a valid JSound doc: for $kind union, $content must be an array.")
};

declare function
jsv:check-ref-type($jstypes as object, $type as string)
{
  switch( $type )
  case "string" return fn:true()
  case "integer" return fn:true()
  case "boolean" return fn:true()
  case "anyURI"  return fn:true()
  default return
    fn:error(QName("jsv", "NYI"),
      "Type ref: not yet implemented: " | $type )

  (: todo: save all refs, check them at the end to see if they exist :)
  (: todo: apply resolution rules :)
};

declare %an:sequential function
jsv:save-type($jstypes as object, $type as item)
{
  if( fn:exists( $type("$name") ) )
  then
    if( $type("$name") instance of string )
    then
      map:set-if-empty($jstypes, $type("$name"), $type)
    else
      fn:error(QName("jsv", "BadJSoundFormat"),
          "Not a valid JSound doc: for $type, $name must be a string.")
  else
    ()
};

(: The validate-xxx functions actualy do the validation of the instance :)

declare function
jsv:validate-instance($jstypes as object, $name as string, $instance as item)
 as boolean
{
  let $type := map:get($jstypes, $name)
  return
    if( exists($type) )
    then
      jsv:validate-type($jstypes, $type, $instance)
    else
      fn:error(QName("jsv", "Invalid"),
             "Requested type name not present in schema: " | $name)
};

declare function
jsv:validate-type($jstypes as object, $type as item, $instance as item) as boolean
{
  switch( $type("$kind") )
  case "atomic" return jsv:validate-atomic-type($jstypes, $type, $instance)
  case "object" return jsv:validate-object-type($jstypes, $type, $instance)
  case "array"  return jsv:validate-array-type($jstypes, $type, $instance)
  case "union"  return jsv:validate-union-type($jstypes, $type, $instance)

  default return
    fn:error(QName("jsv", "BadJSoundFormat"),
      "Not a valid JSound doc: type $kind unknown:" | $type("$kind") )
};

declare function
jsv:validate-type-ref($jstypes as object, $type as string, $instance as item) as boolean
{
  (: todo: check first between the defined types in $jstypes and then the build-in ones, this is per spec :)

  switch( $type )
  case "string" return
    if ( $instance instance of string )
    then
      fn:true()
    else
      fn:error(QName("jsv", "Invalid"), "Expected string value.")
  case "integer" return
    if ( $instance instance of integer or
         $instance instance of int )
    then
      fn:true()
    else
      fn:error(QName("jsv", "Invalid"), "Expected integer value.")
  case "anyURI" return
    if ( $instance instance of anyURI )
    then
      fn:true()
    else
      fn:error(QName("jsv", "Invalid"), "Expected anyURI value.")
  case "boolean" return
    if ( $instance instance of boolean )
    then
      fn:true()
    else
      fn:error(QName("jsv", "Invalid"), "Expected boolean value.")

  default return
    fn:error(QName("jsv", "NYI"),
      "Type ref: not yet implemented: " | $type )
};

declare function
jsv:validate-atomic-type($jstypes as object, $type as object, $instance as item) as boolean
{
  (: todo: check first between the defined types in $jstypes and then the build-in ones, this is per spec :)

  if ( exists($type("$baseType")) and $type("$baseType") instance of string )
  then
    switch( $type("$baseType") )
    case "integer" return
      if ( $instance instance of integer or $instance instance of int )
      then
        fn:true() and jsv:validate-enumeration($jstypes, $type, $instance)
      else
        fn:error(QName("jsv", "Invalid"), "Expected integer or int value")

    case "string" return
      if ( $instance instance of string )
      then
        fn:true() and jsv:validate-enumeration($jstypes, $type, $instance)
      else
        fn:error( QName("jsv", "Invalid"), "Expected string value")

    case "anyURI" return
      if ( $instance instance of anyURI )
      then
        fn:true() and jsv:validate-enumeration($jstypes, $type, $instance)
      else
        fn:error(QName("jsv", "Invalid"), "Expected anyURI value")

    case "boolean" return
      if ( $instance instance of boolean )
      then
        fn:true() and jsv:validate-enumeration($jstypes, $type, $instance)
      else
        fn:error(QName("jsv", "Invalid"), "Expected boolean value")

    default return
      fn:error(QName("jsv", "NYI"),
        "$baseType: not yet implemented" | $type("$baseType") )
  else
    fn:error(QName("jsv", "BadJSoundFormat"),
      "Not a valid JSound doc: atomic type definition must contain '$baseType' key with string value.")

  (: todo: add facets checks: length, min/max Inclusive/Exclusive, etc. :)
};

declare function
jsv:validate-enumeration($jstypes as object, $type as object, $instance as item) as boolean
{
  variable $enum := $type("$enumeration");
  if( exists($enum) )
  then
    if( $enum instance of array )
    then
      if( some $v in jn:members($enum) satisfies $v eq $instance )
      then
        fn:true()
      else
        fn:error(QName("jsv", "Invalid"), "Instance value not in the enumeration list.")
    else
      fn:error(QName("jsv", "BadJSoundFormat"),
        "Not a valid JSound doc: the value of '$enumeration' must be an array.")
  else
    fn:true()
};

declare function
jsv:validate-object-type($jstypes as object, $type as object, $instance as item) as boolean
{
  variable $open := $type("$open");

  if( (not exists($open)) or (exists($open) and $open eq true()) )
  then
    (: object is open: i.e. extra keys alowed but all defined keys must be in the instance except the default ones :)
    (
      every $k in jn:keys($instance) satisfies
        let $ctype := $type("$content")($k)("$type")
        return
          typeswitch( $ctype )
          case string return
            jsv:validate-type-ref($jstypes, $ctype, $instance($k) )
          case object return
            jsv:validate-type($jstypes, $ctype, $instance($k) )
          default return
            fn:error(QName("jsv", "BadJSoundFormat"),
              "Not a valid JSound doc: for $kind object, $content $type must be string or array.")
    ) and (
      every $k in jn:keys($type("$content")) except jn:keys($instance) satisfies
        let $optional := $type("$content")($k)("$optional")
        return
          if( exists($optional) and $optional eq fn:true() )
          then fn:true()
          else
            fn:error(QName("jsv", "Invalid"),
             "Required key not present in instance:" | $k)
    )
  else
    (: object is closed: i.e. all defined keys must be in the instance except
       the default ones, no extra keys alowed :)
    (
      every $k in jn:keys($instance) satisfies
        let $ctype := $type("$content")($k)("$type")
        return
          typeswitch( $ctype )
          case string return
            jsv:validate-type-ref($jstypes, $ctype, $instance($k) )
          case object return
            jsv:validate-type($jstypes, $ctype, $instance($k) )
          default return
            fn:error(QName("jsv", "BadJSoundFormat"),
              "Not a valid JSound doc: for $kind object, $content $type must be string or array.")
    ) and (
      every $k in jn:keys($type("$content")) except jn:keys($instance) satisfies
        let $optional := $type("$content")($k)("$optional")
        return
          if( exists($optional) and $optional eq fn:true() )
          then fn:true()
          else
            fn:error(QName("jsv", "Invalid"),
             "Required key not present in instance:" | $k)
    )
};

declare function
jsv:validate-array-type($jstypes as object, $type as object, $instance as item) as boolean
{
  if( $instance instance of array )
  then
    (: check all the contents :)
    (: let $t := fn:trace($type, "Array type:") :)
    let $content := $type("$content")
    return
        typeswitch( $content(1) )
        case string return
          every $i in jn:members($instance) satisfies jsv:validate-type-ref($jstypes, $content(1), $i)
        case object return
          every $i in jn:members($instance) satisfies jsv:validate-type($jstypes, $content(1), $i)
        default return
          fn:error(QName("jsv", "BadJSoundFormat"),
            "Not a valid JSound doc: for $kind array, $content[1] must be string or array.")
  else
    fn:error(QName("jsv", "Invalid"), "Instance not an array." )

  (: check array facets :)
};

declare function
jsv:validate-union-type($jstypes as object, $type as object, $instance as item) as boolean
{
  let $content := $type("$content")
  return
    if(
      some $i in
        for $c in jn:members($content)
        return
          try
          {
            typeswitch( $c )
            case string return
              fn:true()
            case object return
                jsv:validate-type($jstypes, $c, $instance)
            default return
              fn:error(QName("jsv", "BadJSoundFormat"),
                "Not a valid JSound doc: for $kind union, $content items must be strings or objects.")
          }
          catch Invalid
          {
             fn:false()
          }
      satisfies $i eq fn:true()
    )
    then fn:true()
    else
      fn:error(QName("jsv", "Invalid"), "Instance not valid for union type." )
};