:green_heart: Fix build.

:green_heart: Fix build.

jsoniq version "1.0";

(:
 : Copyright 2012-2013 28msec Inc.
 :)

(:~
 :
This module provides functionality for querying fiscal reports (10-K, 10-Q)
 : submitted to the SEC.

 :
 : @author Charles Hoffman
 : @author Matthias Brantner
 : @author Dennis Knochenwefel
 : @author Ghislain Fourny
 :)
module namespace fiscal-core = "http://28.io/modules/xbrl/profiles/sec/fiscal/core";

import module namespace mw = "http://28.io/modules/xbrl/mongo-wrapper";

import module namespace archives = "http://28.io/modules/xbrl/archives";
import module namespace facts = "http://28.io/modules/xbrl/facts";

import module namespace companies = "http://28.io/modules/xbrl/profiles/sec/companies";
import module namespace entities = "http://28.io/modules/xbrl/entities";
import module namespace sec = "http://28.io/modules/xbrl/profiles/sec/core";

declare namespace ver = "http://zorba.io/options/versioning";
declare option ver:module-version "1.0";

(:~
 : Joker for all fiscal periods.
 :)
declare variable $fiscal-core:ALL_FISCAL_PERIODS as string := "ALL";

(:~
 : Joker for all fiscal period types.
 :)
declare variable $fiscal-core:ALL_FISCAL_PERIOD_TYPES as string := "ALL";

(:~
 : Joker for all fiscal years.
 :)
declare variable $fiscal-core:ALL_FISCAL_YEARS as integer := 0;

(:~
 : Joker for the latest fiscal years.
 :)
declare variable $fiscal-core:LATEST_FISCAL_YEAR as integer := 1;

(:~
 :
Return a distinct set of companies identified by either
 :   CIKs, tags, tickers, or sics.

 :
 : @param $ciks a set of CIKs.
 : @param $tags a set of tags (ALL retrieves all companies).
 : @param $tickers a set of tickers.
 : @param $sics a set of SIC codes.
 :
 : @return the companies with the given identifiers, tags, tickers, or sic codes.
 :)
declare function fiscal-core:filings(
    $entities-or-eids as item*,
    $fiscal-periods as string*,
    $fiscal-years as integer*,
    $archives-or-aids as item*) as object*
{
    let $entities := entities:entities($entities-or-eids)
    return
        if(exists(index-of($fiscal-years, $fiscal-core:LATEST_FISCAL_YEAR)))
        then
            for $entity in $entities
            return
            fiscal-core:latest-filings($entity, $fiscal-periods)
        else
            let $fiscal-years as integer* :=
                $fiscal-years[$$ ne $fiscal-core:LATEST_FISCAL_YEAR]
            return
                fiscal-core:filings-for-entities-and-fiscal-periods-and-years($entities, $fiscal-periods, $fiscal-years),
    archives:archives($archives-or-aids)
};

declare function fiscal-core:latest-filings(
    $entities as object*,
    $fiscal-periods as string*) as object*
{
    for $entity in $entities
    for $fiscal-period in $fiscal-periods
    let $latest-fiscal-year :=
        fiscal-core:latest-reported-fiscal-period($entity, $fiscal-period).year
    return fiscal-core:filings-for-entities-and-fiscal-periods-and-years(
        $entity,
        $fiscal-period,
        $latest-fiscal-year cast as integer?
    )
};

declare function fiscal-core:filter-override(
    $entities-or-eids as item*,
    $fiscal-years as integer*,
    $fiscal-periods as string*,
    $fiscal-period-types as string*,
    $archives-or-aids as item*
) as object?
{
    let $override := exists(($entities-or-eids, $fiscal-years, $fiscal-periods, $fiscal-period-types, $archives-or-aids))
    where $override
    let $eids := entities:eid($entities-or-eids)
    let $entities := entities:entities($entities-or-eids)
    let $aids := archives:aid($archives-or-aids)
    let $latest-filings := fiscal-core:latest-filings($entities, $fiscal-periods)
    let $is-latest := exists(index-of($fiscal-years, $fiscal-core:LATEST_FISCAL_YEAR))
    let $fiscal-years := if(exists(index-of($fiscal-years, $fiscal-core:ALL_FISCAL_YEARS)))
                         then ()
                         else $fiscal-years[$$ ne  $fiscal-core:LATEST_FISCAL_YEAR]
    let $fiscal-periods := if(exists(index-of($fiscal-periods, $fiscal-core:ALL_FISCAL_PERIODS)))
                           then ()
                           else $fiscal-periods
    let $override-fiscal-period-types := exists($fiscal-period-types)
    let $fiscal-period-types := if(exists(index-of($fiscal-period-types, $fiscal-core:ALL_FISCAL_PERIOD_TYPES)))
                                then ()
                                else $fiscal-period-types
    return
     {
        "xbrl:Entity" : {|
          { Type: "string" },
          { Domain: [ $eids ] }[exists($eids)]
        |},

        "sec:Accepted" : {|
          { Type: "string" }
        |},

        "xbrl28:Archive" : {|
          { Type: "string" },
          { Domain : [ if($is-latest) then archives:aid($latest-filings) else $aids ] }
          [exists(if($is-latest) then archives:aid($latest-filings) else $aids)]
        |},

        "sec:FiscalYear" : {|
          { Type: "integer" },
          { Domain: [ $fiscal-years ] }[exists($fiscal-years)]
        |},

        "sec:FiscalPeriod" : {|
          { Type: "string" },
          { Domain: [ $fiscal-periods ] }[exists($fiscal-periods)]
        |},

        "sec:FiscalPeriodType" : {|
          { Type: "string" },
          if($override-fiscal-period-types)
          then { Domain: [ $fiscal-period-types ]}[exists($fiscal-period-types)]
          else { Domain: [ "instant", "YTD" ] }
        |}
    }
};

(:~
 :
Retrieves the fiscal period of a filing.

 :
 : @param $filing-or-id a filing or its id, or the empty sequence.
 :
 : @return the fiscal period (FY, YTD2, YTD3, Q1, Q2 or Q3) if any.
 :)
declare function fiscal-core:fiscal-period-for-filing(
    $filing-or-id as item?) as string?
{
  archives:archives($filing-or-id).Profiles.SEC.Fiscal.DocumentFiscalPeriodFocus
};

(:~
 :
Retrieves the fiscal year of a filing.

 :
 : @param $filing-or-id a filing or its id, or the empty sequence.
 :
 : @return the fiscal year (e.g., 2012) if any.
 :)
declare function fiscal-core:fiscal-year-for-filing(
    $filing-or-id as item?) as integer?
{
  archives:archives($filing-or-id).Profiles.SEC.Fiscal.DocumentFiscalYearFocus
};

(:~
 :
Retrieves the fiscal period of a fact.

 :
 : @param $fact a fact object or the empty sequence.
 :
 : @return the fiscal period (FY, YTD2, YTD3, Q1, Q2 or Q3) if any.
 :)
declare function fiscal-core:fiscal-period-for-fact(
    $fact as object?) as integer?
{
  $fact.Aspects."sec:FiscalPeriod"
};

(:~
 :
Retrieves the fiscal year of a fact.

 :
 : @param $fact a fact object or the empty sequence.
 :
 : @return the fiscal year (e.g., 2012) if any.
 :)
declare function fiscal-core:fiscal-year-for-fact(
    $fact as object?) as integer?
{
  $fact.Aspects."sec:FiscalYear"
};

(:~
 :
Retrieves all filings by entities and for given fiscal periods and years.

 :
 : @param $entities-or-ids entities or their ids.
 : @param $fiscal-period-focus fiscal periods (FY, YTD1, YTD2, YTD3, Q1, Q2, Q3, Q4) or $fiscal-core:ALL_FISCAL_PERIODS to accept all..
 : @param $fiscal-year-focus fiscal years or $fiscal-core:ALL_FISCAL_YEARS to accept all..
 :
 : @return all filings that match the criteria.
 :)
declare function
fiscal-core:filings-for-entities-and-fiscal-periods-and-years(
    $entities-or-ids as item*,
    $fiscal-period-focus as string*,
    $fiscal-year-focus as integer*) as object*
{
  let $fiscal-period-focus := (
    "FY"[$fiscal-period-focus = ("10-K", "FY", "Q4")],
    "Q1"[$fiscal-period-focus = ("10-Q", "YTD1", "Q1")],
    "Q2"[$fiscal-period-focus = ("10-Q", "YTD2", "Q2")],
    "Q3"[$fiscal-period-focus = ("10-Q", "YTD3", "Q3")],
    $fiscal-period-focus[not $$ = ("10-K", "10-Q", "YTD1", "YTD2", "YTD3", "Q1", "Q2", "Q3", "Q4")]
  )
  for $cik-or-entity in $entities-or-ids
  let $entity-id as xs:string := companies:eid($cik-or-entity)
  let $query := {|
      { "Entity" : $entity-id },
      if(deep-equal($fiscal-period-focus, $fiscal-core:ALL_FISCAL_PERIODS) and
         deep-equal($fiscal-year-focus, $fiscal-core:ALL_FISCAL_YEARS))
      then ()
      else {
        "Profiles" : {
          "SEC" : {
            "Fiscal" : {|
              if(deep-equal($fiscal-period-focus, $fiscal-core:ALL_FISCAL_PERIODS))
              then ()
              else { "DocumentFiscalPeriodFocus": [ $fiscal-period-focus ] },
              if(deep-equal($fiscal-year-focus, $fiscal-core:ALL_FISCAL_YEARS))
              then ()
              else { "DocumentFiscalYearFocus": [ $fiscal-year-focus ] }
            |}
          }
        }
      }
    |}
  return mw:find($archives:col, $query)
};

