:lipstick: Fix warning.

:lipstick: Fix warning.

jsoniq version "1.0";
module namespace test = "http://apps.28.io/test";
import module namespace backend = "http://apps.28.io/backend";
import module namespace http-client = "http://zorba.io/modules/http-client";
import module namespace request = "http://www.28msec.com/modules/http-request";
import module namespace response = "http://www.28msec.com/modules/http-response";
import module namespace credentials = "http://www.28msec.com/modules/credentials";
import module namespace functx = "http://www.functx.com";

declare function test:is-dow30() as boolean{
  contains(credentials:credentials("MongoDB", "xbrl").db, "dow30")
};

declare %an:nondeterministic function test:invoke($endpoint as string, $parameters as object) as item*
{
  let $url as string:= backend:url($endpoint, $parameters, true)
  let $response as object := http-client:get($url)
  return ($response.status, parse-json($response.body.content))
};

declare %an:nondeterministic function test:invoke-xml($endpoint as string, $parameters as object) as item*
{
  let $url as string:= backend:url($endpoint, $parameters, true)
  let $response as object := http-client:get($url)
  return ($response.status, parse-xml($response.body.content))
};

declare %an:sequential function test:invoke-body($endpoint as string, $parameters as object, $body as string) as item*
{
  let $url as string:= backend:url($endpoint, $parameters, true)
  let $response as object := http-client:post($url, $body, "application/x-www-form-urlencoded")
  return ($response.status, parse-json($response.body.content))
};

declare %an:nondeterministic function test:invoke-raw($endpoint as string, $parameters as object) as object
{
  let $url as string:= backend:url($endpoint, $parameters, true)
  return http-client:get($url)
};

declare %an:nondeterministic function test:invoke-public($endpoint as string, $parameters as object) as item*
{
  let $url as string:= backend:url($endpoint, $parameters, true)
  let $response as object := http-client:get($url)
  return ($response.status, parse-json($response.body.content))
};

declare %an:nondeterministic function test:get-expected-result(
  $expected-file as string
) as item*
{
  parse-json(
    http-client:get("http://" || request:server-name() || ":" || request:server-port() ||
                    "/test/" || $expected-file).body.content)
};

declare %an:nondeterministic function test:get-expected-result-xml(
  $expected-file as string
) as item*
{
  parse-xml(
    http-client:get("http://" || request:server-name() || ":" || request:server-port() ||
                    "/test/" || $expected-file).body.content)
};

declare %an:nondeterministic function test:invoke-and-assert-deep-equal(
  $endpoint as string,
  $parameters as object,
  $transform as function(object) as item*,
  $expected as item*
) as item
{
  test:invoke-and-assert-deep-equal($endpoint, $parameters, $transform, $expected, {})
};

(:
  Note on NoArrayOrder option: it allows for false positives in case arrays have duplicate elements. The number
  of occurrences of duplicate elements is not checked.
:)
declare %an:nondeterministic function test:invoke-and-assert-deep-equal(
  $endpoint as string,
  $parameters as object,
  $transform as function(object) as item*,
  $expected as item*,
  $options as object?
) as item
{
  let $request := switch($options.Format)
                  case "xml" return test:invoke-xml($endpoint, $parameters)
                  default return test:invoke($endpoint, $parameters)
  let $status as integer := $request[1]
  let $actual as item* := $transform($request[2])
  let $expected as item* := if($options.TrimIdField) then test:trim-ids($expected) else $expected
  let $actual as item* := if($options.TrimIdField) then test:trim-ids($actual) else $actual
  return if($options.NoArrayOrder)
          then test:assert-deep-equal-no-array-order($expected, $actual, $status, backend:url($endpoint, $parameters))
          else test:assert-deep-equal($expected, $actual, $status, backend:url($endpoint, $parameters))
};

declare %an:sequential function test:check-all-success($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
};

declare %private function test:trim-ids($input as item*) as item*
{
  for $i in $input
  return typeswitch($i)
          case object return {|
                                 for $key in keys($i)
                                 where $key ne "_id"
                                 return {$key: test:trim-ids($i.$key) }
                               |}
          case array return [ test:trim-ids($i[]) ]
          default return $i
};

(:    return
        if ($actual eq $expected)
        then true
        else "false [Actual="||$actual||", Expected="||$expected ||"]":)

declare function test:assert-eq(
    $expected as atomic,
    $actual as atomic?,
    $status as integer,
    $url as string) as item
{
    switch(true)
    case $status ne 200 return { "url": $url, status: $status }
    case try {$expected eq $actual} catch * { false } return true
    default return
    {
        "url": $url,
        "expected": $expected,
        "actual": $actual
    }
};

declare function test:assert-deep-equal(
    $expected as item*,
    $actual as item*,
    $status as integer,
    $url as string) as item
{
    switch(true)
    case $status ne 200 return { "url": $url, status: $status }
    case functx:sequence-deep-equal($expected, $actual) return true
    default return
    {
        "url": $url,
        "expected": typeswitch($expected) case json-item return $expected default return serialize($expected),
        "actual": typeswitch($actual) case json-item return $actual default return serialize($actual)
    }
};

declare function test:assert-deep-equal-no-array-order(
    $expected as item*,
    $actual as item*,
    $status as integer,
    $url as string) as item
{
    switch(true)
    case $status ne 200 return { "url": $url, status: $status }
    case test:deep-equal-no-array-order($expected, $actual) return true
    default return
    {
        "url": $url,
        "expected": typeswitch($expected) case json-item return $expected default return serialize($expected),
        "actual": typeswitch($actual) case json-item return $actual default return serialize($actual)
    }
};

declare function test:deep-equal-no-array-order(
    $expected as item*,
    $actual as item*) as boolean
{
    if(count($expected) ne count($actual))
    then false
    else
      every $i in count($expected)
      satisfies
        let $expected := $expected[$i]
        let $actual := $actual[$i]
        return
        typeswitch(($expected, $actual))
        case object* return
          switch(true)
          case not (every $key in keys($expected) satisfies keys($actual) = $key)
            return false
          case not (every $key in keys($actual) satisfies keys($expected) = $key)
            return false
          default return
            let $recursive-calls :=
                for $key in keys($expected)
                return test:deep-equal-no-array-order($expected.$key, $actual.$key)
            return if (every $item in $recursive-calls satisfies $item)
                    then true
                    else false
        case array* return
          switch(true)
          case not size($expected) eq size($actual)
            return false
          case not (every $expected-item in $expected[] satisfies (
            some $actual-item in $actual[] satisfies test:deep-equal-no-array-order($expected-item, $actual-item)
          ))
            return false
          case not (every $actual-item in $actual[] satisfies (
            some $expected-item in $expected[] satisfies test:deep-equal-no-array-order($expected-item, $actual-item)
          ))
            return false
          default return true
        default return deep-equal($expected, $actual)
};

declare function test:assert-eq-array(
    $expected as array,
    $actual as array,
    $status as integer,
    $url as string) as item
{
    let $diff := try {
            {
                actual: [
                    for $a in flatten($actual)
                    where not($a = flatten($expected))
                    return $a ],
                actualDuplicates: [
                    for $a in flatten($actual)
                    where $a = flatten($expected) and count(flatten($actual)[$$ eq $a]) ne count(flatten($expected)[$$ eq $a])
                    return $a ],
                expected: [
                    for $e in flatten($expected)
                    where not($e = flatten($actual))
                    return $e ]
            }
        } catch * { "error: " || $err:description }

    return
        switch(true)
        case $status ne 200 return { "url": $url, status: $status }
        case empty(($diff.actual[],$diff.actualDuplicates[],$diff.expected[])) return true
        default return
        {
            "url": $url,
            "diff": $diff,
            "expected": $expected,
            "actual": $actual
        }
};

jsoniq version "1.0";
module namespace test = "http://apps.28.io/test";
import module namespace backend = "http://apps.28.io/backend";
import module namespace http-client = "http://zorba.io/modules/http-client";
import module namespace request = "http://www.28msec.com/modules/http-request";
import module namespace response = "http://www.28msec.com/modules/http-response";
import module namespace config = "http://apps.28.io/config";
import module namespace credentials = "http://www.28msec.com/modules/credentials";
import module namespace functx = "http://www.functx.com";

declare function test:is-dow30() as boolean{
  contains(credentials:credentials("MongoDB", "xbrl").db, "dow30")
};

declare %an:nondeterministic function test:invoke($endpoint as string, $parameters as object) as item*
{
  let $url as string:= backend:url($endpoint, $parameters, true)
  let $response as object := http-client:get($url)
  return ($response.status, parse-json($response.body.content))
};

declare %an:nondeterministic function test:invoke-xml($endpoint as string, $parameters as object) as item*
{
  let $url as string:= backend:url($endpoint, $parameters, true)
  let $response as object := http-client:get($url)
  return ($response.status, parse-xml($response.body.content))
};

declare %an:sequential function test:invoke-body($endpoint as string, $parameters as object, $body as string) as item*
{
  let $url as string:= backend:url($endpoint, $parameters, true)
  let $response as object := http-client:post($url, $body, "application/x-www-form-urlencoded")
  return ($response.status, parse-json($response.body.content))
};

declare %an:nondeterministic function test:invoke-raw($endpoint as string, $parameters as object) as object
{
  let $url as string:= backend:url($endpoint, $parameters, true)
  return http-client:get($url)
};

declare %an:nondeterministic function test:invoke-public($endpoint as string, $parameters as object) as item*
{
  let $url as string:= backend:url($endpoint, $parameters, true)
  let $response as object := http-client:get($url)
  return ($response.status, parse-json($response.body.content))
};

declare %an:nondeterministic function test:get-expected-result(
  $expected-file as string
) as item*
{
  parse-json(
    http-client:get("http://" || request:server-name() || ":" || request:server-port() ||
                    "/test/" || $expected-file).body.content)
};

declare %an:nondeterministic function test:get-expected-result-xml(
  $expected-file as string
) as item*
{
  parse-xml(
    http-client:get("http://" || request:server-name() || ":" || request:server-port() ||
                    "/test/" || $expected-file).body.content)
};

declare %an:nondeterministic function test:invoke-and-assert-deep-equal(
  $endpoint as string,
  $parameters as object,
  $transform as function(object) as item*,
  $expected as item*
) as item
{
  test:invoke-and-assert-deep-equal($endpoint, $parameters, $transform, $expected, {})
};