(:~
 :
Retrieves facts for the given aspects, fiscal periods and years.

 :
 : @param $aspects an object containing aspects to filter, among which
 :                 xbrl:Concept, xbrl:Entity and xbrl:Period (all optional).
 : @param $fiscal-period-focus the periods (YTD1, YTD2, YTD3, Q1, Q2, Q3 or FY).
 : @param $fiscal-year-focus the years (2012)
 :
 : @return all facts matching the given aspects, fiscal period and year.
 : @deprecated This function has been deprecated in favor of more generic
 :   functions like facts:facts-for and hypercubes:facts.
 :)
declare function
  fiscal-core:facts-for-aspects-and-fiscal-periods-and-years(
    $aspects as object?,
    $fiscal-period-focus as string*,
    $fiscal-year-focus as integer*) as object*
{
  fiscal-core:facts-for-aspects-and-fiscal-periods-and-years(
    $aspects,
    $fiscal-period-focus,
    $fiscal-year-focus,
    ())
};

(:~
 :
Retrieves facts for the given aspects, fiscal periods and years.

 :
 : @param $aspects an object containing aspects to filter, among which
 :                 xbrl:Concept, xbrl:Entity and xbrl:Period (all optional).
 : @param $fiscal-period-focus the periods (YTD1, YTD2, YTD3, Q1, Q2, Q3 or FY) or $fiscal-core:ALL_FISCAL_PERIODS to accept all.
 : @param $fiscal-year-focus the years (2012) or $fiscal-core:ALL_FISCAL_YEARS to accept all.
 : @param $options standard SEC BizQL options.
 :
 : @return all facts matching the given aspects, fiscal period and year.
 : @deprecated This function has been deprecated in favor of more generic
 :   functions like facts:facts-for and hypercubes:facts.
 :)
declare function
  fiscal-core:facts-for-aspects-and-fiscal-periods-and-years(
    $aspects as object?,
    $fiscal-period-focus as string*,
    $fiscal-year-focus as integer*,
    $options as object?) as object*
{
  let $filter as object := {|
    {
      "Filter":
        {|
          if(exists($aspects))
          then { $facts:ASPECTS : $aspects }
          else (),
          if( deep-equal($fiscal-period-focus, $fiscal-core:ALL_FISCAL_PERIODS) and deep-equal($fiscal-year-focus, $fiscal-core:ALL_FISCAL_YEARS))
          then { "Profiles" : { "SEC" : { "Fiscal" : { "$exists" : true } } } }
          else { "Profiles" : { "SEC" : { "Fiscal":
                 {|
                   if(deep-equal($fiscal-period-focus, $fiscal-core:ALL_FISCAL_PERIODS) )
                   then ()
                   else { "Period" : [ $fiscal-period-focus ] },
                   if( deep-equal($fiscal-year-focus, $fiscal-core:ALL_FISCAL_YEARS) )
                   then ()
                   else { "Year": [ $fiscal-year-focus ] }
                 |}
               }}}
        |}
    },
    if (empty($options.Hypercube))
    then { Hypercube: sec:dimensionless-hypercube(()) }
    else ()
  |}
  let $facts :=
    facts:facts-for(
      facts:merge-objects($filter, $options, true (: giving parameters higher priority :))
    )
  return sec:hide-amended-facts($facts, $options)
};

(:~
 :
Retrieves facts for the given entities, concepts, fiscal periods and years.

 :
 : @param $entities-or-ids a list of entities or their EIDs.
 : @param $concepts a list of concept names (us-gaap:Assets)
 : @param $fiscal-period-focus the periods (YTD1, YTD2, YTD3, Q1, Q2, Q3 or FY) or $fiscal-core:ALL_FISCAL_PERIODS to accept all.
 : @param $fiscal-year-focus the years (2012) or $fiscal-core:ALL_FISCAL_YEARS to accept all.
 :
 : @error entities:INVALID_PARAMETER if the CIK or entity is not valid.
 : @return all facts matching entities, concepts, fiscal periods and years
 : @deprecated This function has been deprecated in favor of more generic
 :   functions like facts:facts-for and hypercubes:facts.
 :)
declare function
  fiscal-core:facts-for-entities-and-concepts-and-fiscal-periods-and-years(
    $entities-or-ids as item*,
    $concepts as string*,
    $fiscal-period-focus as string*,
    $fiscal-year-focus as integer*) as object*
{
  fiscal-core:facts-for-entities-and-concepts-and-fiscal-periods-and-years(
    $entities-or-ids,
    $concepts,
    $fiscal-period-focus,
    $fiscal-year-focus,
    ())
};

(:~
 :
Retrieves facts for the given entities, concepts, fiscal periods and years.

 :
 : @param $entities-or-ids a list of entities or their EIDs.
 : @param $concepts a list of concept names (us-gaap:Assets)
 : @param $fiscal-period-focus the periods (YTD1, YTD2, YTD3, Q1, Q2, Q3 or FY) or $fiscal-core:ALL_FISCAL_PERIODS to accept all.
 : @param $fiscal-year-focus the years (2012) or $fiscal-core:ALL_FISCAL_YEARS to accept all.
 : @param $options standard SEC BizQL options.
 :
 : @error entities:INVALID_PARAMETER if the CIK or entity is not valid.
 : @return all facts matching entities, concepts, fiscal periods and years
 : @deprecated This function has been deprecated in favor of more generic
 :   functions like facts:facts-for and hypercubes:facts.
 :)
declare function
  fiscal-core:facts-for-entities-and-concepts-and-fiscal-periods-and-years(
    $entities-or-ids as item*,
    $concepts as string*,
    $fiscal-period-focus as string*,
    $fiscal-year-focus as integer*,
    $options as object?) as object*
{
  let $entity-ids as xs:string* := $entities-or-ids ! companies:eid($$)
  let $aspects := {|
    if(exists($entity-ids))
    then { "xbrl:Entity": [ $entity-ids ] }
    else (),
    if(exists($concepts))
    then { "xbrl:Concept": [ $concepts ] }
    else ()
  |}
  return
    fiscal-core:facts-for-aspects-and-fiscal-periods-and-years(
      $aspects,
      $fiscal-period-focus,
      $fiscal-year-focus,
      $options)
};

(:~
 :
Retrieves the latest reported fiscal period and year for the
 : supplied entity.

 :
 : @param $entity-or-id an entity or its EID.
 :
 : @return an object with two fields: period and year.
 :)
declare function fiscal-core:latest-reported-fiscal-period(
    $entity-or-id as item) as object?
{
  (
  let $entity-or-id := companies:eid($entity-or-id)
  for $archive in archives:archives-for-entities($entity-or-id)
  let $archive-fiscal-period := fiscal-core:fiscal-period-for-filing($archive)
  let $archive-fiscal-year := fiscal-core:fiscal-year-for-filing($archive)
  group by $archive-fiscal-year
  order by $archive-fiscal-year descending empty least
  return {
    year: $archive-fiscal-year,
    period: switch(true)
            case $archive-fiscal-period = ("Q4", "FY") return "FY"
            case $archive-fiscal-period = ("Q3", "YTD3") return "Q3"
            case $archive-fiscal-period = ("Q2", "YTD2") return "Q2"
            case $archive-fiscal-period = ("Q1", "YTD1") return "Q1"
            default return () }
  )[1]
};

(:~
 :
Retrieves the latest fiscal period and year for the
 : supplied entity.

 :
 : @param $entity-or-id an entity or its EID.
 : @param $type 10-K or 10-Q or FY, YTD1, YTD2, YTD3, Q1, Q2, or Q3, or $fiscal-core:ALL_FISCAL_PERIODS
 : to accept all.
 :
 : @return an object with two fields: period and year.
 :)
declare function fiscal-core:latest-reported-fiscal-period(
    $entity-or-id as item,
    $type as string) as object?
{
  if(deep-equal($type, $fiscal-core:ALL_FISCAL_PERIODS))
  then fiscal-core:latest-reported-fiscal-period($entity-or-id)
  else
  (
  let $type-filter :=
    switch(true)
    case $type = ( "10-K", "FY", "Q4") return "FY"
    case $type eq "10-Q" return ( "Q3", "Q2", "Q1" )
    case $type eq "YTD1" return "Q1"
    case $type eq "YTD2" return "Q2"
    case $type eq "YTD3" return "Q3"
    default return $type
  let $entity-or-id := companies:eid($entity-or-id)
  for $archive in archives:archives-for-entities($entity-or-id)
  let $archive-fiscal-period := fiscal-core:fiscal-period-for-filing($archive)
  let $archive-fiscal-year := fiscal-core:fiscal-year-for-filing($archive)
  where $archive-fiscal-period = $type-filter
  group by $archive-fiscal-year
  order by $archive-fiscal-year descending empty least
  return {
    year: $archive-fiscal-year,
    period:
      switch(true)
      case $archive-fiscal-period = "FY" and ($type eq "10-K" or $type eq "FY") return "FY"
      case $archive-fiscal-period = "FY" and ($type eq "10-K" or $type eq "Q4") return "FY"
      case $archive-fiscal-period = "Q3" and ($type eq "10-Q" or $type eq "Q3") return "Q3"
      case $archive-fiscal-period = "Q2" and ($type eq "10-Q" or $type eq "Q2") return "Q2"
      case $archive-fiscal-period = "Q1" and ($type eq "10-Q" or $type eq "Q1") return "Q1"
      default return () }
  )[1]
};

jsoniq version "1.0";

(:
 : Copyright 2012-2013 28msec Inc.
 :)

(:~
 :
This module provides functionality for querying fiscal reports (10-K, 10-Q)
 : submitted to the SEC.

 :
 : @author Charles Hoffman
 : @author Matthias Brantner
 : @author Dennis Knochenwefel
 : @author Ghislain Fourny
 :)
module namespace fiscal-core = "http://28.io/modules/xbrl/profiles/sec/fiscal/core";

import module namespace mw = "http://28.io/modules/xbrl/mongo-wrapper";

import module namespace archives = "http://28.io/modules/xbrl/archives";
import module namespace facts = "http://28.io/modules/xbrl/facts";

import module namespace companies = "http://28.io/modules/xbrl/profiles/sec/companies";
import module namespace entities = "http://28.io/modules/xbrl/entities";
import module namespace sec = "http://28.io/modules/xbrl/profiles/sec/core";

declare namespace ver = "http://zorba.io/options/versioning";
declare option ver:module-version "1.0";

(:~
 : Joker for all fiscal periods.
 :)
declare variable $fiscal-core:ALL_FISCAL_PERIODS as string := "ALL";

(:~
 : Joker for all fiscal period types.
 :)
declare variable $fiscal-core:ALL_FISCAL_PERIOD_TYPES as string := "ALL";

(:~
 : Joker for all fiscal years.
 :)
declare variable $fiscal-core:ALL_FISCAL_YEARS as integer := 0;

(:~
 : Joker for the latest fiscal years.
 :)
declare variable $fiscal-core:LATEST_FISCAL_YEAR as integer := 1;

(:~
 :
Return a distinct set of companies identified by either
 :   CIKs, tags, tickers, or sics.

 :
 : @param $ciks a set of CIKs.
 : @param $tags a set of tags (ALL retrieves all companies).
 : @param $tickers a set of tickers.
 : @param $sics a set of SIC codes.
 :
 : @return the companies with the given identifiers, tags, tickers, or sic codes.
 :)
declare function fiscal-core:filings(
    $entities-or-eids as item*,
    $fiscal-periods as string*,
    $fiscal-years as integer*,
    $archives-or-aids as item*) as object*
{
    let $entities := entities:entities($entities-or-eids)
    return
        if(exists(index-of($fiscal-years, $fiscal-core:LATEST_FISCAL_YEAR)))
        then
            for $entity in $entities
            return
            fiscal-core:latest-filings($entity, $fiscal-periods)
        else
            let $fiscal-years as integer* :=
                $fiscal-years[$$ ne $fiscal-core:LATEST_FISCAL_YEAR]
            return
                fiscal-core:filings-for-entities-and-fiscal-periods-and-years($entities, $fiscal-periods, $fiscal-years),
    archives:archives($archives-or-aids)
};

declare function fiscal-core:latest-filings(
    $entities as object*,
    $fiscal-periods as string*) as object*
{
    for $entity in $entities
    for $fiscal-period in $fiscal-periods
    let $latest-fiscal-year :=
        fiscal-core:latest-reported-fiscal-period($entity, $fiscal-period).year
    return fiscal-core:filings-for-entities-and-fiscal-periods-and-years(
        $entity,
        $fiscal-period,
        $latest-fiscal-year cast as integer?
    )
};

declare function fiscal-core:filter-override(
    $entities-or-eids as item*,
    $fiscal-years as integer*,
    $fiscal-periods as string*,
    $fiscal-period-types as string*,
    $archives-or-aids as item*
) as object?
{
    let $override := exists(($entities-or-eids, $fiscal-years, $fiscal-periods, $fiscal-period-types, $archives-or-aids))
    where $override
    let $eids := entities:eid($entities-or-eids)
    let $entities := entities:entities($entities-or-eids)
    let $aids := archives:aid($archives-or-aids)
    let $latest-filings := fiscal-core:latest-filings($entities, $fiscal-periods)
    let $is-latest := exists(index-of($fiscal-years, $fiscal-core:LATEST_FISCAL_YEAR))
    let $fiscal-years := if(exists(index-of($fiscal-years, $fiscal-core:ALL_FISCAL_YEARS)))
                         then ()
                         else $fiscal-years[$$ ne  $fiscal-core:LATEST_FISCAL_YEAR]
    let $fiscal-periods := if(exists(index-of($fiscal-periods, $fiscal-core:ALL_FISCAL_PERIODS)))
                           then ()
                           else $fiscal-periods
    let $override-fiscal-period-types := exists($fiscal-period-types)
    let $fiscal-period-types := if(exists(index-of($fiscal-period-types, $fiscal-core:ALL_FISCAL_PERIOD_TYPES)))
                                then ()
                                else $fiscal-period-types
    return
     {
        "xbrl:Entity" : {|
          { Type: "string" },
          { Domain: [ $eids ] }[exists($eids)]
        |},

        "sec:Accepted" : {|
          { Type: "string" }
        |},

        "xbrl28:Archive" : {|
          { Type: "string" },
          { Domain : [ if($is-latest) then archives:aid($latest-filings) else $aids ] }
          [exists(if($is-latest) then archives:aid($latest-filings) else $aids)]
        |},

        "sec:FiscalYear" : {|
          { Type: "integer" },
          { Domain: [ $fiscal-years ] }[exists($fiscal-years)]
        |},

        "sec:FiscalPeriod" : {|
          { Type: "string" },
          { Domain: [ $fiscal-periods ] }[exists($fiscal-periods)]
        |},

        "sec:FiscalPeriodType" : {|
          { Type: "string" },
          if($override-fiscal-period-types)
          then { Domain: [ $fiscal-period-types ]}[exists($fiscal-period-types)]
          else { Domain: [ "instant", "YTD" ] }
        |}
    }
};

(:~
 :
Retrieves the fiscal period of a filing.

 :
 : @param $filing-or-id a filing or its id, or the empty sequence.
 :
 : @return the fiscal period (FY, YTD2, YTD3, Q1, Q2 or Q3) if any.
 :)
declare function fiscal-core:fiscal-period-for-filing(
    $filing-or-id as item?) as string?
{
  archives:archives($filing-or-id).Profiles.SEC.Fiscal.DocumentFiscalPeriodFocus
};

(:~
 :
Retrieves the fiscal year of a filing.

 :
 : @param $filing-or-id a filing or its id, or the empty sequence.
 :
 : @return the fiscal year (e.g., 2012) if any.
 :)
declare function fiscal-core:fiscal-year-for-filing(
    $filing-or-id as item?) as integer?
{
  archives:archives($filing-or-id).Profiles.SEC.Fiscal.DocumentFiscalYearFocus
};

(:~
 :
Retrieves the fiscal period of a fact.

 :
 : @param $fact a fact object or the empty sequence.
 :
 : @return the fiscal period (FY, YTD2, YTD3, Q1, Q2 or Q3) if any.
 :)
declare function fiscal-core:fiscal-period-for-fact(
    $fact as object?) as integer?
{
  $fact.Aspects."sec:FiscalPeriod"
};

(:~
 :
Retrieves the fiscal year of a fact.

 :
 : @param $fact a fact object or the empty sequence.
 :
 : @return the fiscal year (e.g., 2012) if any.
 :)
declare function fiscal-core:fiscal-year-for-fact(
    $fact as object?) as integer?
{
  $fact.Aspects."sec:FiscalYear"
};

(:~
 :
Retrieves all filings by entities and for given fiscal periods and years.

 :
 : @param $entities-or-ids entities or their ids.
 : @param $fiscal-period-focus fiscal periods (FY, YTD1, YTD2, YTD3, Q1, Q2, Q3, Q4) or $fiscal-core:ALL_FISCAL_PERIODS to accept all..
 : @param $fiscal-year-focus fiscal years or $fiscal-core:ALL_FISCAL_YEARS to accept all..
 :
 : @return all filings that match the criteria.
 :)
declare function
fiscal-core:filings-for-entities-and-fiscal-periods-and-years(
    $entities-or-ids as item*,
    $fiscal-period-focus as string*,
    $fiscal-year-focus as integer*) as object*
{
  let $fiscal-period-focus := (
    "FY"[$fiscal-period-focus = ("10-K", "FY", "Q4")],
    "Q1"[$fiscal-period-focus = ("10-Q", "YTD1", "Q1")],
    "Q2"[$fiscal-period-focus = ("10-Q", "YTD2", "Q2")],
    "Q3"[$fiscal-period-focus = ("10-Q", "YTD3", "Q3")],
    $fiscal-period-focus[not $$ = ("10-K", "10-Q", "YTD1", "YTD2", "YTD3", "Q1", "Q2", "Q3", "Q4")]
  )
  for $cik-or-entity in $entities-or-ids
  let $entity-id as xs:string := companies:eid($cik-or-entity)
  let $query := {|
      { "Entity" : $entity-id },
      if(deep-equal($fiscal-period-focus, $fiscal-core:ALL_FISCAL_PERIODS) and
         deep-equal($fiscal-year-focus, $fiscal-core:ALL_FISCAL_YEARS))
      then ()
      else {
        "Profiles" : {
          "SEC" : {
            "Fiscal" : {|
              if(deep-equal($fiscal-period-focus, $fiscal-core:ALL_FISCAL_PERIODS))
              then ()
              else { "DocumentFiscalPeriodFocus": [ $fiscal-period-focus ] },
              if(deep-equal($fiscal-year-focus, $fiscal-core:ALL_FISCAL_YEARS))
              then ()
              else { "DocumentFiscalYearFocus": [ $fiscal-year-focus ] }
            |}
          }
        }
      }
    |}
  return mw:find($archives:col, $query)
};

(:~
 :
Retrieves facts for the given aspects, fiscal periods and years.

 :
 : @param $aspects an object containing aspects to filter, among which
 :                 xbrl:Concept, xbrl:Entity and xbrl:Period (all optional).
 : @param $fiscal-period-focus the periods (YTD1, YTD2, YTD3, Q1, Q2, Q3 or FY).
 : @param $fiscal-year-focus the years (2012)
 :
 : @return all facts matching the given aspects, fiscal period and year.
 : @deprecated This function has been deprecated in favor of more generic
 :   functions like facts:facts-for and hypercubes:facts.
 :)