(:
  Note on NoArrayOrder option: it allows for false positives in case arrays have duplicate elements. The number
  of occurrences of duplicate elements is not checked.
:)
declare %an:nondeterministic function test:invoke-and-assert-deep-equal(
  $endpoint as string,
  $parameters as object,
  $transform as function(object) as item*,
  $expected as item*,
  $options as object?
) as item
{
  let $request := switch($options.Format)
                  case "xml" return test:invoke-xml($endpoint, $parameters)
                  default return test:invoke($endpoint, $parameters)
  let $status as integer := $request[1]
  let $actual as item* := $transform($request[2])
  let $expected as item* := if($options.TrimIdField) then test:trim-ids($expected) else $expected
  let $actual as item* := if($options.TrimIdField) then test:trim-ids($actual) else $actual
  return if($options.NoArrayOrder)
          then test:assert-deep-equal-no-array-order($expected, $actual, $status, backend:url($endpoint, $parameters))
          else test:assert-deep-equal($expected, $actual, $status, backend:url($endpoint, $parameters))
};

declare %an:sequential function test:check-all-success($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
};

declare %private function test:trim-ids($input as item*) as item*
{
  for $i in $input
  return typeswitch($i)
          case object return {|
                                 for $key in keys($i)
                                 where $key ne "_id"
                                 return {$key: test:trim-ids($i.$key) }
                               |}
          case array return [ test:trim-ids($i[]) ]
          default return $i
};

(:    return
        if ($actual eq $expected)
        then true
        else "false [Actual="||$actual||", Expected="||$expected ||"]":)

declare function test:assert-eq(
    $expected as atomic,
    $actual as atomic?,
    $status as integer,
    $url as string) as item
{
    switch(true)
    case $status ne 200 return { "url": $url, status: $status }
    case try {$expected eq $actual} catch * { false } return true
    default return
    {
        "url": $url,
        "expected": $expected,
        "actual": $actual
    }
};

declare function test:assert-deep-equal(
    $expected as item*,
    $actual as item*,
    $status as integer,
    $url as string) as item
{
    switch(true)
    case $status ne 200 return { "url": $url, status: $status }
    case functx:sequence-deep-equal($expected, $actual) return true
    default return
    {
        "url": $url,
        "expected": typeswitch($expected) case json-item return $expected default return serialize($expected),
        "actual": typeswitch($actual) case json-item return $actual default return serialize($actual)
    }
};

declare function test:assert-deep-equal-no-array-order(
    $expected as item*,
    $actual as item*,
    $status as integer,
    $url as string) as item
{
    switch(true)
    case $status ne 200 return { "url": $url, status: $status }
    case test:deep-equal-no-array-order($expected, $actual) return true
    default return
    {
        "url": $url,
        "expected": typeswitch($expected) case json-item return $expected default return serialize($expected),
        "actual": typeswitch($actual) case json-item return $actual default return serialize($actual)
    }
};

declare function test:deep-equal-no-array-order(
    $expected as item*,
    $actual as item*) as boolean
{
    if(count($expected) ne count($actual))
    then false
    else
      every $i in count($expected)
      satisfies
        let $expected := $expected[$i]
        let $actual := $actual[$i]
        return
        typeswitch(($expected, $actual))
        case object* return
          switch(true)
          case not (every $key in keys($expected) satisfies keys($actual) = $key)
            return false
          case not (every $key in keys($actual) satisfies keys($expected) = $key)
            return false
          default return
            let $recursive-calls :=
                for $key in keys($expected)
                return test:deep-equal-no-array-order($expected.$key, $actual.$key)
            return if (every $item in $recursive-calls satisfies $item)
                    then true
                    else false
        case array* return
          switch(true)
          case not size($expected) eq size($actual)
            return false
          case not (every $expected-item in $expected[] satisfies (
            some $actual-item in $actual[] satisfies test:deep-equal-no-array-order($expected-item, $actual-item)
          ))
            return false
          case not (every $actual-item in $actual[] satisfies (
            some $expected-item in $expected[] satisfies test:deep-equal-no-array-order($expected-item, $actual-item)
          ))
            return false
          default return true
        default return deep-equal($expected, $actual)
};

declare function test:assert-eq-array(
    $expected as array,
    $actual as array,
    $status as integer,
    $url as string) as item
{
    let $diff := try {
            {
                actual: [
                    for $a in flatten($actual)
                    where not($a = flatten($expected))
                    return $a ],
                actualDuplicates: [
                    for $a in flatten($actual)
                    where $a = flatten($expected) and count(flatten($actual)[$$ eq $a]) ne count(flatten($expected)[$$ eq $a])
                    return $a ],
                expected: [
                    for $e in flatten($expected)
                    where not($e = flatten($actual))
                    return $e ]
            }
        } catch * { "error: " || $err:description }

    return
        switch(true)
        case $status ne 200 return { "url": $url, status: $status }
        case empty(($diff.actual[],$diff.actualDuplicates[],$diff.expected[])) return true
        default return
        {
            "url": $url,
            "diff": $diff,
            "expected": $expected,
            "actual": $actual
        }
};


:lipstick: Now displaying HTML.

:lipstick: Now displaying HTML.

jsoniq version "1.0";

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

import module namespace session    = "http://apps.28.io/session";
import module namespace sec-fiscal = "http://28.io/modules/xbrl/profiles/sec/fiscal/core";
import module namespace html = "http://www.zorba-xquery.com/modules/converters/html";
import module namespace csv = "http://zorba.io/modules/json-csv";

declare function api:json-to-csv($objects as object*) as string
{
  let $flattened as object* := api:flatten-json-object($objects, ())
  return string-join(
    csv:serialize($flattened, { field-names: [ keys($flattened) ], serialize-null-as : "" }),
    "")
  };

declare function api:json-to-xml($objects as object*, $root-name as string) as element()*
{
  for $flattened-object as object in api:flatten-json-object($objects, { KeepArray: true })
  return element { $root-name } {
    for $key in keys($flattened-object)
    let $value := $flattened-object.$key
    return typeswitch($value)
           case array return
             let $key-with-s := if(substring($key, string-length($key), 1) eq "s")
                                then $key
                                else $key || "s"
             let $key-no-s := if(substring($key, string-length($key), 1) eq "s")
                                then substring($key, 1, string-length($key) - 1)
                                else $key
             return element {$key-with-s} { for $v in $value[] return element { $key-no-s } { $v } }
           default return element { $key } { $value }
  }
};

declare %private function api:flatten-json-object(
  $items as item*,
  $options as object?) as item*
{
  for $item in $items
  return typeswitch($item)
         case atomic return $item
         case array return let $atomic-items := flatten($item)[$$ instance of atomic]
                           return if ($options.KeepArray)
                                 then [ $atomic-items ]
                                  else string-join($atomic-items, ",\r")
         case object return {|
             for $key in keys($item)
             return typeswitch($item.$key)
                    case object return api:flatten-json-object($item.$key, $options)
                    default return { $key: api:flatten-json-object($item.$key, $options) }
         |}
         default return ()
};

declare function api:validate-regexp($name as string, $value as string?, $regexp as string)
as ()
{
  if (exists($value))
  then
    if (matches($value, "^" || $regexp || "$"))
    then ();
    else fn:error(xs:QName("api:bad-parameter"), "Provided parameter " || $name || " with value " || $value || " does not match reg. expression " || $regexp || ".");
  else ();
};

declare function api:validate-enum($name as string, $value as string?, $enum as string*)
as ()
{
  if (exists($value))
  then
    if ($value = $enum)
    then ();
    else fn:error(xs:QName("api:bad-parameter"), "Provided parameter " || $name || " with value " || $value || " is not one of these: " || string-join($enum,", ") || ".");
  else ();
};

declare %private function api:sgpl($count as integer, $singular as string, $plural as string)
as string
{
    if ($count eq 1)
    then $count || " " || $singular
    else $count || " " || $plural
};

declare function api:passed($timestamp as xs:dateTime?)
as string
{
    if (empty($timestamp) or $timestamp eq null)
    then "Never"
    else
        let $now := current-dateTime()
        let $difference := $now - $timestamp
        let $years := years-from-duration($difference)
        let $month := months-from-duration($difference)
        let $days := days-from-duration($difference)
        let $hours := hours-from-duration($difference)
        let $minutes := minutes-from-duration($difference)
        return
            if ($years gt 0) then api:sgpl($years,"year ago","years ago")
            else if ($month gt 0) then api:sgpl($month,"month ago","month ago")
            else if ($days gt 0) then api:sgpl($days, "day ago", "days ago")
            else if ($hours gt 0) then api:sgpl($hours, "hour ago", "hours ago")
            else if ($minutes gt 0) then api:sgpl($minutes, "minute ago", "minutes ago")
            else if ($difference div xs:dayTimeDuration("PT1S") lt 0) then "future"
            else "Just Now"
};

declare function api:success() as object
{
  { "success" : true }
};

declare function api:success($data as object()) as object
{
  {|
     {"success" : true },
     $data
  |}
};

declare function api:check-and-return-results(
    $token as string?,
    $results as item*,
    $format as string?
) as item*
{
    switch(session:has-access($token, "data_sec"))
    case $session:ACCESS-ALLOWED return
        $results
    case $session:ACCESS-DENIED return {
          { status: 403 },
          session:error("access denied", $format)
       }
    case $session:ACCESS-AUTH-REQUIRED return {
          { status: 401 },
          session:error("authentication required or session expired", $format)
       }
    default return error()
};

declare function api:normalize-facts(
    $facts as object*) as object*
{
    for $fact in $facts
    return {|
        {
            "Aspects" : {|
                trim($fact.Aspects, ("xbrl:Unit")),
                { "sec:Archive" : $fact.Aspects."xbrl28:Archive" }[exists($fact.Aspects."xbrl28:Archive")]
            |}
        },
        trim($fact, ("Aspects", "_id")),
        { Unit: $fact.Aspects."xbrl:Unit" }[exists($fact.Aspects."xbrl:Unit") and not exists($fact.Unit)]
    |}
};

declare function api:serialize(
    $result as json-item,
    $comment as object,
    $serializers as object,
    $format as string?,
    $file-name as string) as item*
{
    switch ($format)
    case "xml" return {
        {
            serialization: { method: "xml", "omit-xml-declaration" : false, indent : true }
        },
        session:comment("xml", $comment),
        $serializers.to-xml($result)
    }
    case "text" case "csv" return {
        {
            "content-type": "text/csv",
            headers: { "Content-Disposition": "attachment; filename=" || $file-name || ".csv" }
        },
        $serializers.to-csv($result)
    }
    case "html" return {
        let $csv as string := $serializers.to-csv($result)
        return
        {
            { "content-type": "text/html" },
            api:csv-to-html($csv)
        }
    }
    case "excel" return {
        {
            "content-type": "application/vnd.ms-excel",
            headers: { "Content-Disposition": "attachment; filename=" || $file-name || ".csv" }
        },
        $serializers.to-csv($result)
    }
    default return {
        { serialization: { method: "json", indent : true } },
        {|
            session:comment("json", $comment),
            $result
        |}
    }
};