declare function
  fiscal-core:facts-for-aspects-and-fiscal-periods-and-years(
    $aspects as object?,
    $fiscal-period-focus as string*,
    $fiscal-year-focus as integer*) as object*
{
  fiscal-core:facts-for-aspects-and-fiscal-periods-and-years(
    $aspects,
    $fiscal-period-focus,
    $fiscal-year-focus,
    ())
};

(:~
 :
Retrieves facts for the given aspects, fiscal periods and years.

 :
 : @param $aspects an object containing aspects to filter, among which
 :                 xbrl:Concept, xbrl:Entity and xbrl:Period (all optional).
 : @param $fiscal-period-focus the periods (YTD1, YTD2, YTD3, Q1, Q2, Q3 or FY) or $fiscal-core:ALL_FISCAL_PERIODS to accept all.
 : @param $fiscal-year-focus the years (2012) or $fiscal-core:ALL_FISCAL_YEARS to accept all.
 : @param $options standard SEC BizQL options.
 :
 : @return all facts matching the given aspects, fiscal period and year.
 : @deprecated This function has been deprecated in favor of more generic
 :   functions like facts:facts-for and hypercubes:facts.
 :)
declare function
  fiscal-core:facts-for-aspects-and-fiscal-periods-and-years(
    $aspects as object?,
    $fiscal-period-focus as string*,
    $fiscal-year-focus as integer*,
    $options as object?) as object*
{
  let $filter as object := {|
    {
      "Filter":
        {|
          if(exists($aspects))
          then { $facts:ASPECTS : $aspects }
          else (),
          if( deep-equal($fiscal-period-focus, $fiscal-core:ALL_FISCAL_PERIODS) and deep-equal($fiscal-year-focus, $fiscal-core:ALL_FISCAL_YEARS))
          then { "Profiles" : { "SEC" : { "Fiscal" : { "$exists" : true } } } }
          else { "Profiles" : { "SEC" : { "Fiscal":
                 {|
                   if(deep-equal($fiscal-period-focus, $fiscal-core:ALL_FISCAL_PERIODS) )
                   then ()
                   else { "Period" : [ $fiscal-period-focus ] },
                   if( deep-equal($fiscal-year-focus, $fiscal-core:ALL_FISCAL_YEARS) )
                   then ()
                   else { "Year": [ $fiscal-year-focus ] }
                 |}
               }}} }
        |}
    },
    if (empty($options.Hypercube))
    then { Hypercube: sec:dimensionless-hypercube(()) }
    else ()
  |}
  let $facts :=
    facts:facts-for(
      facts:merge-objects($filter, $options, true (: giving parameters higher priority :))
    )
  return sec:hide-amended-facts($facts, $options)
};

(:~
 :
Retrieves facts for the given entities, concepts, fiscal periods and years.

 :
 : @param $entities-or-ids a list of entities or their EIDs.
 : @param $concepts a list of concept names (us-gaap:Assets)
 : @param $fiscal-period-focus the periods (YTD1, YTD2, YTD3, Q1, Q2, Q3 or FY) or $fiscal-core:ALL_FISCAL_PERIODS to accept all.
 : @param $fiscal-year-focus the years (2012) or $fiscal-core:ALL_FISCAL_YEARS to accept all.
 :
 : @error entities:INVALID_PARAMETER if the CIK or entity is not valid.
 : @return all facts matching entities, concepts, fiscal periods and years
 : @deprecated This function has been deprecated in favor of more generic
 :   functions like facts:facts-for and hypercubes:facts.
 :)
declare function
  fiscal-core:facts-for-entities-and-concepts-and-fiscal-periods-and-years(
    $entities-or-ids as item*,
    $concepts as string*,
    $fiscal-period-focus as string*,
    $fiscal-year-focus as integer*) as object*
{
  fiscal-core:facts-for-entities-and-concepts-and-fiscal-periods-and-years(
    $entities-or-ids,
    $concepts,
    $fiscal-period-focus,
    $fiscal-year-focus,
    ())
};

(:~
 :
Retrieves facts for the given entities, concepts, fiscal periods and years.

 :
 : @param $entities-or-ids a list of entities or their EIDs.
 : @param $concepts a list of concept names (us-gaap:Assets)
 : @param $fiscal-period-focus the periods (YTD1, YTD2, YTD3, Q1, Q2, Q3 or FY) or $fiscal-core:ALL_FISCAL_PERIODS to accept all.
 : @param $fiscal-year-focus the years (2012) or $fiscal-core:ALL_FISCAL_YEARS to accept all.
 : @param $options standard SEC BizQL options.
 :
 : @error entities:INVALID_PARAMETER if the CIK or entity is not valid.
 : @return all facts matching entities, concepts, fiscal periods and years
 : @deprecated This function has been deprecated in favor of more generic
 :   functions like facts:facts-for and hypercubes:facts.
 :)
declare function
  fiscal-core:facts-for-entities-and-concepts-and-fiscal-periods-and-years(
    $entities-or-ids as item*,
    $concepts as string*,
    $fiscal-period-focus as string*,
    $fiscal-year-focus as integer*,
    $options as object?) as object*
{
  let $entity-ids as xs:string* := $entities-or-ids ! companies:eid($$)
  let $aspects := {|
    if(exists($entity-ids))
    then { "xbrl:Entity": [ $entity-ids ] }
    else (),
    if(exists($concepts))
    then { "xbrl:Concept": [ $concepts ] }
    else ()
  |}
  return
    fiscal-core:facts-for-aspects-and-fiscal-periods-and-years(
      $aspects,
      $fiscal-period-focus,
      $fiscal-year-focus,
      $options)
};

(:~
 :
Retrieves the latest reported fiscal period and year for the
 : supplied entity.

 :
 : @param $entity-or-id an entity or its EID.
 :
 : @return an object with two fields: period and year.
 :)
declare function fiscal-core:latest-reported-fiscal-period(
    $entity-or-id as item) as object?
{
  (
  let $entity-or-id := companies:eid($entity-or-id)
  for $archive in archives:archives-for-entities($entity-or-id)
  let $archive-fiscal-period := fiscal-core:fiscal-period-for-filing($archive)
  let $archive-fiscal-year := fiscal-core:fiscal-year-for-filing($archive)
  group by $archive-fiscal-year
  order by $archive-fiscal-year descending empty least
  return {
    year: $archive-fiscal-year,
    period: switch(true)
            case $archive-fiscal-period = ("Q4", "FY") return "FY"
            case $archive-fiscal-period = ("Q3", "YTD3") return "Q3"
            case $archive-fiscal-period = ("Q2", "YTD2") return "Q2"
            case $archive-fiscal-period = ("Q1", "YTD1") return "Q1"
            default return () }
  )[1]
};

(:~
 :
Retrieves the latest fiscal period and year for the
 : supplied entity.

 :
 : @param $entity-or-id an entity or its EID.
 : @param $type 10-K or 10-Q or FY, YTD1, YTD2, YTD3, Q1, Q2, or Q3, or $fiscal-core:ALL_FISCAL_PERIODS
 : to accept all.
 :
 : @return an object with two fields: period and year.
 :)
declare function fiscal-core:latest-reported-fiscal-period(
    $entity-or-id as item,
    $type as string) as object?
{
  if(deep-equal($type, $fiscal-core:ALL_FISCAL_PERIODS))
  then fiscal-core:latest-reported-fiscal-period($entity-or-id)
  else
  (
  let $type-filter :=
    switch(true)
    case $type = ( "10-K", "FY", "Q4") return "FY"
    case $type eq "10-Q" return ( "Q3", "Q2", "Q1" )
    case $type eq "YTD1" return "Q1"
    case $type eq "YTD2" return "Q2"
    case $type eq "YTD3" return "Q3"
    default return $type
  let $entity-or-id := companies:eid($entity-or-id)
  for $archive in archives:archives-for-entities($entity-or-id)
  let $archive-fiscal-period := fiscal-core:fiscal-period-for-filing($archive)
  let $archive-fiscal-year := fiscal-core:fiscal-year-for-filing($archive)
  where $archive-fiscal-period = $type-filter
  group by $archive-fiscal-year
  order by $archive-fiscal-year descending empty least
  return {
    year: $archive-fiscal-year,
    period:
      switch(true)
      case $archive-fiscal-period = "FY" and ($type eq "10-K" or $type eq "FY") return "FY"
      case $archive-fiscal-period = "FY" and ($type eq "10-K" or $type eq "Q4") return "FY"
      case $archive-fiscal-period = "Q3" and ($type eq "10-Q" or $type eq "Q3") return "Q3"
      case $archive-fiscal-period = "Q2" and ($type eq "10-Q" or $type eq "Q2") return "Q2"
      case $archive-fiscal-period = "Q1" and ($type eq "10-Q" or $type eq "Q1") return "Q1"
      default return () }
  )[1]
};


Use switch cases instead of nested ifs.

Use switch cases instead of nested ifs.

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.");
:)

  switch( false )
  case  $jsd instance of object
  return
    fn:error(QName("jsv", "BadJSoundFormat"),
      "The specified JSON document is not a valid JSound schema: top item not an object.");

  case exists($jsd("$namespace")) and exists($jsd("$types"))
  return
    fn:error(QName("jsv", "BadJSoundFormat"),
      "Not a valid JSound doc: must contain '$types' and '$namespace' keys.");

  case $jsd("$namespace") instance of string
  return
    fn:error(QName("jsv", "BadJSoundFormat"),
      "Not a valid JSound doc: value of '$namespace' must be a string.");

  case $jsd("$namespace") eq $ns
  return
    fn:error(QName("jsv", "Invalid"),
            "Requested type namespace not matching schema document namespace.");

  case $jsd("$types") instance of array
  return
    fn:error(QName("jsv", "BadJSoundFormat"),
              "Not a valid JSound doc: value of '$types' must be an array.");

  default
  return
    jsv:check-types( $jstypes, $jsd("$types") );

  (: 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 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
{
  fn:trace( $type("$name"), "check-atomic-type");

  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);
  fn:trace( $type("$name"), "check-object-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")( fn:trace($k, "Key")) instance of object )
          then
            if( exists( $type("$content")($k)("$type")) )
            then
              jsv:check-type($jstypes, $type("$content")($k)("$type") )
            else
              if( exists( $type("$content")($k)("$kind")) )
              then
                jsv:check-type($jstypes, $type("$content")($k) )
              else
                fn:error(QName("jsv", "BadJSoundFormat"),
                  "Not a valid JSound doc: Key  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 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
{
  fn:trace( $type("$name"), "check-array-type");

  (: check all the contents :)
  (: let $t := fn:trace($type, "Array type:") :)
  let $content := $type("$content")
  let $checkContent :=
    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
          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.")

  let $checkMinLength :=
      if( fn:exists($type("$minLength")) )
      then
        if( $type("$minLength") instance of integer  )
        then true
        else fn:error(QName("jsv", "BadJSoundFormat"),
                      "Not a valid JSound doc: for $kind array, $minLength must be an integer.")
      else true

  let $checkMaxLength :=
      if( fn:exists($type("$maxLength")) )
      then
        if( $type("$maxLength") instance of integer  )
        then true
        else fn:error(QName("jsv", "BadJSoundFormat"),
                      "Not a valid JSound doc: for $kind array, $maxLength must be an integer.")
      else true

  return
    $checkContent and $checkMinLength and $checkMaxLength
};

declare %an:sequential function
jsv:check-union-type($jstypes as object, $type as object) as boolean
{
  fn:trace( $type("$name"), "check-union-type");

  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
              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 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 true
  case "integer" return true
  case "boolean" return true
  case "anyURI"  return true
  default return
  {
    if( map:has-key($jstypes, $type) )
    then
      true
    else
     (: todo: save all refs, check them at the end to see if they exist
     fn:trace($type, "NYI: type ref not yet suported");
     fn:error(QName("jsv", "NYI"), "Type ref: not yet implemented.") :)
     true
  }

  (: todo: apply resolution rules :)
};

declare %an:sequential function
jsv:save-type($jstypes as object, $type as item) as boolean
{
(:
  todo: check if "switch case" below is equivalent

  if( fn:exists( $type("$name") ) )
  then
    if( $type("$name") instance of string )
    then
      if( not( map:set-if-empty($jstypes, $type("$name"), $type) ) )
      then
        fn:error(QName("jsv", "BadJSoundFormat"),
          "Not a valid JSound doc: type '" | $type("$name") | "' already defined.")
      else
        true()
    else
      fn:error(QName("jsv", "BadJSoundFormat"),
          "Not a valid JSound doc: for $type, $name must be a string.")
  else
    false
:)

  switch( false )
  case fn:exists( $type("$name") )
    return false
  case $type("$name") instance of string
    return fn:error(QName("jsv", "BadJSoundFormat"),
                    "Not a valid JSound doc: for $type, $name must be a string.")
  case map:set-if-empty($jstypes, $type("$name"), $type)
    return fn:error(QName("jsv", "BadJSoundFormat"),
          "Not a valid JSound doc: type '" || $type("$name") || "' already defined.")
  default return true()
};

(: 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 :)

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

    default return
        fn:error(QName("jsv", "Invalid"),
          fn:concat("Type ref not defined or a built-in type: ", $type) )
};