declare function api:csv-to-html(
    $csv as string) as item*
{
    let $csv as string* := $csv
    let $rows :=
      let $newline-tokens :=
        for tumbling window $token as string* in tokenize($csv, "\"")
        start when true
        end $u at $j next $v when ($j mod 2 eq 1 and $u ne "")
                             or ($j mod 2 eq 0 and $v ne "")
        count $k
        return if($k mod 2 eq 1)
               then $token ! replace($$, "\n", "______28newline______")
               else "\"" || string-join($token, "\"") || "\""
      return tokenize(string-join($newline-tokens), "______28newline______")
    let $header := $rows[1]
    let $body := subsequence($rows, 2)
    let $display-cell := function($row as string?, $cell-tag-name as string) {
      for $cell in
        let $comma-tokens :=
          for tumbling window $token as string* in tokenize($row, "\"")
          start when true
          end $u at $j next $v when ($j mod 2 eq 1 and $u ne "")
                               or ($j mod 2 eq 0 and $v ne "")
          count $k
          return if($k mod 2 eq 1)
                 then $token ! replace($$, ",", "______28comma______")
                 else string-join($token, "\"")
        return tokenize(string-join($comma-tokens), "______28comma______")
      return switch(true)
             case contains($cell, "http://") and
                  (contains($cell, "28.io") or
                   contains($cell, "rendering.secxbrl.info"))
             return element { $cell-tag-name } { Link }
             case starts-with(replace($cell, "(\r|\n)", ""), "<")
             return element { $cell-tag-name } {html:parse($cell)/*}
             default return element { $cell-tag-name } {tokenize($cell, "\r") ! ($$,
)}
    }
    return

        <title>Cell Store REST API</title>

{$display-cell($header, "th")}

          {
            for $row as string in $body
            return
{$display-cell($row, "td")}

          }

};

declare function api:preprocess-fiscal-years($fiscal-years as string*) as integer*
{
  distinct-values(
    for $fy in $fiscal-years ! upper-case($$)
    return switch($fy)
           case "LATEST" return $sec-fiscal:LATEST_FISCAL_YEAR
           case "ALL" return $sec-fiscal:ALL_FISCAL_YEARS
           default return if($fy castable as integer) then integer($fy) else ()
  )
};

declare function api:preprocess-fiscal-periods($fiscal-periods as string*) as string*
{
  distinct-values(
    for $fp in $fiscal-periods ! upper-case($$)
    return switch($fp)
           case "Q1"
           case "Q2"
           case "Q3"
           case "FY"
             return $fp
           case "ALL"
             return $sec-fiscal:ALL_FISCAL_PERIODS
           default
             return error(xs:QName("local:INVALID-PERIOD"),
               $fp || ": fiscalPeriod values must be one or more of Q1, Q2, Q3, FY, ALL")
  )
};

declare function api:preprocess-fiscal-period-types($fiscal-period-types as string*) as string*
{
  distinct-values(
    for $fpt in $fiscal-period-types
    return switch($fpt)
           case "instant"
           case "QTD"
           case "YTD"
             return $fpt
           case "ALL"
             return $sec-fiscal:ALL_FISCAL_PERIOD_TYPES
           default
             return error(xs:QName("local:INVALID-PERIOD-TYPE"),
               $fpt || ": fiscalPeriodType values must be one or more of instant, YTD, QTD")
  )
};

declare function api:preprocess-format($format as string?, $request-uri as string) as string?
{
  let $request-path := tokenize($request-uri, "\\?")[1]
  return lower-case(($format, substring-after($request-path, ".jq."))[1])
};

declare function api:preprocess-tags($tags as string*) as string*
{
  distinct-values($tags ! upper-case($$))
};

declare function api:preprocess-concept-kind($kind as string*) as string*
{
  distinct-values(
    for $kind in $kind
    return switch(lower-case($kind))
           case "hypercube" return "Hypercube"
           case "dimension" return "Dimension"
           case "member" return "Member"
           case "lineitems" return "LineItems"
           case "abstract" return "Abstract"
           case "concept" return "Concept"
           default return ()
  )
};

declare function api:preprocess-boolean($name as string, $value as string)
as boolean
{
  if ($value eq "")
  then true
  else
  {
      api:validate-enum($name, lower-case($value), ("true", "false"));
      boolean($value)
  }
};

declare function api:not-found($name as string) as item*
{
  { status: 404, content-type: "application/json" },
  session:error("No such " || $name || " could be found.", "json")
};

jsoniq version "1.0";

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

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

import module namespace csv = "http://zorba.io/modules/json-csv";

declare function api:json-to-csv($objects as object*) as string
{
  let $flattened as object* := api:flatten-json-object($objects, ())
  return string-join(
    csv:serialize($flattened, { field-names: [ keys($flattened) ], serialize-null-as : "" }),
    "")
  };

declare function api:json-to-xml($objects as object*, $root-name as string) as element()*
{
  for $flattened-object as object in api:flatten-json-object($objects, { KeepArray: true })
  return element { $root-name } {
    for $key in keys($flattened-object)
    let $value := $flattened-object.$key
    return typeswitch($value)
           case array return
             let $key-with-s := if(substring($key, string-length($key), 1) eq "s")
                                then $key
                                else $key || "s"
             let $key-no-s := if(substring($key, string-length($key), 1) eq "s")
                                then substring($key, 1, string-length($key) - 1)
                                else $key
             return element {$key-with-s} { for $v in $value[] return element { $key-no-s } { $v } }
           default return element { $key } { $value }
  }
};

declare %private function api:flatten-json-object(
  $items as item*,
  $options as object?) as item*
{
  for $item in $items
  return typeswitch($item)
         case atomic return $item
         case array return let $atomic-items := flatten($item)[$$ instance of atomic]
                           return if ($options.KeepArray)
                                 then [ $atomic-items ]
                                  else string-join($atomic-items, ",\r")
         case object return {|
             for $key in keys($item)
             return typeswitch($item.$key)
                    case object return api:flatten-json-object($item.$key, $options)
                    default return { $key: api:flatten-json-object($item.$key, $options) }
         |}
         default return ()
};

declare function api:validate-regexp($name as string, $value as string?, $regexp as string)
as ()
{
  if (exists($value))
  then
    if (matches($value, "^" || $regexp || "$"))
    then ();
    else fn:error(xs:QName("api:bad-parameter"), "Provided parameter " || $name || " with value " || $value || " does not match reg. expression " || $regexp || ".");
  else ();
};

declare function api:validate-enum($name as string, $value as string?, $enum as string*)
as ()
{
  if (exists($value))
  then
    if ($value = $enum)
    then ();
    else fn:error(xs:QName("api:bad-parameter"), "Provided parameter " || $name || " with value " || $value || " is not one of these: " || string-join($enum,", ") || ".");
  else ();
};

declare %private function api:sgpl($count as integer, $singular as string, $plural as string)
as string
{
    if ($count eq 1)
    then $count || " " || $singular
    else $count || " " || $plural
};

declare function api:passed($timestamp as xs:dateTime?)
as string
{
    if (empty($timestamp) or $timestamp eq null)
    then "Never"
    else
        let $now := current-dateTime()
        let $difference := $now - $timestamp
        let $years := years-from-duration($difference)
        let $month := months-from-duration($difference)
        let $days := days-from-duration($difference)
        let $hours := hours-from-duration($difference)
        let $minutes := minutes-from-duration($difference)
        return
            if ($years gt 0) then api:sgpl($years,"year ago","years ago")
            else if ($month gt 0) then api:sgpl($month,"month ago","month ago")
            else if ($days gt 0) then api:sgpl($days, "day ago", "days ago")
            else if ($hours gt 0) then api:sgpl($hours, "hour ago", "hours ago")
            else if ($minutes gt 0) then api:sgpl($minutes, "minute ago", "minutes ago")
            else if ($difference div xs:dayTimeDuration("PT1S") lt 0) then "future"
            else "Just Now"
};

declare function api:success() as object
{
  { "success" : true }
};

declare function api:success($data as object()) as object
{
  {|
     {"success" : true },
     $data
  |}
};

declare function api:check-and-return-results(
    $token as string?,
    $results as item*,
    $format as string?
) as item*
{
    switch(session:has-access($token, "data_sec"))
    case $session:ACCESS-ALLOWED return
        $results
    case $session:ACCESS-DENIED return {
          { status: 403 },
          session:error("access denied", $format)
       }
    case $session:ACCESS-AUTH-REQUIRED return {
          { status: 401 },
          session:error("authentication required or session expired", $format)
       }
    default return error()
};

declare function api:normalize-facts(
    $facts as object*) as object*
{
    for $fact in $facts
    return {|
        {
            "Aspects" : {|
                trim($fact.Aspects, ("xbrl:Unit")),
                { "sec:Archive" : $fact.Aspects."xbrl28:Archive" }[exists($fact.Aspects."xbrl28:Archive")]
            |}
        },
        trim($fact, ("Aspects", "_id")),
        { Unit: $fact.Aspects."xbrl:Unit" }[exists($fact.Aspects."xbrl:Unit") and not exists($fact.Unit)]
    |}
};

declare function api:serialize(
    $result as json-item,
    $comment as object,
    $serializers as object,
    $format as string?,
    $file-name as string) as item*
{
    switch ($format)
    case "xml" return {
        {
            serialization: { method: "xml", "omit-xml-declaration" : false, indent : true }
        },
        session:comment("xml", $comment),
        $serializers.to-xml($result)
    }
    case "text" case "csv" return {
        {
            "content-type": "text/csv",
            headers: { "Content-Disposition": "attachment; filename=" || $file-name || ".csv" }
        },
        $serializers.to-csv($result)
    }
    case "html" return {
        let $csv as string := $serializers.to-csv($result)
        return
        {
            { "content-type": "text/html" },
            api:csv-to-html($csv)
        }
    }
    case "excel" return {
        {
            "content-type": "application/vnd.ms-excel",
            headers: { "Content-Disposition": "attachment; filename=" || $file-name || ".csv" }
        },
        $serializers.to-csv($result)
    }
    default return {
        { serialization: { method: "json", indent : true } },
        {|
            session:comment("json", $comment),
            $result
        |}
    }
};

declare function api:csv-to-html(
    $csv as string) as item*
{
    let $csv as string* := $csv
    let $rows :=
      let $newline-tokens :=
        for tumbling window $token as string* in tokenize($csv, "\"")
        start when true
        end $u at $j next $v when ($j mod 2 eq 1 and $u ne "")
                             or ($j mod 2 eq 0 and $v ne "")
        count $k
        return if($k mod 2 eq 1)
               then $token ! replace($$, "\n", "______28newline______")
               else "\"" || string-join($token, "\"") || "\""
      return tokenize(string-join($newline-tokens), "______28newline______")
    let $header := $rows[1]
    let $body := subsequence($rows, 2)
    let $display-cell := function($row as string?, $cell-tag-name as string) {
      for $cell in
        let $comma-tokens :=
          for tumbling window $token as string* in tokenize($row, "\"")
          start when true
          end $u at $j next $v when ($j mod 2 eq 1 and $u ne "")
                               or ($j mod 2 eq 0 and $v ne "")
          count $k
          return if($k mod 2 eq 1)
                 then $token ! replace($$, ",", "______28comma______")
                 else string-join($token, "\"")
        return tokenize(string-join($comma-tokens), "______28comma______")
      return if(contains($cell, "http://") and
                (contains($cell, "28.io") or
                 contains($cell, "rendering.secxbrl.info")))
      then element { $cell-tag-name } { Link }
      else element { $cell-tag-name } {tokenize($cell, "\r") ! ($$,
)}
    }
    return

        <title>Cell Store REST API</title>

{$display-cell($header, "th")}

          {
            for $row as string in $body
            return
{$display-cell($row, "td")}

          }

};

declare function api:preprocess-fiscal-years($fiscal-years as string*) as integer*
{
  distinct-values(
    for $fy in $fiscal-years ! upper-case($$)
    return switch($fy)
           case "LATEST" return $sec-fiscal:LATEST_FISCAL_YEAR
           case "ALL" return $sec-fiscal:ALL_FISCAL_YEARS
           default return if($fy castable as integer) then integer($fy) else ()
  )
};

declare function api:preprocess-fiscal-periods($fiscal-periods as string*) as string*
{
  distinct-values(
    for $fp in $fiscal-periods ! upper-case($$)
    return switch($fp)
           case "Q1"
           case "Q2"
           case "Q3"
           case "FY"
             return $fp
           case "ALL"
             return $sec-fiscal:ALL_FISCAL_PERIODS
           default
             return error(xs:QName("local:INVALID-PERIOD"),
               $fp || ": fiscalPeriod values must be one or more of Q1, Q2, Q3, FY, ALL")
  )
};

declare function api:preprocess-fiscal-period-types($fiscal-period-types as string*) as string*
{
  distinct-values(
    for $fpt in $fiscal-period-types
    return switch($fpt)
           case "instant"
           case "QTD"
           case "YTD"
             return $fpt
           case "ALL"
             return $sec-fiscal:ALL_FISCAL_PERIOD_TYPES
           default
             return error(xs:QName("local:INVALID-PERIOD-TYPE"),
               $fpt || ": fiscalPeriodType values must be one or more of instant, YTD, QTD")
  )
};

declare function api:preprocess-format($format as string?, $request-uri as string) as string?
{
  let $request-path := tokenize($request-uri, "\\?")[1]
  return lower-case(($format, substring-after($request-path, ".jq."))[1])
};

declare function api:preprocess-tags($tags as string*) as string*
{
  distinct-values($tags ! upper-case($$))
};

declare function api:preprocess-concept-kind($kind as string*) as string*
{
  distinct-values(
    for $kind in $kind
    return switch(lower-case($kind))
           case "hypercube" return "Hypercube"
           case "dimension" return "Dimension"
           case "member" return "Member"
           case "lineitems" return "LineItems"
           case "abstract" return "Abstract"
           case "concept" return "Concept"
           default return ()
  )
};

declare function api:preprocess-boolean($name as string, $value as string)
as boolean
{
  if ($value eq "")
  then true
  else
  {
      api:validate-enum($name, lower-case($value), ("true", "false"));
      boolean($value)
  }
};

declare function api:not-found($name as string) as item*
{
  { status: 404, content-type: "application/json" },
  session:error("No such " || $name || " could be found.", "json")
};


:pencil: Adding Japanese labels to standard concepts.

jsoniq version "1.0";
(:
 : Copyright 2015 28msec Inc.
 :)

(:~
 :
This module provides functionality for retrieving labels.

 :
 : @author Federico Cavalieri, Ghislain Fourny
 :)
module namespace labels = "http://28.io/modules/xbrl/labels";

import module namespace concepts = "http://28.io/modules/xbrl/concepts";
import module namespace entities = "http://28.io/modules/xbrl/entities";

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

(:~
 : A helper variable holding the default language ("en-US")
 :)
declare variable $labels:AMERICAN_ENGLISH as xs:string := "en-US";

(:~
 : The standard label role.
 :)
declare variable $labels:STANDARD_LABEL_ROLE as xs:string :=
    "http://www.xbrl.org/2003/role/label";

(:~
 : The verbose label role.
 :)
declare variable $labels:VERBOSE_LABEL_ROLE as xs:string :=
    "http://www.xbrl.org/2003/role/verboseLabel";

declare variable $labels:standard-concepts as object* :=
(
    {
      Name: "xbrl:Concept",
      Label: "Concept [Axis]",
      Kind: "Dimension",
      IsNillable: true,
      IsAbstract: true,
      PeriodType: "duration",
      SubstitutionGroup: "xbrldt:dimensionItem",
      DataType: "nonnum:domainItemType",
      BaseType: "string",
      ClosestSchemaBuiltinType: "xs:string",
      IsTextBlock: false,
      Labels: [
        {
          Role: "http://www.xbrl.org/2003/role/label",
          Language: "fr",
          Value: "Concept [Axe]"
        },
        {
          Role: "http://www.xbrl.org/2003/role/label",
          Language: "de",
          Value: "Konzept [Achse]"
        },
        {
          Role: "http://www.xbrl.org/2003/role/label",
          Language: "en-us",
          Value: "Concept [Axis]"
        },
        {
          Role: "http://www.xbrl.org/2003/role/label",
          Language: "ja",
          Value: "項目(Concept)"
        }
      ]
    },
    {
      Name: "xbrl:Period",
      Label: "Period [Axis]",
      Kind: "Dimension",
      IsNillable: true,
      IsAbstract: true,
      PeriodType: "duration",
      SubstitutionGroup: "xbrldt:dimensionItem",
      DataType: "nonnum:domainItemType",
      BaseType: "string",
      ClosestSchemaBuiltinType: "xs:string",
      IsTextBlock: false,
      Labels: [
        {
          Role: "http://www.xbrl.org/2003/role/label",
          Language: "fr",
          Value: "Période [Axe]"
        },
        {
          Role: "http://www.xbrl.org/2003/role/label",
          Language: "en-us",
          Value: "Period [Axis]"
        },
        {
          Role: "http://www.xbrl.org/2003/role/label",
          Language: "de",
          Value: "Periode [Achse]"
        },
        {
          Role: "http://www.xbrl.org/2003/role/label",
          Language: "ja",
          Value: "期間(Period)"
        }
      ]
    },
    {
      Name: "xbrl:Entity",
      Label: "Entity [Axis]",
      Kind: "Dimension",
      IsNillable: true,
      IsAbstract: true,
      PeriodType: "duration",
      SubstitutionGroup: "xbrldt:dimensionItem",
      DataType: "nonnum:domainItemType",
      BaseType: "string",
      ClosestSchemaBuiltinType: "xs:string",
      IsTextBlock: false,
      Labels: [
        {
          Role: "http://www.xbrl.org/2003/role/label",
          Language: "de",
          Value: "Entität [Achse]"
        },
        {
          Role: "http://www.xbrl.org/2003/role/label",
          Language: "fr",
          Value: "Entité [Axe]"
        },
        {
          Role: "http://www.xbrl.org/2003/role/label",
          Language: "en-us",
          Value: "Reporting Entity [Axis]"
        },
        {
          Role: "http://www.xbrl.org/2003/role/label",
          Language: "ja",
          Value: "企業名(Entity)"
        }
      ]
    },
    {
      Name: "xbrl28:Archive",
      Label: "Archiv [Achse]",
      Kind: "Dimension",
      IsNillable: true,
      IsAbstract: true,
      PeriodType: "duration",
      SubstitutionGroup: "xbrldt:dimensionItem",
      DataType: "nonnum:domainItemType",
      BaseType: "string",
      ClosestSchemaBuiltinType: "xs:string",
      IsTextBlock: false,
      Labels: [
        {
          Role: "http://www.xbrl.org/2003/role/label",
          Language: "fr",
          Value: "Archive [Axe]"
        },
        {
          Role: "http://www.xbrl.org/2003/role/label",
          Language: "de",
          Value: "Archiv [Achse]"
        },
        {
          Role: "http://www.xbrl.org/2003/role/label",
          Language: "en-us",
          Value: "Archive [Axis]"
        },
        {
          Role: "http://www.xbrl.org/2003/role/label",
          Language: "ja",
          Value: "ファイル(Archive)"
        }
      ]
    }
);

(:~
 :
Retrieves all the labels with the given label role and language in the
 : matching concepts, one label for each concept.

 :
 :
Matching concepts are those which:
 :  - concept name and archive number match a given one;
 :  - component role matches a given one or is the default
 :    component role.
 :

 :
 :
The set of concepts to search in is specified as a parameter.

 :
 :
Language matching can either be exact, if no options are given,
 : or approximated, if at least one of the following options is given:

 :

 :
MatchDown: whether to match a more specific language, e.g.:
 :       "en" will match labels which language is "en" or "en-US".

 :
MatchUp: whether to match a less specific language, e.g.:
 :       "en-US" will match labels which language is "en-US" or "en".

 :
MatchAnyVariant: whether to match a different variant of the same
 :       language, e.g.: "en-US" will match labels which language is "en-US"
 :       or "en-UK".

 :

 :
 : @param $concept-names the concepts names.
 : @param $label-roles the label role.
 : @param $languages the label language.
 : @param $concepts the concepts in which the labels will be
 :                  searched.
 : @param $entities the entities in which entity labels will be searched.
 : @param $options Optional parameters to control language matching.
 :
 : @return the matching labels (one associated with each input concept, as
 : an object).
 :)
declare function labels:labels(
    $concept-names as string*,
    $label-roles as string*,
    $languages as string*,
    $concepts as object*,
    $entities as object*,
    $options as object?
  ) as object
{
  let $concepts := ($concepts, $labels:standard-concepts)
  return {|
    let $normalized-languages as string* := $languages ! labels:normalize-language($$)
    return (
      for $concept as object in $concepts
      where ($concepts:ALL_CONCEPT_NAMES, $concept.$concepts:NAME) = $concept-names
      group by $concept.$concepts:NAME
      let $concept-labels as object* :=
        $concept.$concepts:LABELS[][$$.Role = $label-roles]
      let $available-languages as string* :=
        distinct-values($concept-labels.Language)
      let $approximate-languages as string* := labels:approximate-languages(
        $available-languages,
        $normalized-languages,
        $options
      )
      let $perfect-matches as object* :=
        $concept-labels[$$.Language = $normalized-languages]
      let $approximate-matches as object* :=
        $concept-labels[$$.Language = $approximate-languages]
      let $label as string? :=
        ($perfect-matches.Value,
         $approximate-matches.Value,
         $concept-labels.Value,
         $concept.Label)[not $$ instance of null][1]
      where exists($label)
      return {
        $concept.Name : $label
      },

      for $concept in $concept-names
      where not (($concepts:ALL_CONCEPT_NAMES, $concepts.$concepts:NAME) = $concept)
      let $label as string* :=
          switch(true)
            case starts-with($concept, "iso4217:")
            return substring($concept, 9)
            case $concept castable as date
            return format-date(date($concept), "[MNn] [D], [Y]", "en", (), ())
            case (every $date in tokenize($concept, "/") satisfies $date castable as date)
            return "From " || string-join(
                     for $date in tokenize($concept, "/")
                     return format-date(date($date), "[MNn] [D], [Y]", "en", (), ()),
                   " to ")
            case contains($concept, " ")
            return
              switch(true)
              case $concept = entities:eid($entities) and $normalized-languages = ("ja")
              return ($entities[entities:eid($$) = $concept].Profiles.SEC.EntityRegistrantName,
                       $entities[entities:eid($$) = $concept].Profiles.FSA.SubmitterName)[1]
              case $concept = entities:eid($entities)
              return ($entities[entities:eid($$) = $concept].Profiles.SEC.CompanyName,
                       $entities[entities:eid($$) = $concept].Profiles.FSA.SubmitterNameAlphabetic)[1]
              default return ()
           default return ()
      where exists($label)
      return {
        $concept : $label
      }
    )
  |}
};

(:~
 :
Retrieves all the labels with the given label role and language for
 : all concepts used in the fact and matching a concept in the list of
 : concepts. Concepts used in a fact include not only those from the
 : 'xbrl:Concept' aspect, but also Members of any custom axis.

 :
 :
Matching concepts are those which:
 :  - concept name matches a given one,
 :  - archive number matches that of a given component,
 :  - component role matches that of a given component or is the default
 :    component role.
 :

 :
 :
The set of concepts to search in is specified as a parameter.

 :
 :
Language matching can either be exact, if no options are given,
 : or approximated, if at least one of the following options is given:

 :

 :
MatchDown: whether to match a more specific language, e.g.:
 :       "en" will match labels which language is "en" or "en-US".

 :
MatchUp: whether to match a less specific language, e.g.:
 :       "en-US" will match labels which language is "en-US" or "en".

 :
MatchAnyVariant: whether to match a different variant of the same
 :       language, e.g.: "en-US" will match labels which language is "en-US"
 :       or "en-UK".

 :

 :
 : @param $facts a sequence of facts.
 : @param $label-role the label role.
 : @param $language the label language.
 : @param $concepts the concepts in which the labels will be
 :                  searched (in the version-7 format).
 : @param $options optional parameters to control language matching.
 :
 : @return an object with matching concepts as keys and labels as values.
 :)
declare function labels:labels-for-facts(
    $facts as object*,
    $label-role as string,
    $language as string,
    $concepts as object*,
    $entities as object*,
    $options as object?
  ) as object?
{
    for $facts in $facts
    group by $archive := $facts."Aspects"."xbrl28:Archive"
    let $concept-names as string* :=
        distinct-values((
          values($facts.Aspects),
          keys($facts.Aspects)
    )[$$ instance of string])
    return
        {|
            labels:labels(
                    $concept-names,
                    $label-role,
                    $language,
                    $concepts,
                    $entities,
                    $options)
        |}
};

(:~
 :
Normalizes the language code. This normalized language code can then
 : be used to find the right labels in the concepts collection.

 :
 : @param $language the language identifier.
 :
 : @return the normalized language.
 :)
declare function labels:normalize-language($language as string) as string
{
  replace(lower-case($language), "_", "-")
};

declare %private function labels:approximate-languages(
    $all-languages as string*,
    $normalized-language as string,
    $options as object?
  ) as string*
{
    if(not $options.MatchDown cast as boolean? eq false)
    then $all-languages[starts-with($$, $normalized-language)]
    else (),
    if(not $options.MatchUp cast as boolean? eq false)
    then $all-languages[starts-with($normalized-language, $$)]
    else (),
    if ((not $options.MatchAnyVariant cast as boolean? eq false) and contains($normalized-language, "-"))
    then
      $all-languages[starts-with($$, substring-before($normalized-language, "-") || "-")]
    else ()
};

jsoniq version "1.0";
(:
 : Copyright 2015 28msec Inc.
 :)

(:~
 :
This module provides functionality for retrieving labels.

 :
 : @author Federico Cavalieri, Ghislain Fourny
 :)
module namespace labels = "http://28.io/modules/xbrl/labels";

import module namespace concepts = "http://28.io/modules/xbrl/concepts";
import module namespace entities = "http://28.io/modules/xbrl/entities";

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

(:~
 : A helper variable holding the default language ("en-US")
 :)
declare variable $labels:AMERICAN_ENGLISH as xs:string := "en-US";

(:~
 : The standard label role.
 :)
declare variable $labels:STANDARD_LABEL_ROLE as xs:string :=
    "http://www.xbrl.org/2003/role/label";

(:~
 : The verbose label role.
 :)
declare variable $labels:VERBOSE_LABEL_ROLE as xs:string :=
    "http://www.xbrl.org/2003/role/verboseLabel";

(:~
 :
Retrieves all the labels with the given label role and language in the
 : matching concepts, one label for each concept.

 :
 :
Matching concepts are those which:
 :  - concept name and archive number match a given one;
 :  - component role matches a given one or is the default
 :    component role.
 :

 :
 :
The set of concepts to search in is specified as a parameter.

 :
 :
Language matching can either be exact, if no options are given,
 : or approximated, if at least one of the following options is given:

 :

 :
MatchDown: whether to match a more specific language, e.g.:
 :       "en" will match labels which language is "en" or "en-US".

 :
MatchUp: whether to match a less specific language, e.g.:
 :       "en-US" will match labels which language is "en-US" or "en".

 :
MatchAnyVariant: whether to match a different variant of the same
 :       language, e.g.: "en-US" will match labels which language is "en-US"
 :       or "en-UK".

 :

 :
 : @param $concept-names the concepts names.
 : @param $label-roles the label role.
 : @param $languages the label language.
 : @param $concepts the concepts in which the labels will be
 :                  searched.
 : @param $entities the entities in which entity labels will be searched.
 : @param $options Optional parameters to control language matching.
 :
 : @return the matching labels (one associated with each input concept, as
 : an object).
 :)
declare function labels:labels(
    $concept-names as string*,
    $label-roles as string*,
    $languages as string*,
    $concepts as object*,
    $entities as object*,
    $options as object?
  ) as object
{
  {|
    let $normalized-languages as string* := $languages ! labels:normalize-language($$)
    return (
      for $concept as object in $concepts
      where ($concepts:ALL_CONCEPT_NAMES, $concept.$concepts:NAME) = $concept-names
      group by $concept.$concepts:NAME
      let $concept-labels as object* :=
        $concept.$concepts:LABELS[][$$.Role = $label-roles]
      let $available-languages as string* :=
        distinct-values($concept-labels.Language)
      let $approximate-languages as string* := labels:approximate-languages(
        $available-languages,
        $normalized-languages,
        $options
      )
      let $perfect-matches as object* :=
        $concept-labels[$$.Language = $normalized-languages]
      let $approximate-matches as object* :=
        $concept-labels[$$.Language = $approximate-languages]
      let $label as string? :=
        ($perfect-matches.Value,
         $approximate-matches.Value,
         $concept-labels.Value,
         $concept.Label)[not $$ instance of null][1]
      where exists($label)
      return {
        $concept.Name : $label
      },

      for $concept in $concept-names
      where not (($concepts:ALL_CONCEPT_NAMES, $concepts.$concepts:NAME) = $concept)
      let $label as string* :=
          switch(true)
            case starts-with($concept, "iso4217:")
            return substring($concept, 9)
            case $concept castable as date
            return format-date(date($concept), "[MNn] [D], [Y]", "en", (), ())
            case (every $date in tokenize($concept, "/") satisfies $date castable as date)
            return "From " || string-join(
                     for $date in tokenize($concept, "/")
                     return format-date(date($date), "[MNn] [D], [Y]", "en", (), ()),
                   " to ")
            case contains($concept, " ")
            return
              switch(true)
              case $concept = entities:eid($entities) and $normalized-languages = ("ja")
              return ($entities[entities:eid($$) = $concept].Profiles.SEC.EntityRegistrantName,
                       $entities[entities:eid($$) = $concept].Profiles.FSA.SubmitterName)[1]
              case $concept = entities:eid($entities)
              return ($entities[entities:eid($$) = $concept].Profiles.SEC.CompanyName,
                       $entities[entities:eid($$) = $concept].Profiles.FSA.SubmitterNameAlphabetic)[1]
              default return ()
           default return ()
      where exists($label)
      return {
        $concept : $label
      }
    )
  |}
};

(:~
 :
Retrieves all the labels with the given label role and language for
 : all concepts used in the fact and matching a concept in the list of
 : concepts. Concepts used in a fact include not only those from the
 : 'xbrl:Concept' aspect, but also Members of any custom axis.

 :
 :
Matching concepts are those which:
 :  - concept name matches a given one,
 :  - archive number matches that of a given component,
 :  - component role matches that of a given component or is the default
 :    component role.
 :

 :
 :
The set of concepts to search in is specified as a parameter.

 :
 :
Language matching can either be exact, if no options are given,
 : or approximated, if at least one of the following options is given:

 :

 :
MatchDown: whether to match a more specific language, e.g.:
 :       "en" will match labels which language is "en" or "en-US".

 :
MatchUp: whether to match a less specific language, e.g.:
 :       "en-US" will match labels which language is "en-US" or "en".

 :
MatchAnyVariant: whether to match a different variant of the same
 :       language, e.g.: "en-US" will match labels which language is "en-US"
 :       or "en-UK".

 :

 :
 : @param $facts a sequence of facts.
 : @param $label-role the label role.
 : @param $language the label language.
 : @param $concepts the concepts in which the labels will be
 :                  searched (in the version-7 format).
 : @param $options optional parameters to control language matching.
 :
 : @return an object with matching concepts as keys and labels as values.
 :)
declare function labels:labels-for-facts(
    $facts as object*,
    $label-role as string,
    $language as string,
    $concepts as object*,
    $entities as object*,
    $options as object?
  ) as object?
{
    for $facts in $facts
    group by $archive := $facts."Aspects"."xbrl28:Archive"
    let $concept-names as string* :=
        distinct-values((
          values($facts.Aspects),
          keys($facts.Aspects)
    )[$$ instance of string])
    return
        {|
            labels:labels(
                    $concept-names,
                    $label-role,
                    $language,
                    $concepts,
                    $entities,
                    $options)
        |}
};

(:~
 :
Normalizes the language code. This normalized language code can then
 : be used to find the right labels in the concepts collection.

 :
 : @param $language the language identifier.
 :
 : @return the normalized language.
 :)
declare function labels:normalize-language($language as string) as string
{
  replace(lower-case($language), "_", "-")
};

declare %private function labels:approximate-languages(
    $all-languages as string*,
    $normalized-language as string,
    $options as object?
  ) as string*
{
    if(not $options.MatchDown cast as boolean? eq false)
    then $all-languages[starts-with($$, $normalized-language)]
    else (),
    if(not $options.MatchUp cast as boolean? eq false)
    then $all-languages[starts-with($normalized-language, $$)]
    else (),
    if ((not $options.MatchAnyVariant cast as boolean? eq false) and contains($normalized-language, "-"))
    then
      $all-languages[starts-with($$, substring-before($normalized-language, "-") || "-")]
    else ()
};


:green_heart: Fix.

:green_heart: Fix.

jsoniq version "1.0";
module namespace test = "http://apps.28.io/test";
import module namespace http-client = "http://zorba.io/modules/http-client";
import module namespace request = "http://www.28msec.com/modules/http-request";
import module namespace response = "http://www.28msec.com/modules/http-response";
import module namespace config = "http://apps.28.io/config";
import module namespace functx = "http://www.functx.com";
import module namespace backend = "http://apps.28.io/backend";

declare function test:url($endpoint as string, $parameters as object) as string
{
  backend:url(
    $endpoint,
    {|
      { token: $config:test-token },
      $parameters
    |}
  )
};

declare %an:nondeterministic function test:invoke($endpoint as string, $parameters as object) as item*
{
  let $url as string:= test:url($endpoint, $parameters)
  let $response as object := http-client:get($url)
  return ($response.status, parse-json($response.body.content))
};

declare %an:nondeterministic function test:invoke-xml($endpoint as string, $parameters as object) as item*
{
  let $url as string:= test:url($endpoint, $parameters)
  let $response as object := http-client:get($url)
  return ($response.status, parse-xml($response.body.content))
};

declare %an:sequential function test:invoke-body($endpoint as string, $parameters as object, $body as string) as item*
{
  let $url as string:= test:url($endpoint, $parameters)
  let $response as object := http-client:post($url, $body, "application/x-www-form-urlencoded")
  return ($response.status, parse-json($response.body.content))
};

declare %an:nondeterministic function test:invoke-raw($endpoint as string, $parameters as object) as object
{
  let $url as string:= test:url($endpoint, $parameters)
  return http-client:get($url)
};

declare %an:nondeterministic function test:invoke-public($endpoint as string, $parameters as object) as item*
{
  let $url as string:= test:url($endpoint, $parameters)
  let $response as object := http-client:get($url)
  return ($response.status, parse-json($response.body.content))
};

declare %an:nondeterministic function test:get-expected-result(
  $expected-file as string
) as item*
{
  parse-json(
    http-client:get("http://" || request:server-name() || ":" || request:server-port() ||
                    "/test/" || $expected-file).body.content)
};

declare %an:nondeterministic function test:get-expected-result-xml(
  $expected-file as string
) as item*
{
  parse-xml(
    http-client:get("http://" || request:server-name() || ":" || request:server-port() ||
                    "/test/" || $expected-file).body.content)
};

declare %an:nondeterministic function test:invoke-and-assert-deep-equal(
  $endpoint as string,
  $parameters as object,
  $transform as function(object) as item*,
  $expected as item*
) as item
{
  test:invoke-and-assert-deep-equal($endpoint, $parameters, $transform, $expected, {})
};

declare %an:sequential function test:invoke-and-assert-status(
  $endpoint as string,
  $parameters as object,
  $expected-status as integer
) as item
{
  let $url as string:= test:url($endpoint, $parameters)
  let $response as object := http-client:send-request({href: $url, method: "GET", "options" : { "status-only": true, "follow-redirect" : false } })
  let $actual-status := $response.status
  return if($actual-status eq $expected-status)
         then true
         else {
           url: test:url($endpoint, $parameters),
           expected: $expected-status,
           actual: $actual-status
         }
};

(:
  Note on NoArrayOrder option: it allows for false positives in case arrays have duplicate elements. The number
  of occurrences of duplicate elements is not checked.
:)
declare %an:nondeterministic function test:invoke-and-assert-deep-equal(
  $endpoint as string,
  $parameters as object,
  $transform as function(object) as item*,
  $expected as item*,
  $options as object?
) as item
{
  let $request := switch($options.Format)
                  case "xml" return test:invoke-xml($endpoint, $parameters)
                  default return test:invoke($endpoint, $parameters)
  let $status as integer := $request[1]
  let $actual as item* := $transform($request[2])
  let $expected as item* := if($options.TrimIdField) then test:trim-ids($expected) else $expected
  let $actual as item* := if($options.TrimIdField) then test:trim-ids($actual) else $actual
  return if($options.NoArrayOrder)
          then test:assert-deep-equal-no-array-order($expected, $actual, $status, test:url($endpoint, $parameters))
          else test:assert-deep-equal($expected, $actual, $status, test:url($endpoint, $parameters))
};

declare %an:sequential function test:check-all-success($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
};

declare %private function test:trim-ids($input as item*) as item*
{
  for $i in $input
  return typeswitch($i)
          case object return {|
                                 for $key in keys($i)
                                 where $key ne "_id"
                                 return {$key: test:trim-ids($i.$key) }
                               |}
          case array return [ test:trim-ids($i[]) ]
          default return $i
};

(:    return
        if ($actual eq $expected)
        then true
        else "false [Actual="||$actual||", Expected="||$expected ||"]":)

declare function test:assert-eq(
    $expected as atomic,
    $actual as atomic?,
    $status as integer,
    $url as string) as item
{
    switch(true)
    case $status ne 200 return { "url": $url, status: $status }
    case try {$expected eq $actual} catch * { false } return true
    default return
    {
        "url": $url,
        "expected": $expected,
        "actual": $actual
    }
};

declare function test:assert-deep-equal(
    $expected as item*,
    $actual as item*,
    $status as integer,
    $url as string) as item
{
    switch(true)
    case $status ne 200 return { "url": $url, status: $status }
    case functx:sequence-deep-equal($expected, $actual) return true
    default return
    {
        "url": $url,
        "expected": typeswitch($expected) case json-item return $expected default return serialize($expected),
        "actual": typeswitch($actual) case json-item return $actual default return serialize($actual)
    }
};

declare function test:assert-deep-equal-no-array-order(
    $expected as item*,
    $actual as item*,
    $status as integer,
    $url as string) as item
{
    switch(true)
    case $status ne 200 return { "url": $url, status: $status }
    case test:deep-equal-no-array-order($expected, $actual) return true
    default return
    {
        "url": $url,
        "expected": typeswitch($expected) case json-item return $expected default return serialize($expected),
        "actual": typeswitch($actual) case json-item return $actual default return serialize($actual)
    }
};

declare function test:deep-equal-no-array-order(
    $expected as item*,
    $actual as item*) as boolean
{
    if(count($expected) ne count($actual))
    then false
    else
      every $i in 1 to count($expected)
      satisfies
        let $expected as item := $expected[$i]
        let $actual as item := $actual[$i]
        return
        typeswitch(($expected, $actual))
        case object* return
          switch(true)
          case not (every $key in keys($expected) satisfies keys($actual) = $key)
            return false
          case not (every $key in keys($actual) satisfies keys($expected) = $key)
            return false
          default return
            let $recursive-calls :=
                for $key in keys($expected)
                return test:deep-equal-no-array-order($expected.$key, $actual.$key)
            return if (every $item in $recursive-calls satisfies $item)
                    then true
                    else false
        case array* return
          if(not size($expected) eq size($actual))
          then false
          else
            let $expected-items as item* :=
              for $e as item in $expected[]
              let $s as string := serialize($e)
              order by $s
              return $e
            let $actual-items as item* :=
              for $a as item in $actual[]
              let $s as string := serialize($a)
              order by $s
              return $a
            let $size as integer := size($expected)
            return every $i in 1 to $size
                   satisfies test:deep-equal-no-array-order(
                       $expected-items[$i],
                       $actual-items[$i])
        default return deep-equal($expected, $actual)
};

declare function test:assert-eq-array(
    $expected as array,
    $actual as array,
    $status as integer,
    $url as string) as item
{
    let $diff := try {
            {
                actual: [
                    for $a in flatten($actual)
                    where not($a = flatten($expected))
                    return $a ],
                actualDuplicates: [
                    for $a in flatten($actual)
                    where $a = flatten($expected) and count(flatten($actual)[$$ eq $a]) ne count(flatten($expected)[$$ eq $a])
                    return $a ],
                expected: [
                    for $e in flatten($expected)
                    where not($e = flatten($actual))
                    return $e ]
            }
        } catch * { "error: " || $err:description }

    return
        switch(true)
        case $status ne 200 return { "url": $url, status: $status }
        case empty(($diff.actual[],$diff.actualDuplicates[],$diff.expected[])) return true
        default return
        {
            "url": $url,
            "diff": $diff,
            "expected": $expected,
            "actual": $actual
        }
};

jsoniq version "1.0";
module namespace test = "http://apps.28.io/test";
import module namespace http-client = "http://zorba.io/modules/http-client";
import module namespace request = "http://www.28msec.com/modules/http-request";
import module namespace response = "http://www.28msec.com/modules/http-response";
import module namespace config = "http://apps.28.io/config";
import module namespace functx = "http://www.functx.com";
import module namespace backend = "http://apps.28.io/backend";

declare function test:url($endpoint as string, $parameters as object) as string
{
  backend:url(
    $endpoint,
    {|
      $config:test-token,
      $parameters
    |}
  )
};

declare %an:nondeterministic function test:invoke($endpoint as string, $parameters as object) as item*
{
  let $url as string:= test:url($endpoint, $parameters)
  let $response as object := http-client:get($url)
  return ($response.status, parse-json($response.body.content))
};

declare %an:nondeterministic function test:invoke-xml($endpoint as string, $parameters as object) as item*
{
  let $url as string:= test:url($endpoint, $parameters)
  let $response as object := http-client:get($url)
  return ($response.status, parse-xml($response.body.content))
};

declare %an:sequential function test:invoke-body($endpoint as string, $parameters as object, $body as string) as item*
{
  let $url as string:= test:url($endpoint, $parameters)
  let $response as object := http-client:post($url, $body, "application/x-www-form-urlencoded")
  return ($response.status, parse-json($response.body.content))
};

declare %an:nondeterministic function test:invoke-raw($endpoint as string, $parameters as object) as object
{
  let $url as string:= test:url($endpoint, $parameters)
  return http-client:get($url)
};

declare %an:nondeterministic function test:invoke-public($endpoint as string, $parameters as object) as item*
{
  let $url as string:= test:url($endpoint, $parameters)
  let $response as object := http-client:get($url)
  return ($response.status, parse-json($response.body.content))
};

declare %an:nondeterministic function test:get-expected-result(
  $expected-file as string
) as item*
{
  parse-json(
    http-client:get("http://" || request:server-name() || ":" || request:server-port() ||
                    "/test/" || $expected-file).body.content)
};

declare %an:nondeterministic function test:get-expected-result-xml(
  $expected-file as string
) as item*
{
  parse-xml(
    http-client:get("http://" || request:server-name() || ":" || request:server-port() ||
                    "/test/" || $expected-file).body.content)
};

declare %an:nondeterministic function test:invoke-and-assert-deep-equal(
  $endpoint as string,
  $parameters as object,
  $transform as function(object) as item*,
  $expected as item*
) as item
{
  test:invoke-and-assert-deep-equal($endpoint, $parameters, $transform, $expected, {})
};

declare %an:sequential function test:invoke-and-assert-status(
  $endpoint as string,
  $parameters as object,
  $expected-status as integer
) as item
{
  let $url as string:= test:url($endpoint, $parameters)
  let $response as object := http-client:send-request({href: $url, method: "GET", "options" : { "status-only": true, "follow-redirect" : false } })
  let $actual-status := $response.status
  return if($actual-status eq $expected-status)
         then true
         else {
           url: test:url($endpoint, $parameters),
           expected: $expected-status,
           actual: $actual-status
         }
};

(:
  Note on NoArrayOrder option: it allows for false positives in case arrays have duplicate elements. The number
  of occurrences of duplicate elements is not checked.
:)
declare %an:nondeterministic function test:invoke-and-assert-deep-equal(
  $endpoint as string,
  $parameters as object,
  $transform as function(object) as item*,
  $expected as item*,
  $options as object?
) as item
{
  let $request := switch($options.Format)
                  case "xml" return test:invoke-xml($endpoint, $parameters)
                  default return test:invoke($endpoint, $parameters)
  let $status as integer := $request[1]
  let $actual as item* := $transform($request[2])
  let $expected as item* := if($options.TrimIdField) then test:trim-ids($expected) else $expected
  let $actual as item* := if($options.TrimIdField) then test:trim-ids($actual) else $actual
  return if($options.NoArrayOrder)
          then test:assert-deep-equal-no-array-order($expected, $actual, $status, test:url($endpoint, $parameters))
          else test:assert-deep-equal($expected, $actual, $status, test:url($endpoint, $parameters))
};

declare %an:sequential function test:check-all-success($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
};

declare %private function test:trim-ids($input as item*) as item*
{
  for $i in $input
  return typeswitch($i)
          case object return {|
                                 for $key in keys($i)
                                 where $key ne "_id"
                                 return {$key: test:trim-ids($i.$key) }
                               |}
          case array return [ test:trim-ids($i[]) ]
          default return $i
};

(:    return
        if ($actual eq $expected)
        then true
        else "false [Actual="||$actual||", Expected="||$expected ||"]":)

declare function test:assert-eq(
    $expected as atomic,
    $actual as atomic?,
    $status as integer,
    $url as string) as item
{
    switch(true)
    case $status ne 200 return { "url": $url, status: $status }
    case try {$expected eq $actual} catch * { false } return true
    default return
    {
        "url": $url,
        "expected": $expected,
        "actual": $actual
    }
};

declare function test:assert-deep-equal(
    $expected as item*,
    $actual as item*,
    $status as integer,
    $url as string) as item
{
    switch(true)
    case $status ne 200 return { "url": $url, status: $status }
    case functx:sequence-deep-equal($expected, $actual) return true
    default return
    {
        "url": $url,
        "expected": typeswitch($expected) case json-item return $expected default return serialize($expected),
        "actual": typeswitch($actual) case json-item return $actual default return serialize($actual)
    }
};

declare function test:assert-deep-equal-no-array-order(
    $expected as item*,
    $actual as item*,
    $status as integer,
    $url as string) as item
{
    switch(true)
    case $status ne 200 return { "url": $url, status: $status }
    case test:deep-equal-no-array-order($expected, $actual) return true
    default return
    {
        "url": $url,
        "expected": typeswitch($expected) case json-item return $expected default return serialize($expected),
        "actual": typeswitch($actual) case json-item return $actual default return serialize($actual)
    }
};

declare function test:deep-equal-no-array-order(
    $expected as item*,
    $actual as item*) as boolean
{
    if(count($expected) ne count($actual))
    then false
    else
      every $i in 1 to count($expected)
      satisfies
        let $expected as item := $expected[$i]
        let $actual as item := $actual[$i]
        return
        typeswitch(($expected, $actual))
        case object* return
          switch(true)
          case not (every $key in keys($expected) satisfies keys($actual) = $key)
            return false
          case not (every $key in keys($actual) satisfies keys($expected) = $key)
            return false
          default return
            let $recursive-calls :=
                for $key in keys($expected)
                return test:deep-equal-no-array-order($expected.$key, $actual.$key)
            return if (every $item in $recursive-calls satisfies $item)
                    then true
                    else false
        case array* return
          if(not size($expected) eq size($actual))
          then false
          else
            let $expected-items as item* :=
              for $e as item in $expected[]
              let $s as string := serialize($e)
              order by $s
              return $e
            let $actual-items as item* :=
              for $a as item in $actual[]
              let $s as string := serialize($a)
              order by $s
              return $a
            let $size as integer := size($expected)
            return every $i in 1 to $size
                   satisfies test:deep-equal-no-array-order(
                       $expected-items[$i],
                       $actual-items[$i])
        default return deep-equal($expected, $actual)
};

declare function test:assert-eq-array(
    $expected as array,
    $actual as array,
    $status as integer,
    $url as string) as item
{
    let $diff := try {
            {
                actual: [
                    for $a in flatten($actual)
                    where not($a = flatten($expected))
                    return $a ],
                actualDuplicates: [
                    for $a in flatten($actual)
                    where $a = flatten($expected) and count(flatten($actual)[$$ eq $a]) ne count(flatten($expected)[$$ eq $a])
                    return $a ],
                expected: [
                    for $e in flatten($expected)
                    where not($e = flatten($actual))
                    return $e ]
            }
        } catch * { "error: " || $err:description }

    return
        switch(true)
        case $status ne 200 return { "url": $url, status: $status }
        case empty(($diff.actual[],$diff.actualDuplicates[],$diff.expected[])) return true
        default return
        {
            "url": $url,
            "diff": $diff,
            "expected": $expected,
            "actual": $actual
        }
};


remove trace

remove trace

jsoniq version "1.0";
(:
 : Copyright 2012 28msec Inc.
 :)

(:~
 :
This module provides a driver to access
 : ElasticSearch.

 :
 : @author Dennis Knochenwefel
 :)
module namespace es = "http://28.io/modules/elasticsearch";

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

declare namespace ver = "http://zorba.io/options/versioning";

declare option ver:module-version "1.0";

declare %private variable $es:CREDENTIALS-CATEGORY as xs:string :=
    "ElasticSearch";
declare %private variable $es:RETRY-OPTIONS :=
  {
    retry :
    {
      "delay" : [1000, 2000, 4000],
      "on-connection-error" : true(),
      "on-statuses" : [ 500, 503 ]
    }
  };

declare function es:connection($credentials-name as xs:string?)
as object?
{
  let $credentials as object? := credentials:credentials(
      $es:CREDENTIALS-CATEGORY,
      $credentials-name)
  where exists($credentials)
  return
      let $urls as string+ :=
        for $url in $credentials.urls[]
        return $url || ("/"[not ends-with($url, "/")])
      let $protocol as string := ($credentials.protocol, "https")[1]
      let $user as string? := $credentials.user
      let $password as string? := $credentials.pass
      let $authentication as string? :=
        (
          encode-for-uri($user) || ":" || encode-for-uri($password) || "@"
        )[exists($user) and exists($password)]
      return
        {
          "urls": [
            for $url in $urls
            return $protocol || "://" || $authentication || $url
          ]
        }
};

declare %private function es:url(
  $connection as object,
  $paths as string*
) as string
{
  es:url($connection, $paths, ())
};

declare %private function es:url(
  $connection as object,
  $paths as string*,
  $parameters as string*
) as string
{
  let $server as string := $connection.urls[][1]
  let $parameters as string* :=
    for $parameter in $parameters
    return encode-for-uri($parameter)
  let $path as string := string-join($paths, "/")
  let $query as string := string-join($parameters, "&")
  return
    $server || string-join(($path, $query), "?")
};

declare %private function es:request(
  $method as string,
  $url as string,
  $body as item?
) as object
{
  {|
    {
      "method": $method,
      "href": $url,
      "options": $es:RETRY-OPTIONS
    },
    switch(true)

    case empty($body) return ()

    case $body instance of string
    return
      {
        "body":
        {
          "media-type": "text/plain",
          "content": $body
        }
      }

    case $body instance of object
    case $body instance of array
    return
      {
        "body":
        {
          "media-type": "application/json",
          "content": serialize($body)
        }
      }

    default return
      error(xs:QName("es:UNKNOWN_BODY_TYPE"),
            "cannot create request from body content",
            $body)
  |}
};

declare %an:nondeterministic function es:exists(
  $connection as object,
  $index as string
) as boolean
{
  es:exists($connection, $index, (), ())
};

declare %an:nondeterministic function es:exists(
  $connection as object,
  $index as string,
  $type as string?
) as boolean
{
  es:exists($connection, $index, $type, ())
};

declare %an:nondeterministic function es:exists(
  $connection as object,
  $index as string,
  $type as string?,
  $id as string?
) as boolean
{
  let $url as string := es:url($connection, ($index, $type, $id))
  let $request as object := es:request("HEAD", $url, ())
  let $response as object := http:send-nondeterministic-request($request)
  return
    switch ($response.status)
    case 200
    case 204 return true
    case 404 return false
    default return error(xs:QName("es:SERVER_ERROR"),
                         $response.body.content,
                         $response)
};

declare %an:sequential function es:create(
  $connection as object,
  $index as string
) as object
{
  es:create($connection, $index, ())
};

declare %an:sequential function es:create(
  $connection as object,
  $index as string,
  $mapping as object?
) as object
{
  let $method as string :=
    if(empty($mapping))
    then "POST"
    else "PUT"
  let $url as string := es:url($connection, ($index))
  let $request as object := es:request($method, $url, $mapping)
  let $response as object := http:send-request($request)
  return
    if($response.status eq 200)
    then parse-json($response.body.content)
    else error(xs:QName("es:SERVER_ERROR"), $response.body.content, $response)
};

declare %an:sequential function es:create(
  $connection as object,
  $index as string,
  $type as string,
  $mapping as object
) as object?
{
  let $url as string := es:url($connection, ($index, "_mapping", $type))
  let $request as object := es:request("PUT", $url, $mapping)
  let $response as object := http:send-request($request)
  return
    if($response.status eq 200)
    then parse-json($response.body.content)
    else error(xs:QName("es:SERVER_ERROR"), $response.body.content, $response)
};

declare %an:sequential function es:delete(
  $connection as object,
  $index as string
) as object?
{
  es:delete($connection, $index, (),())
};

declare %an:sequential function es:delete(
  $connection as object,
  $index as string,
  $type as string?
) as object?
{
  es:delete($connection, $index, $type, ())
};

declare %an:sequential function es:delete(
  $connection as object,
  $index as string,
  $type as string?,
  $id as string?
) as object?
{
  let $url as string := es:url($connection, ($index, $type, $id))
  let $request as object := es:request("DELETE", $url, ())
  let $response as object := http:send-request($request)
  return
    if($response.status eq 200)
    then parse-json($response.body.content)
    else error(xs:QName("es:SERVER_ERROR"), $response.body.content, $response)
};

declare %an:nondeterministic function es:get(
  $connection as object,
  $index as string,
  $type as string,
  $id as string
) as object?
{
  let $url as string := es:url($connection, ($index, $type, $id))
  let $response as object := http:get($url)
  return
    switch ($response.status)
    case 200 return parse-json($response.body.content)
    case 404 return ()
    default return error(xs:QName("es:CONNECTION_ERROR"),
                         $response.body.content,
                         $response)
};

declare %an:sequential function es:insert(
  $connection as object,
  $index as string,
  $type as string,
  $doc as object
) as object?
{
  let $id as string? := $doc._id
  return es:insert($connection, $index, $type, $id, $doc)
};

declare %an:sequential function es:insert(
  $connection as object,
  $index as string,
  $type as string,
  $id as string?,
  $doc as object
) as object?
{
  let $method as string := if(empty($id)) then "POST" else "PUT"
  let $url as string := es:url($connection, ($index, $type, $id))
  let $request as object := es:request($method, $url, $doc)
  let $response as object := http:send-request($request)
  return
    switch ($response.status)
    case 200
    case 201 return parse-json($response.body.content)
    default return error(xs:QName("es:CONNECTION_ERROR"),
                         $response.body.content,
                         $response)
};

declare %an:nondeterministic function es:list(
  $connection as object,
  $index as string,
  $type as string
) as object?
{
  let $url as string := es:url($connection, ($index, $type, "_search"))
  let $request as object := es:request("GET", $url, ())
  let $response as object := http:send-nondeterministic-request($request)
  return
    if($response.status eq 200)
    then parse-json($response.body.content)
    else error(xs:QName("es:CONNECTION_ERROR"),
                        $response.body.content,
                        $response)
};

declare %an:nondeterministic function es:search(
  $connection as object,
  $index as string,
  $type as string,
  $query as object
) as object?
{
  let $url as string := es:url($connection, ($index, $type, "_search"))
  let $request as object := es:request("GET", $url, $query)
  let $response as object := http:send-nondeterministic-request($request)
  return
    if($response.status eq 200)
    then parse-json($response.body.content)
    else error(xs:QName("es:CONNECTION_ERROR"),
                        $response.body.content,
                        $response)
};

jsoniq version "1.0";
(:
 : Copyright 2012 28msec Inc.
 :)

(:~
 :
This module provides a driver to access
 : ElasticSearch.

 :
 : @author Dennis Knochenwefel
 :)
module namespace es = "http://28.io/modules/elasticsearch";

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

declare namespace ver = "http://zorba.io/options/versioning";

declare option ver:module-version "1.0";

declare %private variable $es:CREDENTIALS-CATEGORY as xs:string :=
    "ElasticSearch";
declare %private variable $es:RETRY-OPTIONS :=
  {
    retry :
    {
      "delay" : [1000, 2000, 4000],
      "on-connection-error" : true(),
      "on-statuses" : [ 500, 503 ]
    }
  };

declare function es:connection($credentials-name as xs:string?)
as object?
{
  let $credentials as object? := credentials:credentials(
      $es:CREDENTIALS-CATEGORY,
      $credentials-name)
  where exists($credentials)
  return
      let $urls as string+ :=
        for $url in $credentials.urls[]
        return $url || ("/"[not ends-with($url, "/")])
      let $protocol as string := ($credentials.protocol, "https")[1]
      let $user as string? := $credentials.user
      let $password as string? := $credentials.pass
      let $authentication as string? :=
        (
          encode-for-uri($user) || ":" || encode-for-uri($password) || "@"
        )[exists($user) and exists($password)]
      return
        {
          "urls": [
            for $url in $urls
            return $protocol || "://" || $authentication || $url
          ]
        }
};

declare %private function es:url(
  $connection as object,
  $paths as string*
) as string
{
  es:url($connection, $paths, ())
};

declare %private function es:url(
  $connection as object,
  $paths as string*,
  $parameters as string*
) as string
{
  let $server as string := $connection.urls[][1]
  let $parameters as string* :=
    for $parameter in $parameters
    return encode-for-uri($parameter)
  let $path as string := string-join($paths, "/")
  let $query as string := string-join($parameters, "&")
  return
    $server || string-join(($path, $query), "?")
};

declare %private function es:request(
  $method as string,
  $url as string,
  $body as item?
) as object
{
  {|
    {
      "method": $method,
      "href": $url,
      "options": $es:RETRY-OPTIONS
    },
    switch(true)

    case empty($body) return ()

    case $body instance of string
    return
      {
        "body":
        {
          "media-type": "text/plain",
          "content": $body
        }
      }

    case $body instance of object
    case $body instance of array
    return
      {
        "body":
        {
          "media-type": "application/json",
          "content": serialize($body)
        }
      }

    default return
      error(xs:QName("es:UNKNOWN_BODY_TYPE"),
            "cannot create request from body content",
            $body)
  |}
};

declare %an:nondeterministic function es:exists(
  $connection as object,
  $index as string
) as boolean
{
  es:exists($connection, $index, (), ())
};

declare %an:nondeterministic function es:exists(
  $connection as object,
  $index as string,
  $type as string?
) as boolean
{
  es:exists($connection, $index, $type, ())
};

declare %an:nondeterministic function es:exists(
  $connection as object,
  $index as string,
  $type as string?,
  $id as string?
) as boolean
{
  let $url as string := es:url($connection, ($index, $type, $id))
  let $request as object := es:request("HEAD", trace($url, "$url"), ())
  let $response as object := http:send-nondeterministic-request($request)
  return
    switch ($response.status)
    case 200
    case 204 return true
    case 404 return false
    default return error(xs:QName("es:SERVER_ERROR"),
                         trace($response, "$response").body.content,
                         $response)
};

declare %an:sequential function es:create(
  $connection as object,
  $index as string
) as object
{
  es:create($connection, $index, ())
};

declare %an:sequential function es:create(
  $connection as object,
  $index as string,
  $mapping as object?
) as object
{
  let $method as string :=
    if(empty($mapping))
    then "POST"
    else "PUT"
  let $url as string := es:url($connection, ($index))
  let $request as object := es:request($method, $url, $mapping)
  let $response as object := http:send-request($request)
  return
    if($response.status eq 200)
    then parse-json($response.body.content)
    else error(xs:QName("es:SERVER_ERROR"), $response.body.content, $response)
};

declare %an:sequential function es:create(
  $connection as object,
  $index as string,
  $type as string,
  $mapping as object
) as object?
{
  let $url as string := es:url($connection, ($index, "_mapping", $type))
  let $request as object := es:request("PUT", $url, $mapping)
  let $response as object := http:send-request($request)
  return
    if($response.status eq 200)
    then parse-json($response.body.content)
    else error(xs:QName("es:SERVER_ERROR"), $response.body.content, $response)
};

declare %an:sequential function es:delete(
  $connection as object,
  $index as string
) as object?
{
  es:delete($connection, $index, (),())
};

declare %an:sequential function es:delete(
  $connection as object,
  $index as string,
  $type as string?
) as object?
{
  es:delete($connection, $index, $type, ())
};

declare %an:sequential function es:delete(
  $connection as object,
  $index as string,
  $type as string?,
  $id as string?
) as object?
{
  let $url as string := es:url($connection, ($index, $type, $id))
  let $request as object := es:request("DELETE", $url, ())
  let $response as object := http:send-request($request)
  return
    if($response.status eq 200)
    then parse-json($response.body.content)
    else error(xs:QName("es:SERVER_ERROR"), $response.body.content, $response)
};

declare %an:nondeterministic function es:get(
  $connection as object,
  $index as string,
  $type as string,
  $id as string
) as object?
{
  let $url as string := es:url($connection, ($index, $type, $id))
  let $response as object := http:get($url)
  return
    switch ($response.status)
    case 200 return parse-json($response.body.content)
    case 404 return ()
    default return error(xs:QName("es:CONNECTION_ERROR"),
                         $response.body.content,
                         $response)
};

declare %an:sequential function es:insert(
  $connection as object,
  $index as string,
  $type as string,
  $doc as object
) as object?
{
  let $id as string? := $doc._id
  return es:insert($connection, $index, $type, $id, $doc)
};

declare %an:sequential function es:insert(
  $connection as object,
  $index as string,
  $type as string,
  $id as string?,
  $doc as object
) as object?
{
  let $method as string := if(empty($id)) then "POST" else "PUT"
  let $url as string := es:url($connection, ($index, $type, $id))
  let $request as object := es:request($method, $url, $doc)
  let $response as object := http:send-request($request)
  return
    switch ($response.status)
    case 200
    case 201 return parse-json($response.body.content)
    default return error(xs:QName("es:CONNECTION_ERROR"),
                         $response.body.content,
                         $response)
};

declare %an:nondeterministic function es:list(
  $connection as object,
  $index as string,
  $type as string
) as object?
{
  let $url as string := es:url($connection, ($index, $type, "_search"))
  let $request as object := es:request("GET", $url, ())
  let $response as object := http:send-nondeterministic-request($request)
  return
    if($response.status eq 200)
    then parse-json($response.body.content)
    else error(xs:QName("es:CONNECTION_ERROR"),
                        $response.body.content,
                        $response)
};

declare %an:nondeterministic function es:search(
  $connection as object,
  $index as string,
  $type as string,
  $query as object
) as object?
{
  let $url as string := es:url($connection, ($index, $type, "_search"))
  let $request as object := es:request("GET", $url, $query)
  let $response as object := http:send-nondeterministic-request($request)
  return
    if($response.status eq 200)
    then parse-json($response.body.content)
    else error(xs:QName("es:CONNECTION_ERROR"),
                        $response.body.content,
                        $response)
};