declare function
jsv:validate-atomic-type($jstypes as object, $type as object, $instance as item) as boolean
{
  fn:trace( $type("$name"), "validate-atomic-type");
  (: 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
        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
        jsv:validate-enumeration($jstypes, $type, $instance)
      else
        fn:error( QName("jsv", "Invalid"), "Expected string value")

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

    case "boolean" return
      if ( $instance instance of boolean )
      then
        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
        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
    true
};

declare function
jsv:validate-object-type($jstypes as object, $type as object, $instance as item) as boolean
{
  fn:trace( $type("$name"), "validate-object-type");

  variable $open := $type("$open");

  if ( $instance instance of object )
  then
    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 and optional 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
          jsv:value-except(jn:keys($type("$content")), jn:keys($instance))
        satisfies
          let $optional := $type("$content")($k)("$optional")
          return
            if( exists($optional) and $optional eq true )
            then true
            else
              fn:error(QName("jsv", "Invalid"),
                fn:concat("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) :)
          jsv:value-except(jn:keys($type("$content")), jn:keys($instance))
        satisfies
          let $optional := $type("$content")($k)("$optional")
          return
            if( exists($optional) and $optional eq true )
            then true
            else
              fn:error(QName("jsv", "Invalid"),
                fn:concat("Required key not present in instance: ", $k) )
      )
  else
    fn:error( QName("jsv", "Invalid"),
      fn:concat("Instance not an object specified by type: ", jsv:get-type-name($type)) )
};

declare function
jsv:validate-array-type($jstypes as object, $type as object, $instance as item) as boolean
{
  fn:trace( $type("$name"), "validate-array-type");

  if( $instance instance of array )
  then
    (: check all the contents :)
    let $content := $type("$content")
    let $validateContent :=
        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.")
    let $validateMinLength :=
      if( fn:exists($type("$minLength")) )
      then
        if ( $type("$minLength") le size($instance) )
        then true
        else fn:error(QName("jsv", "Invalid"),
                      "Invalid array size, smaller then $minLength facet constraint.")
      else true
    let $validateMaxLength :=
      if( fn:exists($type("$maxLength")) )
      then
        if( $type("$maxLength") ge size($instance) )
        then true
        else fn:error(QName("jsv", "Invalid"),
                      "Invalid array size, bigger than $maxLength facet constraint.")
      else true
    return
      $validateContent and $validateMinLength and $validateMaxLength
  else
    fn:error(QName("jsv", "Invalid"),
      fn:concat("Instance not an array for type: ", jsv:get-type-name($type)) )

  (: 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
              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
          {
             false
          }
      satisfies $i eq true
    )
    then true
    else
      fn:error(QName("jsv", "Invalid"),
        fn:concat("Instance not valid for union type: ", jsv:get-type-name($type)) )
};

(:---- Util functions ---------:)

declare function
jsv:get-type-name($type as object) as string
{
  let $name := $type("$name")
  return
    if ( exists($name) and $name instance of string )
    then
      $name
    else
      "N/A"
};

declare function
jsv:value-except($arg1 as xs:anyAtomicType*, $arg2 as xs:anyAtomicType*)
  as xs:anyAtomicType*
{
  (: fn:distinct-values( $arg1[ not($$ eq $arg2) ] ) :)
  fn:distinct-values(
    for $i in $arg1
    where not( some $x in $arg2 satisfies $i eq $x )
    return $i
  )
};

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 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
{
  fn:trace( $type("$name"), "check-atomic-type");

  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);
  fn:trace( $type("$name"), "check-object-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")( fn:trace($k, "Key")) instance of object )
          then
            if( exists( $type("$content")($k)("$type")) )
            then
              jsv:check-type($jstypes, $type("$content")($k)("$type") )
            else
              if( exists( $type("$content")($k)("$kind")) )
              then
                jsv:check-type($jstypes, $type("$content")($k) )
              else
                fn:error(QName("jsv", "BadJSoundFormat"),
                  "Not a valid JSound doc: Key  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 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
{
  fn:trace( $type("$name"), "check-array-type");

  (: check all the contents :)
  (: let $t := fn:trace($type, "Array type:") :)
  let $content := $type("$content")
  let $checkContent :=
    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
          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.")

  let $checkMinLength :=
      if( fn:exists($type("$minLength")) )
      then
        if( $type("$minLength") instance of integer  )
        then true
        else fn:error(QName("jsv", "BadJSoundFormat"),
                      "Not a valid JSound doc: for $kind array, $minLength must be an integer.")
      else true

  let $checkMaxLength :=
      if( fn:exists($type("$maxLength")) )
      then
        if( $type("$maxLength") instance of integer  )
        then true
        else fn:error(QName("jsv", "BadJSoundFormat"),
                      "Not a valid JSound doc: for $kind array, $maxLength must be an integer.")
      else true

  return
    $checkContent and $checkMinLength and $checkMaxLength
};

declare %an:sequential function
jsv:check-union-type($jstypes as object, $type as object) as boolean
{
  fn:trace( $type("$name"), "check-union-type");

  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
              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 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 true
  case "integer" return true
  case "boolean" return true
  case "anyURI"  return true
  default return
  {
    if( map:has-key($jstypes, $type) )
    then
      true
    else
     (: todo: save all refs, check them at the end to see if they exist
     fn:trace($type, "NYI: type ref not yet suported");
     fn:error(QName("jsv", "NYI"), "Type ref: not yet implemented.") :)
     true
  }

  (: todo: apply resolution rules :)
};

declare %an:sequential function
jsv:save-type($jstypes as object, $type as item) as boolean
{
(:
  todo: check if "switch case" below is equivalent

  if( fn:exists( $type("$name") ) )
  then
    if( $type("$name") instance of string )
    then
      if( not( map:set-if-empty($jstypes, $type("$name"), $type) ) )
      then
        fn:error(QName("jsv", "BadJSoundFormat"),
          "Not a valid JSound doc: type '" | $type("$name") | "' already defined.")
      else
        true()
    else
      fn:error(QName("jsv", "BadJSoundFormat"),
          "Not a valid JSound doc: for $type, $name must be a string.")
  else
    false
:)

  switch( false )
  case fn:exists( $type("$name") )
    return false
  case $type("$name") instance of string
    return fn:error(QName("jsv", "BadJSoundFormat"),
                    "Not a valid JSound doc: for $type, $name must be a string.")
  case map:set-if-empty($jstypes, $type("$name"), $type)
    return fn:error(QName("jsv", "BadJSoundFormat"),
          "Not a valid JSound doc: type '" || $type("$name") || "' already defined.")
  default return true()
};

(: 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 :)

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

    default return
        fn:error(QName("jsv", "Invalid"),
          fn:concat("Type ref not defined or a built-in type: ", $type) )
};

declare function
jsv:validate-atomic-type($jstypes as object, $type as object, $instance as item) as boolean
{
  fn:trace( $type("$name"), "validate-atomic-type");
  (: 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
        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
        jsv:validate-enumeration($jstypes, $type, $instance)
      else
        fn:error( QName("jsv", "Invalid"), "Expected string value")

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

    case "boolean" return
      if ( $instance instance of boolean )
      then
        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
        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
    true
};

declare function
jsv:validate-object-type($jstypes as object, $type as object, $instance as item) as boolean
{
  fn:trace( $type("$name"), "validate-object-type");

  variable $open := $type("$open");

  if ( $instance instance of object )
  then
    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 and optional 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
          jsv:value-except(jn:keys($type("$content")), jn:keys($instance))
        satisfies
          let $optional := $type("$content")($k)("$optional")
          return
            if( exists($optional) and $optional eq true )
            then true
            else
              fn:error(QName("jsv", "Invalid"),
                fn:concat("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) :)
          jsv:value-except(jn:keys($type("$content")), jn:keys($instance))
        satisfies
          let $optional := $type("$content")($k)("$optional")
          return
            if( exists($optional) and $optional eq true )
            then true
            else
              fn:error(QName("jsv", "Invalid"),
                fn:concat("Required key not present in instance: ", $k) )
      )
  else
    fn:error( QName("jsv", "Invalid"),
      fn:concat("Instance not an object specified by type: ", jsv:get-type-name($type)) )
};

declare function
jsv:validate-array-type($jstypes as object, $type as object, $instance as item) as boolean
{
  fn:trace( $type("$name"), "validate-array-type");

  if( $instance instance of array )
  then
    (: check all the contents :)
    let $content := $type("$content")
    let $validateContent :=
        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.")
    let $validateMinLength :=
      if( fn:exists($type("$minLength")) )
      then
        if ( $type("$minLength") le size($instance) )
        then true
        else fn:error(QName("jsv", "Invalid"),
                      "Invalid array size, smaller then $minLength facet constraint.")
      else true
    let $validateMaxLength :=
      if( fn:exists($type("$maxLength")) )
      then
        if( $type("$maxLength") ge size($instance) )
        then true
        else fn:error(QName("jsv", "Invalid"),
                      "Invalid array size, bigger than $maxLength facet constraint.")
      else true
    return
      $validateContent and $validateMinLength and $validateMaxLength
  else
    fn:error(QName("jsv", "Invalid"),
      fn:concat("Instance not an array for type: ", jsv:get-type-name($type)) )

  (: 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
              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
          {
             false
          }
      satisfies $i eq true
    )
    then true
    else
      fn:error(QName("jsv", "Invalid"),
        fn:concat("Instance not valid for union type: ", jsv:get-type-name($type)) )
};

(:---- Util functions ---------:)

declare function
jsv:get-type-name($type as object) as string
{
  let $name := $type("$name")
  return
    if ( exists($name) and $name instance of string )
    then
      $name
    else
      "N/A"
};

declare function
jsv:value-except($arg1 as xs:anyAtomicType*, $arg2 as xs:anyAtomicType*)
  as xs:anyAtomicType*
{
  (: fn:distinct-values( $arg1[ not($$ eq $arg2) ] ) :)
  fn:distinct-values(
    for $i in $arg1
    where not( some $x in $arg2 satisfies $i eq $x )
    return $i
  )
};


:green_heart: Fix test.

:green_heart: Fix test.

import module namespace response = "http://www.28msec.com/modules/http-response";
import module namespace request = "http://www.28msec.com/modules/http-request";
import module namespace test = "http://apps.28.io/test";
import module namespace config = "http://apps.28.io/config";

declare variable $local:expected as object :=
    {
      "dow30" : [ "http://www.sec.gov/CIK 0000066740", "http://www.sec.gov/CIK 0000004962", "http://www.sec.gov/CIK 0000732717", "http://www.sec.gov/CIK 0000012927", "http://www.sec.gov/CIK 0000018230", "http://www.sec.gov/CIK 0000093410", "http://www.sec.gov/CIK 0000858877", "http://www.sec.gov/CIK 0000021344", "http://www.sec.gov/CIK 0000030554", "http://www.sec.gov/CIK 0000034088", "http://www.sec.gov/CIK 0000040545", "http://www.sec.gov/CIK 0000886982", "http://www.sec.gov/CIK 0000354950", "http://www.sec.gov/CIK 0000050863", "http://www.sec.gov/CIK 0000051143", "http://www.sec.gov/CIK 0000200406", "http://www.sec.gov/CIK 0000019617", "http://www.sec.gov/CIK 0000063908", "http://www.sec.gov/CIK 0000789019", "http://www.sec.gov/CIK 0000310158", "http://www.sec.gov/CIK 0000320187", "http://www.sec.gov/CIK 0000078003", "http://www.sec.gov/CIK 0000080424", "http://www.sec.gov/CIK 0000086312", "http://www.sec.gov/CIK 0000101829", "http://www.sec.gov/CIK 0000731766", "http://www.sec.gov/CIK 0000732712", "http://www.sec.gov/CIK 0001403161", "http://www.sec.gov/CIK 0000104169", "http://www.sec.gov/CIK 0001001039" ],
      "generic" : [ "http://www.sec.gov/CIK 0000004962" ],
      "cik" : [ "http://www.sec.gov/CIK 0000004962" ],
      "ticker" : [ "http://www.sec.gov/CIK 0000104169" ],
      "ticker2" : [ "http://www.sec.gov/CIK 0000021344", "http://www.sec.gov/CIK 0000104169" ],
      "sic" : [ "http://www.sec.gov/CIK 0001585644", "http://www.sec.gov/CIK 0001608249", "http://www.sec.gov/CIK 0001023731", "http://www.sec.gov/CIK 0001089511", "http://www.sec.gov/CIK 0000104777", "http://www.sec.gov/CIK 0001047919", "http://www.sec.gov/CIK 0000732717", "http://www.sec.gov/CIK 0000879585", "http://www.sec.gov/CIK 0001122063", "http://www.sec.gov/CIK 0001104358", "http://www.sec.gov/CIK 0001205727", "http://www.sec.gov/CIK 0000018926", "http://www.sec.gov/CIK 0000716133", "http://www.sec.gov/CIK 0001161706", "http://www.sec.gov/CIK 0001457737", "http://www.sec.gov/CIK 0001304421", "http://www.sec.gov/CIK 0001086740", "http://www.sec.gov/CIK 0001352819", "http://www.sec.gov/CIK 0001084384", "http://www.sec.gov/CIK 0001101239", "http://www.sec.gov/CIK 0000766561", "http://www.sec.gov/CIK 0001062613", "http://www.sec.gov/CIK 0001504678", "http://www.sec.gov/CIK 0000020520", "http://www.sec.gov/CIK 0001071411", "http://www.sec.gov/CIK 0000075679", "http://www.sec.gov/CIK 0000808461", "http://www.sec.gov/CIK 0001061322", "http://www.sec.gov/CIK 0000746210", "http://www.sec.gov/CIK 0001384929", "http://www.sec.gov/CIK 0001315255", "http://www.sec.gov/CIK 0001006837", "http://www.sec.gov/CIK 0001005731", "http://www.sec.gov/CIK 0001041954", "http://www.sec.gov/CIK 0001292653", "http://www.sec.gov/CIK 0001160142", "http://www.sec.gov/CIK 0001084752", "http://www.sec.gov/CIK 0000794323", "http://www.sec.gov/CIK 0001130888", "http://www.sec.gov/CIK 0001520744", "http://www.sec.gov/CIK 0001005699", "http://www.sec.gov/CIK 0000732412", "http://www.sec.gov/CIK 0000071557", "http://www.sec.gov/CIK 0001328571", "http://www.sec.gov/CIK 0001288359", "http://www.sec.gov/CIK 0001372041", "http://www.sec.gov/CIK 0000090721", "http://www.sec.gov/CIK 0001558294", "http://www.sec.gov/CIK 0001405660", "http://www.sec.gov/CIK 0001458704", "http://www.sec.gov/CIK 0001143238", "http://www.sec.gov/CIK 0001037949", "http://www.sec.gov/CIK 0000068622", "http://www.sec.gov/CIK 0001341319", "http://www.sec.gov/CIK 0000354963", "http://www.sec.gov/CIK 0000101830", "http://www.sec.gov/CIK 0000943117", "http://www.sec.gov/CIK 0001560158", "http://www.sec.gov/CIK 0001051512", "http://www.sec.gov/CIK 0001101688", "http://www.sec.gov/CIK 0001499355", "http://www.sec.gov/CIK 0000732712", "http://www.sec.gov/CIK 0001270400", "http://www.sec.gov/CIK 0001322791", "http://www.sec.gov/CIK 0001353505", "http://www.sec.gov/CIK 0001272830", "http://www.sec.gov/CIK 0001562636", "http://www.sec.gov/CIK 0001282266", "http://www.sec.gov/CIK 0000919742", "http://www.sec.gov/CIK 0001111634", "http://www.sec.gov/CIK 0001448962", "http://www.sec.gov/CIK 0001502756", "http://www.sec.gov/CIK 0001373444", "http://www.sec.gov/CIK 0001057758" ],
      "sic-dow30": [ "http://www.sec.gov/CIK 0000732717", "http://www.sec.gov/CIK 0000732712" ],
      "mixed" : [ "http://www.sec.gov/CIK 0001585644", "http://www.sec.gov/CIK 0001608249", "http://www.sec.gov/CIK 0001023731", "http://www.sec.gov/CIK 0001089511", "http://www.sec.gov/CIK 0000104777", "http://www.sec.gov/CIK 0001047919", "http://www.sec.gov/CIK 0000004962", "http://www.sec.gov/CIK 0000732717", "http://www.sec.gov/CIK 0000879585", "http://www.sec.gov/CIK 0001122063", "http://www.sec.gov/CIK 0001104358", "http://www.sec.gov/CIK 0001205727", "http://www.sec.gov/CIK 0000018926", "http://www.sec.gov/CIK 0000716133", "http://www.sec.gov/CIK 0000021344", "http://www.sec.gov/CIK 0001161706", "http://www.sec.gov/CIK 0001457737", "http://www.sec.gov/CIK 0001304421", "http://www.sec.gov/CIK 0001086740", "http://www.sec.gov/CIK 0001352819", "http://www.sec.gov/CIK 0001084384", "http://www.sec.gov/CIK 0001101239", "http://www.sec.gov/CIK 0000766561", "http://www.sec.gov/CIK 0001062613", "http://www.sec.gov/CIK 0001504678", "http://www.sec.gov/CIK 0000020520", "http://www.sec.gov/CIK 0001071411", "http://www.sec.gov/CIK 0000075679", "http://www.sec.gov/CIK 0000808461", "http://www.sec.gov/CIK 0001061322", "http://www.sec.gov/CIK 0000746210", "http://www.sec.gov/CIK 0001384929", "http://www.sec.gov/CIK 0001315255", "http://www.sec.gov/CIK 0001006837", "http://www.sec.gov/CIK 0001005731", "http://www.sec.gov/CIK 0001041954", "http://www.sec.gov/CIK 0001292653", "http://www.sec.gov/CIK 0001160142", "http://www.sec.gov/CIK 0001084752", "http://www.sec.gov/CIK 0000794323", "http://www.sec.gov/CIK 0001130888", "http://www.sec.gov/CIK 0001520744", "http://www.sec.gov/CIK 0001005699", "http://www.sec.gov/CIK 0000732412", "http://www.sec.gov/CIK 0000071557", "http://www.sec.gov/CIK 0001328571", "http://www.sec.gov/CIK 0001288359", "http://www.sec.gov/CIK 0001372041", "http://www.sec.gov/CIK 0000090721", "http://www.sec.gov/CIK 0001558294", "http://www.sec.gov/CIK 0001405660", "http://www.sec.gov/CIK 0001458704", "http://www.sec.gov/CIK 0001143238", "http://www.sec.gov/CIK 0001037949", "http://www.sec.gov/CIK 0000068622", "http://www.sec.gov/CIK 0001341319", "http://www.sec.gov/CIK 0000354963", "http://www.sec.gov/CIK 0000101830", "http://www.sec.gov/CIK 0000943117", "http://www.sec.gov/CIK 0001560158", "http://www.sec.gov/CIK 0001051512", "http://www.sec.gov/CIK 0001101688", "http://www.sec.gov/CIK 0001499355", "http://www.sec.gov/CIK 0000732712", "http://www.sec.gov/CIK 0001270400", "http://www.sec.gov/CIK 0001322791", "http://www.sec.gov/CIK 0001353505", "http://www.sec.gov/CIK 0001272830", "http://www.sec.gov/CIK 0001562636", "http://www.sec.gov/CIK 0000104169", "http://www.sec.gov/CIK 0001282266", "http://www.sec.gov/CIK 0000919742", "http://www.sec.gov/CIK 0001111634", "http://www.sec.gov/CIK 0001448962", "http://www.sec.gov/CIK 0001502756", "http://www.sec.gov/CIK 0001373444", "http://www.sec.gov/CIK 0001057758" ],
      "mixed-dow30": [ "http://www.sec.gov/CIK 0000004962", "http://www.sec.gov/CIK 0000732717", "http://www.sec.gov/CIK 0000021344", "http://www.sec.gov/CIK 0000732712", "http://www.sec.gov/CIK 0000104169" ]
    };

declare %an:nondeterministic function local:test-example1() as item
{
  let $expected :=
  [ {
    "_id" : "http://www.sec.gov/CIK 0000021344",
    "EID" : "http://www.sec.gov/CIK 0000021344",
    "Archives" : "http://" || request:server-name() || ":" || request:server-port() ||
      "/v1/_queries/public/api/filings.jq?_method=POST&token=" || $config:test-token || "&eid=http%3A%2F%2Fwww.sec.gov%2FCIK%200000021344&format=&profile-name=sec&fiscalYear=ALL&fiscalPeriod=ALL",
    "Profiles" : {
      "SEC" : {
        "Name" : "SEC",
        "CompanyName" : "COCA COLA CO",
        "CompanyType" : "Corporation",
        "CIK" : "0000021344",
        "SIC" : "2080",
        "SICDescription" : "BEVERAGES",
        "SICGroup" : 9,
        "Taxonomy" : "ci",
        "Sector" : "Food/Kindred Products",
        "Tickers" : [ "ko" ],
        "Tags" : [ "FORTUNE100", "DOW30", "RUSSELL1000", "SP500" ],
        "IsTrust" : false
      }
    }
  } ]
  let $endpoint := "entities"
  let $params := {cik:"21344"}
  let $request := test:invoke($endpoint, $params)
  let $status as integer := $request[1]
  let $actual as array? := $request[2].Entities
  return test:assert-deep-equal($expected, $actual, $status, test:url($endpoint, $params))
};

declare %an:nondeterministic function local:test-entities(
    $expected as array,
    $params as object) as item
{
    let $endpoint := "entities"
    let $request := test:invoke($endpoint, $params)
    let $actual as array := [ $request[2].Entities[]._id ]
    let $status as integer := $request[1]
    return test:assert-eq-array($expected, $actual, $status, test:url($endpoint, $params))
};

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
};

let $dow30 := test:is-dow30()
return local:check({
    dow30: local:test-entities($local:expected.dow30, {tag:"DOW30"}),
    generic: local:test-entities($local:expected.generic, {eid:"http://www.sec.gov/CIK 0000004962", profile-name:"generic"}),
    cik: local:test-entities($local:expected.cik, {cik:"4962"}),
    ticker: local:test-entities($local:expected.ticker, {ticker:"wmt"}),
    ticker2: local:test-entities($local:expected.ticker2, {ticker:["wmt","ko"]}),
    sic: local:test-entities(if($dow30) then $local:expected.sic-dow30 else $local:expected.sic, {sic:"4813"}),
    mixed: local:test-entities(if($dow30) then $local:expected.mixed-dow30 else $local:expected.mixed, {cik:"4962", sic:"4813", ticker:[ "wmt", "ko" ]}),
    example1: local:test-example1()
})

import module namespace response = "http://www.28msec.com/modules/http-response";
import module namespace request = "http://www.28msec.com/modules/http-request";
import module namespace test = "http://apps.28.io/test";
import module namespace config = "http://apps.28.io/config";

declare variable $local:expected as object :=
    {
      "dow30" : [ "http://www.sec.gov/CIK 0000066740", "http://www.sec.gov/CIK 0000004962", "http://www.sec.gov/CIK 0000732717", "http://www.sec.gov/CIK 0000012927", "http://www.sec.gov/CIK 0000018230", "http://www.sec.gov/CIK 0000093410", "http://www.sec.gov/CIK 0000858877", "http://www.sec.gov/CIK 0000021344", "http://www.sec.gov/CIK 0000030554", "http://www.sec.gov/CIK 0000034088", "http://www.sec.gov/CIK 0000040545", "http://www.sec.gov/CIK 0000886982", "http://www.sec.gov/CIK 0000354950", "http://www.sec.gov/CIK 0000050863", "http://www.sec.gov/CIK 0000051143", "http://www.sec.gov/CIK 0000200406", "http://www.sec.gov/CIK 0000019617", "http://www.sec.gov/CIK 0000063908", "http://www.sec.gov/CIK 0000789019", "http://www.sec.gov/CIK 0000310158", "http://www.sec.gov/CIK 0000320187", "http://www.sec.gov/CIK 0000078003", "http://www.sec.gov/CIK 0000080424", "http://www.sec.gov/CIK 0000086312", "http://www.sec.gov/CIK 0000101829", "http://www.sec.gov/CIK 0000731766", "http://www.sec.gov/CIK 0000732712", "http://www.sec.gov/CIK 0001403161", "http://www.sec.gov/CIK 0000104169", "http://www.sec.gov/CIK 0001001039" ],
      "generic" : [ "http://www.sec.gov/CIK 0000004962" ],
      "cik" : [ "http://www.sec.gov/CIK 0000004962" ],
      "ticker" : [ "http://www.sec.gov/CIK 0000104169" ],
      "ticker2" : [ "http://www.sec.gov/CIK 0000021344", "http://www.sec.gov/CIK 0000104169" ],
      "sic" : [ "http://www.sec.gov/CIK 0001585644", "http://www.sec.gov/CIK 0001608249", "http://www.sec.gov/CIK 0001023731", "http://www.sec.gov/CIK 0001089511", "http://www.sec.gov/CIK 0000104777", "http://www.sec.gov/CIK 0001047919", "http://www.sec.gov/CIK 0000732717", "http://www.sec.gov/CIK 0000879585", "http://www.sec.gov/CIK 0001122063", "http://www.sec.gov/CIK 0001104358", "http://www.sec.gov/CIK 0001205727", "http://www.sec.gov/CIK 0000018926", "http://www.sec.gov/CIK 0000716133", "http://www.sec.gov/CIK 0001161706", "http://www.sec.gov/CIK 0001457737", "http://www.sec.gov/CIK 0001304421", "http://www.sec.gov/CIK 0001086740", "http://www.sec.gov/CIK 0001352819", "http://www.sec.gov/CIK 0001084384", "http://www.sec.gov/CIK 0001101239", "http://www.sec.gov/CIK 0000766561", "http://www.sec.gov/CIK 0001062613", "http://www.sec.gov/CIK 0001504678", "http://www.sec.gov/CIK 0000020520", "http://www.sec.gov/CIK 0001071411", "http://www.sec.gov/CIK 0000075679", "http://www.sec.gov/CIK 0000808461", "http://www.sec.gov/CIK 0001061322", "http://www.sec.gov/CIK 0000746210", "http://www.sec.gov/CIK 0001384929", "http://www.sec.gov/CIK 0001315255", "http://www.sec.gov/CIK 0001006837", "http://www.sec.gov/CIK 0001005731", "http://www.sec.gov/CIK 0001041954", "http://www.sec.gov/CIK 0001292653", "http://www.sec.gov/CIK 0001160142", "http://www.sec.gov/CIK 0001084752", "http://www.sec.gov/CIK 0000794323", "http://www.sec.gov/CIK 0001130888", "http://www.sec.gov/CIK 0001520744", "http://www.sec.gov/CIK 0001005699", "http://www.sec.gov/CIK 0000732412", "http://www.sec.gov/CIK 0000071557", "http://www.sec.gov/CIK 0001328571", "http://www.sec.gov/CIK 0001288359", "http://www.sec.gov/CIK 0001372041", "http://www.sec.gov/CIK 0000090721", "http://www.sec.gov/CIK 0001558294", "http://www.sec.gov/CIK 0001405660", "http://www.sec.gov/CIK 0001458704", "http://www.sec.gov/CIK 0001143238", "http://www.sec.gov/CIK 0001037949", "http://www.sec.gov/CIK 0000068622", "http://www.sec.gov/CIK 0001341319", "http://www.sec.gov/CIK 0000354963", "http://www.sec.gov/CIK 0000101830", "http://www.sec.gov/CIK 0000943117", "http://www.sec.gov/CIK 0001560158", "http://www.sec.gov/CIK 0001051512", "http://www.sec.gov/CIK 0001101688", "http://www.sec.gov/CIK 0001499355", "http://www.sec.gov/CIK 0000732712", "http://www.sec.gov/CIK 0001270400", "http://www.sec.gov/CIK 0001322791", "http://www.sec.gov/CIK 0001353505", "http://www.sec.gov/CIK 0001272830", "http://www.sec.gov/CIK 0001562636", "http://www.sec.gov/CIK 0001282266", "http://www.sec.gov/CIK 0000919742", "http://www.sec.gov/CIK 0001111634", "http://www.sec.gov/CIK 0001448962", "http://www.sec.gov/CIK 0001502756", "http://www.sec.gov/CIK 0001373444", "http://www.sec.gov/CIK 0001057758" ],
      "sic-dow30": [ "http://www.sec.gov/CIK 0000732717", "http://www.sec.gov/CIK 0000732712" ],
      "mixed" : [ "http://www.sec.gov/CIK 0001585644", "http://www.sec.gov/CIK 0001608249", "http://www.sec.gov/CIK 0001023731", "http://www.sec.gov/CIK 0001089511", "http://www.sec.gov/CIK 0000104777", "http://www.sec.gov/CIK 0001047919", "http://www.sec.gov/CIK 0000004962", "http://www.sec.gov/CIK 0000732717", "http://www.sec.gov/CIK 0000879585", "http://www.sec.gov/CIK 0001122063", "http://www.sec.gov/CIK 0001104358", "http://www.sec.gov/CIK 0001205727", "http://www.sec.gov/CIK 0000018926", "http://www.sec.gov/CIK 0000716133", "http://www.sec.gov/CIK 0000021344", "http://www.sec.gov/CIK 0001161706", "http://www.sec.gov/CIK 0001457737", "http://www.sec.gov/CIK 0001304421", "http://www.sec.gov/CIK 0001086740", "http://www.sec.gov/CIK 0001352819", "http://www.sec.gov/CIK 0001084384", "http://www.sec.gov/CIK 0001101239", "http://www.sec.gov/CIK 0000766561", "http://www.sec.gov/CIK 0001062613", "http://www.sec.gov/CIK 0001504678", "http://www.sec.gov/CIK 0000020520", "http://www.sec.gov/CIK 0001071411", "http://www.sec.gov/CIK 0000075679", "http://www.sec.gov/CIK 0000808461", "http://www.sec.gov/CIK 0001061322", "http://www.sec.gov/CIK 0000746210", "http://www.sec.gov/CIK 0001384929", "http://www.sec.gov/CIK 0001315255", "http://www.sec.gov/CIK 0001006837", "http://www.sec.gov/CIK 0001005731", "http://www.sec.gov/CIK 0001041954", "http://www.sec.gov/CIK 0001292653", "http://www.sec.gov/CIK 0001160142", "http://www.sec.gov/CIK 0001084752", "http://www.sec.gov/CIK 0000794323", "http://www.sec.gov/CIK 0001130888", "http://www.sec.gov/CIK 0001520744", "http://www.sec.gov/CIK 0001005699", "http://www.sec.gov/CIK 0000732412", "http://www.sec.gov/CIK 0000071557", "http://www.sec.gov/CIK 0001328571", "http://www.sec.gov/CIK 0001288359", "http://www.sec.gov/CIK 0001372041", "http://www.sec.gov/CIK 0000090721", "http://www.sec.gov/CIK 0001558294", "http://www.sec.gov/CIK 0001405660", "http://www.sec.gov/CIK 0001458704", "http://www.sec.gov/CIK 0001143238", "http://www.sec.gov/CIK 0001037949", "http://www.sec.gov/CIK 0000068622", "http://www.sec.gov/CIK 0001341319", "http://www.sec.gov/CIK 0000354963", "http://www.sec.gov/CIK 0000101830", "http://www.sec.gov/CIK 0000943117", "http://www.sec.gov/CIK 0001560158", "http://www.sec.gov/CIK 0001051512", "http://www.sec.gov/CIK 0001101688", "http://www.sec.gov/CIK 0001499355", "http://www.sec.gov/CIK 0000732712", "http://www.sec.gov/CIK 0001270400", "http://www.sec.gov/CIK 0001322791", "http://www.sec.gov/CIK 0001353505", "http://www.sec.gov/CIK 0001272830", "http://www.sec.gov/CIK 0001562636", "http://www.sec.gov/CIK 0000104169", "http://www.sec.gov/CIK 0001282266", "http://www.sec.gov/CIK 0000919742", "http://www.sec.gov/CIK 0001111634", "http://www.sec.gov/CIK 0001448962", "http://www.sec.gov/CIK 0001502756", "http://www.sec.gov/CIK 0001373444", "http://www.sec.gov/CIK 0001057758" ],
      "mixed-dow30": [ "http://www.sec.gov/CIK 0000004962", "http://www.sec.gov/CIK 0000732717", "http://www.sec.gov/CIK 0000021344", "http://www.sec.gov/CIK 0000732712", "http://www.sec.gov/CIK 0000104169" ]
    };

declare %an:nondeterministic function local:test-example1() as item
{
  let $expected :=
  [ {
    "_id" : "http://www.sec.gov/CIK 0000021344",
    "Archives" : "http://" || request:server-name() || ":" || request:server-port() ||
      "/v1/_queries/public/api/filings.jq?_method=POST&token=" || $config:test-token || "&eid=http%3A%2F%2Fwww.sec.gov%2FCIK%200000021344&format=&profile-name=sec&fiscalYear=ALL&fiscalPeriod=ALL",
    "Profiles" : {
      "SEC" : {
        "Name" : "SEC",
        "CompanyName" : "COCA COLA CO",
        "CompanyType" : "Corporation",
        "CIK" : "0000021344",
        "SIC" : "2080",
        "SICDescription" : "BEVERAGES",
        "SICGroup" : 9,
        "Taxonomy" : "ci",
        "Sector" : "Food/Kindred Products",
        "Tickers" : [ "ko" ],
        "Tags" : [ "FORTUNE100", "DOW30", "RUSSELL1000", "SP500" ],
        "IsTrust" : false
      }
    }
  } ]
  let $endpoint := "entities"
  let $params := {cik:"21344"}
  let $request := test:invoke($endpoint, $params)
  let $status as integer := $request[1]
  let $actual as array? := $request[2].Entities
  return test:assert-deep-equal($expected, $actual, $status, test:url($endpoint, $params))
};

declare %an:nondeterministic function local:test-entities(
    $expected as array,
    $params as object) as item
{
    let $endpoint := "entities"
    let $request := test:invoke($endpoint, $params)
    let $actual as array := [ $request[2].Entities[]._id ]
    let $status as integer := $request[1]
    return test:assert-eq-array($expected, $actual, $status, test:url($endpoint, $params))
};

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
};

let $dow30 := test:is-dow30()
return local:check({
    dow30: local:test-entities($local:expected.dow30, {tag:"DOW30"}),
    generic: local:test-entities($local:expected.generic, {eid:"http://www.sec.gov/CIK 0000004962", profile-name:"generic"}),
    cik: local:test-entities($local:expected.cik, {cik:"4962"}),
    ticker: local:test-entities($local:expected.ticker, {ticker:"wmt"}),
    ticker2: local:test-entities($local:expected.ticker2, {ticker:["wmt","ko"]}),
    sic: local:test-entities(if($dow30) then $local:expected.sic-dow30 else $local:expected.sic, {sic:"4813"}),
    mixed: local:test-entities(if($dow30) then $local:expected.mixed-dow30 else $local:expected.mixed, {cik:"4962", sic:"4813", ticker:[ "wmt", "ko" ]}),
    example1: local:test-example1()
})