diff --git a/.fern/metadata.json b/.fern/metadata.json index 93e7eb0c..646cdc3e 100644 --- a/.fern/metadata.json +++ b/.fern/metadata.json @@ -1,7 +1,7 @@ { - "cliVersion": "5.106.0", + "cliVersion": "5.131.2", "generatorName": "fernapi/fern-java-sdk", - "generatorVersion": "4.13.2", + "generatorVersion": "4.19.2", "generatorConfig": { "enable-inline-types": false, "client-class-name": "PhenomlClient", @@ -10,10 +10,10 @@ "enable-wire-tests": true, "publish-to": "central" }, - "originGitCommit": "4a08550f5db230949c7423d0ce5aa7055e8f0d65", + "originGitCommit": "70c52e949023cff024c8c9dfaccaecd12617548e", "originGitCommitIsDirty": true, "invokedBy": "ci", "requestedVersion": "AUTO", "ciProvider": "unknown", - "sdkVersion": "17.13.0" + "sdkVersion": "17.13.1" } \ No newline at end of file diff --git a/.fern/replay.lock b/.fern/replay.lock index c617af9d..f08413d9 100644 --- a/.fern/replay.lock +++ b/.fern/replay.lock @@ -66,5 +66,11 @@ generations: cli_version: unknown generator_versions: fernapi/fern-java-sdk: 4.13.2 -current_generation: 4258b268221e8a5a749caef1d27a3736460088e1 + - commit_sha: ed63cfb20bc0aea36becd93c57c118f5b1fadfa1 + tree_hash: 04ee18ec6900646539c3789427a9a4e97e9cf62c + timestamp: 2026-09-22T21:09:03.164Z + cli_version: unknown + generator_versions: + fernapi/fern-java-sdk: 4.19.2 +current_generation: ed63cfb20bc0aea36becd93c57c118f5b1fadfa1 patches: [] diff --git a/build.gradle b/build.gradle index d6c11758..66766750 100644 --- a/build.gradle +++ b/build.gradle @@ -58,7 +58,7 @@ java { group = 'com.phenoml.maven' -version = '17.13.0' +version = '17.13.1' jar { dependsOn(":generatePomFileForMavenPublication") @@ -89,7 +89,7 @@ publishing { maven(MavenPublication) { groupId = 'com.phenoml.maven' artifactId = 'phenoml-java-sdk' - version = '17.13.0' + version = '17.13.1' from components.java pom { name = 'phenoml' diff --git a/changelog.md b/changelog.md index be7eeed3..5ba5296f 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,5 @@ +## [17.13.1] - 2026-09-22 + ## [17.13.0] - 2026-08-26 ### Added - **`PatientReference`** — new staged-builder type with required `system` (identifier namespace) and `value` (identifier value) fields for supplying a structured patient identifier on extraction requests. diff --git a/code-examples.json b/code-examples.json index ced7dcbf..48c7c83a 100644 --- a/code-examples.json +++ b/code-examples.json @@ -2,8 +2,8 @@ "metadata": { "language": "java", "packageName": "com.phenoml.maven:phenoml-java-sdk", - "sdkVersion": "17.13.0", - "specCommit": "4a08550f5db230949c7423d0ce5aa7055e8f0d65", + "sdkVersion": "17.13.1", + "specCommit": "70c52e949023cff024c8c9dfaccaecd12617548e", "generatorName": "fernapi/fern-java-sdk" }, "renderRules": { @@ -3222,6 +3222,216 @@ } } }, + "POST /lang2fhir/batch": { + "httpMethod": "POST", + "httpPath": "/lang2fhir/batch", + "request": { + "body": { + "request_id": "submit-2025-09-02-batch-001" + } + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.lang2FhirBatch().create(CreateBatchRequest.builder(){{__body__}}.build())", + "params": [], + "body": { + "fieldSeparator": "", + "fields": [ + { + "jsonKey": "request_id", + "fieldTemplate": ".requestId({{value}})", + "kind": "string", + "required": false + } + ] + } + } + }, + "GET /lang2fhir/batch": { + "httpMethod": "GET", + "httpPath": "/lang2fhir/batch", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.lang2FhirBatch().list(ListRequest.builder(){{__body__}}.build())", + "params": [], + "body": { + "fieldSeparator": "", + "fields": [ + { + "jsonKey": "cursor", + "fieldTemplate": ".cursor({{value}})", + "kind": "string", + "required": false + }, + { + "jsonKey": "limit", + "fieldTemplate": ".limit({{value}})", + "kind": "number", + "required": false + } + ] + } + } + }, + "POST /lang2fhir/batch/{job_id}/items": { + "httpMethod": "POST", + "httpPath": "/lang2fhir/batch/{job_id}/items", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.lang2FhirBatch().uploadItem({{job_id}})", + "params": [ + { + "name": "job_id", + "kind": "string" + } + ] + } + }, + "POST /lang2fhir/batch/{job_id}/finalize": { + "httpMethod": "POST", + "httpPath": "/lang2fhir/batch/{job_id}/finalize", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.lang2FhirBatch().finalize({{job_id}})", + "params": [ + { + "name": "job_id", + "kind": "string" + } + ] + } + }, + "POST /lang2fhir/batch/{job_id}/cancel": { + "httpMethod": "POST", + "httpPath": "/lang2fhir/batch/{job_id}/cancel", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.lang2FhirBatch().cancel({{job_id}})", + "params": [ + { + "name": "job_id", + "kind": "string" + } + ] + } + }, + "GET /lang2fhir/batch/{job_id}": { + "httpMethod": "GET", + "httpPath": "/lang2fhir/batch/{job_id}", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.lang2FhirBatch().get({{job_id}}, GetRequest.builder(){{__body__}}.build())", + "params": [ + { + "name": "job_id", + "kind": "string" + } + ], + "body": { + "fieldSeparator": "", + "fields": [ + { + "jsonKey": "cursor", + "fieldTemplate": ".cursor({{value}})", + "kind": "string", + "required": false + }, + { + "jsonKey": "limit", + "fieldTemplate": ".limit({{value}})", + "kind": "number", + "required": false + } + ] + } + } + }, + "GET /lang2fhir/batch/{job_id}/results": { + "httpMethod": "GET", + "httpPath": "/lang2fhir/batch/{job_id}/results", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.lang2FhirBatch().getResults({{job_id}}, GetResultsRequest.builder(){{__body__}}.build())", + "params": [ + { + "name": "job_id", + "kind": "string" + } + ], + "body": { + "fieldSeparator": "", + "fields": [ + { + "jsonKey": "cursor", + "fieldTemplate": ".cursor({{value}})", + "kind": "string", + "required": false + }, + { + "jsonKey": "limit", + "fieldTemplate": ".limit({{value}})", + "kind": "number", + "required": false + } + ] + } + } + }, + "GET /lang2fhir/batch/{job_id}/results/{item_id}": { + "httpMethod": "GET", + "httpPath": "/lang2fhir/batch/{job_id}/results/{item_id}", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.lang2FhirBatch().getResult({{job_id}}, {{item_id}})", + "params": [ + { + "name": "job_id", + "kind": "string" + }, + { + "name": "item_id", + "kind": "string" + } + ] + } + }, "POST /cohort": { "httpMethod": "POST", "httpPath": "/cohort", @@ -3335,7 +3545,10 @@ "auto", "appointment", "condition-encounter-diagnosis", + "familymemberhistory", + "medicationadministration", "medicationrequest", + "medicationstatement", "careplan", "condition-problems-health-concerns", "coverage", @@ -3451,6 +3664,90 @@ "kind": "string", "required": false }, + { + "jsonKey": "primary_patient", + "fieldTemplate": ".primaryPatient({{value}})", + "kind": "object", + "required": false, + "nested": { + "fieldSeparator": "", + "fields": [ + { + "jsonKey": "identifier", + "fieldTemplate": ".identifier({{value}})", + "kind": "object", + "required": false, + "nested": { + "fieldSeparator": "", + "fields": [ + { + "jsonKey": "system", + "fieldTemplate": ".system({{value}})", + "kind": "string", + "required": true + }, + { + "jsonKey": "value", + "fieldTemplate": ".value({{value}})", + "kind": "string", + "required": true + } + ], + "wrap": "PatientReference.builder(){{__body__}}.build()" + } + }, + { + "jsonKey": "name", + "fieldTemplate": ".name({{value}})", + "kind": "object", + "required": false, + "nested": { + "fieldSeparator": "", + "fields": [ + { + "jsonKey": "family", + "fieldTemplate": ".family({{value}})", + "kind": "string", + "required": false + }, + { + "jsonKey": "given", + "fieldTemplate": ".given({{value}})", + "kind": "list", + "required": false, + "items": { + "jsonKey": "", + "fieldTemplate": "{{value}}", + "kind": "string", + "required": true + } + } + ], + "wrap": "PrimaryPatientName.builder(){{__body__}}.build()" + } + }, + { + "jsonKey": "birthDate", + "fieldTemplate": ".birthDate({{value}})", + "kind": "string", + "required": false + }, + { + "jsonKey": "gender", + "fieldTemplate": ".gender({{value}})", + "kind": "enum", + "required": false, + "enumValues": [ + "male", + "female", + "other", + "unknown" + ] + } + ], + "wrap": "PrimaryPatient.builder(){{__body__}}.build()" + } + }, { "jsonKey": "patient_reference", "fieldTemplate": ".patientReference({{value}})", @@ -3641,7 +3938,7 @@ "body": { "version": "R4", "resource": "questionnaire", - "content": "JVBERi0xLjQKJeLjz9MK...(base64-encoded PDF or image bytes)" + "content": "JVBERi0xLjQKJeLjz9MK...(base64-encoded document bytes)" } }, "response": { @@ -3807,7 +4104,7 @@ "request": { "body": { "version": "R4", - "content": "JVBERi0xLjQKJeLjz9MK...(base64-encoded PDF or image bytes)", + "content": "JVBERi0xLjQKJeLjz9MK...(base64-encoded document bytes)", "provider": "medplum", "config": { "split_classifications": [ @@ -3989,6 +4286,90 @@ "kind": "string", "required": false }, + { + "jsonKey": "primary_patient", + "fieldTemplate": ".primaryPatient({{value}})", + "kind": "object", + "required": false, + "nested": { + "fieldSeparator": "", + "fields": [ + { + "jsonKey": "identifier", + "fieldTemplate": ".identifier({{value}})", + "kind": "object", + "required": false, + "nested": { + "fieldSeparator": "", + "fields": [ + { + "jsonKey": "system", + "fieldTemplate": ".system({{value}})", + "kind": "string", + "required": true + }, + { + "jsonKey": "value", + "fieldTemplate": ".value({{value}})", + "kind": "string", + "required": true + } + ], + "wrap": "PatientReference.builder(){{__body__}}.build()" + } + }, + { + "jsonKey": "name", + "fieldTemplate": ".name({{value}})", + "kind": "object", + "required": false, + "nested": { + "fieldSeparator": "", + "fields": [ + { + "jsonKey": "family", + "fieldTemplate": ".family({{value}})", + "kind": "string", + "required": false + }, + { + "jsonKey": "given", + "fieldTemplate": ".given({{value}})", + "kind": "list", + "required": false, + "items": { + "jsonKey": "", + "fieldTemplate": "{{value}}", + "kind": "string", + "required": true + } + } + ], + "wrap": "PrimaryPatientName.builder(){{__body__}}.build()" + } + }, + { + "jsonKey": "birthDate", + "fieldTemplate": ".birthDate({{value}})", + "kind": "string", + "required": false + }, + { + "jsonKey": "gender", + "fieldTemplate": ".gender({{value}})", + "kind": "enum", + "required": false, + "enumValues": [ + "male", + "female", + "other", + "unknown" + ] + } + ], + "wrap": "PrimaryPatient.builder(){{__body__}}.build()" + } + }, { "jsonKey": "patient_reference", "fieldTemplate": ".patientReference({{value}})", @@ -4574,6 +4955,140 @@ ] } }, + "POST /fhir/implementation-guides/{name}/versions": { + "httpMethod": "POST", + "httpPath": "/fhir/implementation-guides/{name}/versions", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.implementationGuides().implementationguides().createVersion({{name}}, CreateCanonicalImplementationGuideRequest.builder(){{__body__}}.build())", + "params": [ + { + "name": "name", + "kind": "string" + } + ], + "body": { + "fieldSeparator": "", + "fields": [ + { + "jsonKey": "implementation_guide", + "fieldTemplate": ".implementationGuide({{value}})", + "kind": "object", + "required": true, + "nested": { + "fieldSeparator": "", + "fields": [ + { + "jsonKey": "resourceType", + "fieldTemplate": ".resourceType({{value}})", + "kind": "enum", + "required": true, + "enumValues": [ + "ImplementationGuide" + ] + }, + { + "jsonKey": "url", + "fieldTemplate": ".url({{value}})", + "kind": "string", + "required": true + }, + { + "jsonKey": "version", + "fieldTemplate": ".version({{value}})", + "kind": "string", + "required": true + }, + { + "jsonKey": "id", + "fieldTemplate": ".id({{value}})", + "kind": "string", + "required": false + }, + { + "jsonKey": "name", + "fieldTemplate": ".name({{value}})", + "kind": "string", + "required": false + }, + { + "jsonKey": "status", + "fieldTemplate": ".status({{value}})", + "kind": "string", + "required": false + }, + { + "jsonKey": "packageId", + "fieldTemplate": ".packageId({{value}})", + "kind": "string", + "required": false + }, + { + "jsonKey": "fhirVersion", + "fieldTemplate": ".fhirVersion({{value}})", + "kind": "list", + "required": false, + "items": { + "jsonKey": "", + "fieldTemplate": "{{value}}", + "kind": "string", + "required": true + } + } + ], + "wrap": "FHIRImplementationGuide.builder(){{__body__}}.build()" + } + }, + { + "jsonKey": "profile_refs", + "fieldTemplate": ".profileRefs({{value}})", + "kind": "list", + "required": true, + "items": { + "jsonKey": "", + "fieldTemplate": "{{value}}", + "kind": "string", + "required": true + } + }, + { + "jsonKey": "profile_context", + "fieldTemplate": ".profileContext({{value}})", + "kind": "string", + "required": false + } + ] + } + } + }, + "GET /fhir/implementation-guides/{name}/versions/{version}": { + "httpMethod": "GET", + "httpPath": "/fhir/implementation-guides/{name}/versions/{version}", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.implementationGuides().implementationguides().getVersion({{name}}, {{version}})", + "params": [ + { + "name": "name", + "kind": "string" + }, + { + "name": "version", + "kind": "string" + } + ] + } + }, "GET /fhir/profiles": { "httpMethod": "GET", "httpPath": "/fhir/profiles", @@ -4704,6 +5219,102 @@ ] } }, + "GET /fhir/profiles/{id}/versions": { + "httpMethod": "GET", + "httpPath": "/fhir/profiles/{id}/versions", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.profiles().versions().list({{id}})", + "params": [ + { + "name": "id", + "kind": "string" + } + ] + } + }, + "POST /fhir/profiles/{id}/versions": { + "httpMethod": "POST", + "httpPath": "/fhir/profiles/{id}/versions", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.profiles().versions().create({{id}}, {{__body__}})", + "params": [ + { + "name": "id", + "kind": "string" + } + ], + "body": { + "fieldSeparator": "", + "fields": [ + { + "jsonKey": "", + "fieldTemplate": "{{value}}", + "kind": "object", + "required": true, + "passthroughBody": true + } + ] + } + } + }, + "GET /fhir/profiles/{id}/versions/{version}": { + "httpMethod": "GET", + "httpPath": "/fhir/profiles/{id}/versions/{version}", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.profiles().versions().get({{id}}, {{version}})", + "params": [ + { + "name": "id", + "kind": "string" + }, + { + "name": "version", + "kind": "string" + } + ] + } + }, + "DELETE /fhir/profiles/{id}/versions/{version}": { + "httpMethod": "DELETE", + "httpPath": "/fhir/profiles/{id}/versions/{version}", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.profiles().versions().delete({{id}}, {{version}})", + "params": [ + { + "name": "id", + "kind": "string" + }, + { + "name": "version", + "kind": "string" + } + ] + } + }, "POST /tools/lang2fhir-and-create": { "httpMethod": "POST", "httpPath": "/tools/lang2fhir-and-create", diff --git a/reference.md b/reference.md index 3c244443..1459d3d4 100644 --- a/reference.md +++ b/reference.md @@ -3201,52 +3201,87 @@ Multiple FHIR provider integrations can be provided as comma-separated values.
-Maps a FHIR R4 resource or Bundle into OMOP Common Data Model v5.4 rows -(person, visit_occurrence, condition_occurrence, drug_exposure, -procedure_occurrence, measurement, observation). - -Resource support is intentionally limited to the OMOP tables returned by -this endpoint: -- `Patient` -> `person` +Maps a FHIR R4 resource or Bundle into OMOP Common Data Model v5.4 rows, +grouped by destination table in `tables`. + +Current resource coverage: +- `Patient` -> `person`; `deceased[x]` can also produce `death`, and the + first address can produce `location` +- `observation_period` -> one derived row per person with dated visit, + clinical, or death rows, spanning those dates +- `Location` -> `location` and `care_site` +- `Organization` -> `care_site`; its first address can produce `location` +- `HealthcareService` -> `care_site` +- `Practitioner` and `PractitionerRole` -> `provider` - `Encounter` -> `visit_occurrence` - `Condition` -> `condition_occurrence` - `Procedure` -> `procedure_occurrence` - `MedicationRequest`, `MedicationStatement`, and `MedicationAdministration` -> `drug_exposure` - `Immunization` -> `drug_exposure` -- `Observation` with a numeric `valueQuantity`, `valueInteger`, or - numeric-looking `valueString` (for example `"<2"`) -> `measurement` -- non-numeric `Observation` -> `observation` +- `Observation` -> `measurement` or `observation`. For coded + Observations, the resolved OMOP concept domain selects the table; value + form only breaks ties. For text-only Observations, numeric values route + to `measurement` and nonnumeric values to `observation`. - `AllergyIntolerance` -> `observation` -`Medication` is supported only as reference data for medication -resources; it is not emitted as its own row because OMOP CDM has no -Medication table. Other reference/admin resources such as `Practitioner`, -`Organization`, `Location`, `Coverage`, and `Claim`, and clinical -workflow/document resources such as `DiagnosticReport`, `ServiceRequest`, -`CarePlan`, `DocumentReference`, `Composition`, `Specimen`, and -`DeviceUseStatement`, are currently accepted in a Bundle but are not -shaped into OMOP rows. Unsupported resource types are ignored rather than -listed under `dropped`; `dropped` is reserved for supported resource types -that were missing the subject/patient, code, or medication reference data -needed to produce a valid row. - -Each resource's primary clinical coding is resolved to a standard OMOP -`concept_id`. Alongside the OMOP rows grouped by table (`tables`), the -response carries `mappings` (how each source coding resolved, linked back -to the row it produced), `dropped` (resources that could not be shaped -into a row), `vocab_version` (the OMOP vocabulary release codes were -resolved against), and a small `summary` of the resolution outcomes. +`Medication` is reference data for medication resources; it does not +create its own row because OMOP CDM has no Medication table. Administrative +linkages (provider, care site, and location) are best-effort and limited to +references supplied in the request. Their supporting concepts, including +provider specialty, country, and place of service, are not mapped. + +`DiagnosticReport`, `ServiceRequest`, `CarePlan`, `DocumentReference`, +`Composition`, `Specimen`, `DeviceUseStatement`, `Coverage`, `Claim`, and +other unsupported resource types are accepted in a Bundle but ignored: they +create no row and no `dropped` entry. `dropped` is reserved for supported +row-producing resources that could not be shaped because the subject/patient, +clinical code/text, or medication data was not usable. A single-Patient +Bundle can attribute a supported clinical resource with a missing or +unresolvable subject to that sole person; in a multi-Patient Bundle, that +resource is dropped instead. + +Coded Observation routing is selected from the resolved OMOP concept +domain. Numeric and nonnumeric `value[x]` forms establish the preferred +target only when the code is valid for both tables. A text-only +Observation has no resolver target, so numeric values route to +`measurement` and nonnumeric values to `observation`. Numeric values +populate `value_as_number` in the selected row; nonnumeric values +populate `value_as_string` for an `observation` or `value_source_value` +for a `measurement`. `valueCodeableConcept` remains source text and does +not populate `value_as_concept_id`; other unsupported `value[x]` forms +and Observation components do not populate separate converted values. A +numeric comparator (`<`, `<=`, `>`, `>=`) is represented only by a +measurement's `operator_concept_id`; units remain source text and have +`unit_concept_id` of `0`. + +A single standard OMOP `concept_id` is selected for each clinical row +after considering all of the resource's supplied codings. Alongside the +OMOP rows grouped by table (`tables`), the response carries `mappings` +(an entry for every source coding, linked back to the row it produced), +`dropped` (resources that could not be shaped into a row), +`vocab_version` (the OMOP vocabulary release codes were resolved +against), and a small `summary` of the resolution outcomes. A `concept_id` of `0` is reported, not omitted (OMOP "no matching concept" semantics): it covers both a coding with no standard match (`UNMAPPED`) and an unverified suggestion for a text-only resource -(`UNCHECKED`). Only the primary clinical coding is resolved, so -`gender`/`race`/`ethnicity`/`visit`/`value`/`unit` `concept_id`s are -always `0`; the one populated non-resolved concept is measurement +(`UNCHECKED`). Demographic, visit, categorical-value, and unit concept +fields currently remain `0`; the one populated non-resolved concept is +measurement `operator_concept_id`, set from a value comparator (`<`, `<=`, `>`, `>=`) -rather than the resolver. Each `*_source_value` carries the verbatim FHIR -coding (`system#code`), and `*_type_concept_id` is set to `32817` (EHR). +rather than terminology resolution. Clinical `*_source_value` fields +preserve the selected FHIR coding (`system#code`, or `code` when no +system is supplied), falling back to source text for text-only resources. +Known OID-form coding systems are accepted as either FHIR OID URNs (for +example, `urn:oid:2.16.840.1.113883.6.1` for LOINC) or bare OIDs, and +are normalized to their canonical system URLs before terminology +resolution. `*_source_value` and `mappings[].source_system` report that +canonical URL, so the OID and URL forms produce the same mapping. An +unknown OID is not rewritten and may be `UNMAPPED`. +Other `*_source_value` fields preserve row-specific raw source values, +such as resource identifiers, names, units, or status codes, and +`*_type_concept_id` is set to `32817` (EHR). Medication codes are resolved whether they appear inline (`medicationCodeableConcept`) or via a `medicationReference` to a contained, @@ -3254,8 +3289,18 @@ relative (`Type/id`), or bundle-entry (`urn:uuid`) `Medication` resource. Resources that cannot be shaped into a row — a medication with no usable code, resolvable reference, or display, or any clinical resource whose subject/patient reference cannot be tied to a person — are reported under -`dropped` rather than emitted as blank rows. The -bundle must contain at least one Patient resource. +`dropped` rather than emitted as blank rows. The Bundle must contain at +least one Patient resource. + +All row IDs start at `1` for each request and are not stable or global. +For clinical conversion rows whose resource supplies an `id`, `mappings` +associates each row with that source FHIR resource ID. A `person` row +retains the Patient ID or its first identifier value in +`person_source_value`, when present; other reference and derived rows do +not uniformly carry a FHIR resource ID. Input resources without those +source identifiers cannot be correlated across responses from the +returned rows alone. Consumers combining responses need to establish +their own stable keys and remap every primary and foreign key together.
@@ -3347,11 +3392,12 @@ client.fhir2Omop().create( FHIR resources (single resource or Bundle). Must contain at least one Patient resource. Supported row-producing resources are Patient, -Encounter, Condition, Procedure, MedicationRequest, +Location, Organization, HealthcareService, Practitioner, +PractitionerRole, Encounter, Condition, Procedure, MedicationRequest, MedicationStatement, MedicationAdministration, Immunization, Observation, and AllergyIntolerance. Standalone Medication resources are consumed by medication references rather than mapped to their own -table. Other resource types are accepted but ignored. +table. Unsupported resource types are accepted in a Bundle but ignored. @@ -4023,11 +4069,9 @@ client.implementationGuides().implementationGuides().update(
-Deletes the stored metadata for an implementation guide — its -profile_context and timestamps. Member profiles keep their -implementation_guide assignment, so a guide still referenced by at least -one profile continues to appear in listings, just without context or -timestamps. +Deletes the stored name-level metadata and any exact canonical package +versions beneath the guide. Legacy member profile assignments are not +changed.
@@ -4065,6 +4109,149 @@ client.implementationGuides().implementationGuides().delete("acme-cardiology"); + + + + +
client.implementationGuides.implementationGuides.createVersion(name, request) -> ImplementationGuideVersionDetail +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Publishes an exact package beneath this guide family. PR 2 temporarily +permits one exact package version per guide family; publishing another +version returns `409 Conflict` until multi-version package support lands. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```java +client.implementationGuides().implementationGuides().createVersion( + "name", + CreateCanonicalImplementationGuideRequest + .builder() + .implementationGuide( + FhirImplementationGuide + .builder() + .url("url") + .version("version") + .build() + ) + .profileRefs( + Arrays.asList("profile_refs") + ) + .build() +); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**name:** `String` + +
+
+ +
+
+ +**implementationGuide:** `FhirImplementationGuide` + +
+
+ +
+
+ +**profileRefs:** `List` — Exact canonical `url|version` references to builtin or custom profiles. A package can contain at most 250 references. + +
+
+ +
+
+ +**profileContext:** `Optional` — Natural-language profile-selection context for this package. + +
+
+
+
+ + +
+
+
+ +
client.implementationGuides.implementationGuides.getVersion(name, version) -> ImplementationGuideVersionDetail +
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```java +client.implementationGuides().implementationGuides().getVersion("name", "1.0.0"); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**name:** `String` + +
+
+ +
+
+ +**version:** `String` — The authored ImplementationGuide.version. It may contain letters, numbers, and the punctuation characters `.`, `_`, `~`, `+`, and `-`; it cannot be exactly `.` or `..`. + +
+
+
+
+ +
@@ -4129,7 +4316,7 @@ client.lang2Fhir().create(
-**resource:** `CreateRequestResource` — Type of FHIR resource to create. Use 'auto' for automatic resource type detection, or specify a supported US Core profile. Recommended to use the supported US Core Profiles for validated results but you can also use any custom profile you've uploaded (if you're a develop or launch customer) +**resource:** `CreateRequestResource` — Type of FHIR resource to create. Use 'auto' for automatic resource type detection, or specify a supported profile. The default profile set includes US Core profiles and selected base R4 resources; you can also use any custom profile you've uploaded (if you're a develop or launch customer).
@@ -4225,7 +4412,15 @@ client.lang2Fhir().createMulti(
-**patientReference:** `Optional` +**primaryPatient:** `Optional` + +
+
+ +
+
+ +**patientReference:** `Optional` — Deprecated compatibility alias for primary_patient.identifier. Cannot be combined with primary_patient.
@@ -4233,7 +4428,7 @@ client.lang2Fhir().createMulti(
-**implementationGuide:** `Optional` — Custom Implementation Guide name. When specified, profiles from this IG are included alongside US Core profiles during resource detection. US Core is always the base layer; custom IG profiles are additive. +**implementationGuide:** `Optional` — Custom Implementation Guide name. When specified, profiles from this IG are included alongside the default profiles during resource detection. Default profiles are always the base layer; custom IG profiles are additive.
@@ -4433,7 +4628,7 @@ client.lang2Fhir().uploadProfile(
-Extracts text from a document (PDF or image) and converts it into a structured FHIR resource. +Extracts text from a PDF, image, RTF, or XML/C-CDA document and converts it into a structured FHIR resource. **Patient identifier handling.** When generating a `patient` (or `patient-canvas`) resource, US Core requires `Patient.identifier` (a business identifier such as an MRN). When the source text contains an identifier, it is extracted with an appropriate URI system. When the source text does not contain a detectable identifier, a synthetic one is generated with `system: "urn:phenoml:lang2fhir-generated-id"` and a UUID `value` so the resource remains FHIR-valid and US Core conformant. Callers who need a tenant-specific namespace should rewrite the synthetic system after extraction.
@@ -4455,7 +4650,7 @@ client.lang2Fhir().document( .builder() .version("R4") .resource("questionnaire") - .content("JVBERi0xLjQKJeLjz9MK...(base64-encoded PDF or image bytes)") + .content("JVBERi0xLjQKJeLjz9MK...(base64-encoded document bytes)") .build() ); ``` @@ -4491,8 +4686,11 @@ client.lang2Fhir().document( **content:** `String` Base64 encoded file content. -Supported file types: PDF (application/pdf), PNG (image/png), JPEG (image/jpeg), TIFF (image/tiff). +Supported file types: PDF (application/pdf), PNG (image/png), JPEG (image/jpeg), TIFF (image/tiff), RTF (application/rtf), XML/C-CDA (text/xml). +RTF and XML/C-CDA uploads are available on dedicated instances only. File type is auto-detected from content magic bytes. +The decoded file must not exceed 20 MiB. RTF and XML/C-CDA documents whose extracted text exceeds 1 MiB are rejected. +Generic XML must include an XML declaration; C-CDA documents rooted at `ClinicalDocument` may omit it.
@@ -4524,7 +4722,7 @@ File type is auto-detected from content magic bytes.
-Extracts text from a document (PDF or image) and converts it into multiple FHIR resources, +Extracts text from a PDF, image, RTF, or XML/C-CDA document and converts it into multiple FHIR resources, returned as a transaction Bundle. Combines document text extraction with multi-resource detection. Automatically detects Patient, Condition, MedicationRequest, Observation, and other resource types. Resources are linked with proper references (e.g., Conditions reference the Patient). @@ -4550,7 +4748,7 @@ client.lang2Fhir().documentMulti( DocumentMultiRequest .builder() .version("R4") - .content("JVBERi0xLjQKJeLjz9MK...(base64-encoded PDF or image bytes)") + .content("JVBERi0xLjQKJeLjz9MK...(base64-encoded document bytes)") .provider("medplum") .config( DocumentConfig @@ -4602,8 +4800,11 @@ client.lang2Fhir().documentMulti( **content:** `String` Base64 encoded file content. -Supported file types: PDF (application/pdf), PNG (image/png), JPEG (image/jpeg), TIFF (image/tiff). +Supported file types: PDF (application/pdf), PNG (image/png), JPEG (image/jpeg), TIFF (image/tiff), RTF (application/rtf), XML/C-CDA (text/xml). +RTF and XML/C-CDA uploads are available on dedicated instances only. File type is auto-detected from content magic bytes. +The decoded file must not exceed 20 MiB. RTF and XML/C-CDA documents whose extracted text exceeds 1 MiB are rejected. +Generic XML must include an XML declaration; C-CDA documents rooted at `ClinicalDocument` may omit it.
@@ -4619,7 +4820,15 @@ File type is auto-detected from content magic bytes.
-**patientReference:** `Optional` +**primaryPatient:** `Optional` + +
+
+ +
+
+ +**patientReference:** `Optional` — Deprecated compatibility alias for primary_patient.identifier. Cannot be combined with primary_patient.
@@ -4627,7 +4836,7 @@ File type is auto-detected from content magic bytes.
-**implementationGuide:** `Optional` — Custom Implementation Guide name. When specified, profiles from this IG are included alongside US Core profiles during resource detection. US Core is always the base layer; custom IG profiles are additive. +**implementationGuide:** `Optional` — Custom Implementation Guide name. When specified, profiles from this IG are included alongside the default profiles during resource detection. Default profiles are always the base layer; custom IG profiles are additive.
@@ -4663,8 +4872,8 @@ File type is auto-detected from content magic bytes. -## Profiles -
client.profiles.profiles.list() -> ProfileListResponse +## Lang2FhirBatch +
client.lang2FhirBatch.list() -> JobListResponse
@@ -4676,14 +4885,10 @@ File type is auto-detected from content magic bytes.
-Returns metadata for every custom (uploaded) FHIR profile on this -instance, across all implementation guides. The full StructureDefinition -JSON is omitted from each entry; fetch a single profile by id to retrieve it. - -The `url` query parameter filters by canonical URL. The canonical URL is the -stable key other platform features use to reference a profile (FHIR's -`meta.profile`, `baseDefinition`), since StructureDefinition ids are only -unique within a package. A non-matching filter returns an empty list, not a 404. +Returns a page of the instance's batch jobs, newest first, without +per-job counts. Jobs are shared across the instance's credentials, so +this lists every batch job on the instance, not just the calling +credential's.
@@ -4698,10 +4903,11 @@ unique within a package. A non-matching filter returns an empty list, not a 404.
```java -client.profiles().profiles().list( +client.lang2FhirBatch().list( ListRequest .builder() - .url("http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0") + .cursor("cursor") + .limit(1) .build() ); ``` @@ -4718,7 +4924,15 @@ client.profiles().profiles().list(
-**url:** `Optional` — Filter by canonical URL. Accepts the FHIR pinned form `url|version` (split on the last `|`); the bare form matches the current version. +**cursor:** `Optional` — Opaque pagination cursor from a previous page's next_cursor. + +
+
+ +
+
+ +**limit:** `Optional` — Page size. Defaults to 20; values above 100 are clamped to 100.
@@ -4730,7 +4944,7 @@ client.profiles().profiles().list(
-
client.profiles.profiles.create(request) -> ProfileSummary +
client.lang2FhirBatch.create(request) -> BatchJob
@@ -4742,12 +4956,18 @@ client.profiles().profiles().list(
-Creates a custom profile from a FHIR StructureDefinition supplied as a JSON -object. Metadata such as version, resource type, and url is read from the -StructureDefinition; the lowercase StructureDefinition id becomes the -profile's lookup key. When id is omitted, a random UUID is assigned. Code -system configuration is auto-extracted from the snapshot. Optionally group -the profile under a named implementation guide. +Opens an empty batch job. Items arrive on later upload calls and the set +is sealed at finalize. + +Supplying `request_id` makes the create idempotent on that token: a +retried submit whose response was lost returns the original job rather +than opening a second one. This dedupe is scoped to the calling +credential. A `request_id` whose job was canceled or failed before it +finalized is released for a fresh replay; once a job is finalized, its +`request_id` keeps resolving to it even after cancellation. + +There is no limit on how many jobs an instance may hold at once; how many +items run in parallel is a property of the instance, not of the job count.
@@ -4762,14 +4982,10 @@ the profile under a named implementation guide.
```java -client.profiles().profiles().create( - ProfileUploadRequest +client.lang2FhirBatch().create( + CreateBatchRequest .builder() - .structureDefinition( - new HashMap() {{ - put("key", "value"); - }} - ) + .requestId("submit-2025-09-02-batch-001") .build() ); ``` @@ -4786,7 +5002,11 @@ client.profiles().profiles().create(
-**request:** `ProfileUploadRequest` +**requestId:** `Optional` + +Optional client idempotency token (at most 256 UTF-8 bytes). A +retried create with the same token returns the original job instead +of opening a second one.
@@ -4798,7 +5018,7 @@ client.profiles().profiles().create(
-
client.profiles.profiles.get(id) -> ProfileGetResponse +
client.lang2FhirBatch.uploadItem(jobId, request) -> UploadItemResponse
@@ -4810,7 +5030,43 @@ client.profiles().profiles().create(
-Returns a single custom profile by id, including its full StructureDefinition JSON. +Stores one item of a job from a multipart upload. A batch's items arrive +one per request. The item carries **either** a `document` extraction +(whose input file rides as raw bytes in the `file` part) **or** a +`create` extraction (JSON only, no file). + +The upload enforces these rules: +- Set **exactly one** of `document` or `create`. Setting both, or + neither, is a `400`. +- When `document` is set, `file` is **required** — it supplies the + document's file content (PDF, image, RTF, or XML/C-CDA). +- When `create` is set, `file` is **forbidden** — a create item carries + no file. +- `document` and `create` must each be a JSON **object**. + +Only the item's structure is checked here: the fields inside `document` +or `create` are not validated at upload. A body that is well-formed JSON +but not a valid request for its endpoint is still accepted with `202` +and fails later during processing, recorded as an item `error`. A +wrong-typed field the endpoint cannot decode fails as `invalid_input`; a +body that decodes but the pipeline rejects (for example, a missing +required field) fails as `processing_failed`. + +Supplying `request_id` makes the upload idempotent on that token. A +re-upload under the same token overwrites the same item rather than +adding a second, so a client that lost an upload's response can safely +re-send it. The response's `deduplicated` is `true` only when the +re-uploaded payload matches the one already stored; a same-token upload +with a changed payload overwrites in place and returns `false`. + +Set a `request_id` on **every** upload: re-sending under the same token +is the only way to repair a lost or incomplete upload, including the one +a finalize `409` reports. Without one, a re-send adds a new item instead +of replacing the missing one, and the job cannot be finalized. + +Uploads are rejected once the job has been finalized (`409`), once it +holds its 500-item limit (`409`), or when the item is too large (`413` — +see the raw-file limit in the API description).
@@ -4825,7 +5081,13 @@ Returns a single custom profile by id, including its full StructureDefinition JS
```java -client.profiles().profiles().get("custom-patient"); +client.lang2FhirBatch().uploadItem( + "job_id", + null, + UploadItemRequest + .builder() + .build() +); ```
@@ -4840,7 +5102,7 @@ client.profiles().profiles().get("custom-patient");
-**id:** `String` — The lowercase StructureDefinition id of the custom profile. +**jobId:** `String`
@@ -4852,7 +5114,7 @@ client.profiles().profiles().get("custom-patient");
-
client.profiles.profiles.update(id, request) -> ProfileSummary +
client.lang2FhirBatch.finalize(jobId) -> BatchJob
@@ -4864,14 +5126,12 @@ client.profiles().profiles().get("custom-patient");
-Replaces an existing custom profile with a new StructureDefinition. The -`id` path parameter is authoritative: if the StructureDefinition includes -an `id` it must match the path parameter, and if it omits one the path -parameter is used. The FHIR resource type of the profile cannot change. -Code system configuration is -re-derived from the new StructureDefinition. When `implementation_guide` is -omitted, the profile keeps its existing implementation guide. The instance -stores a single version per canonical URL, so this replaces it in place. +Seals the job's item set and starts processing. Takes no request body. +Finalize is idempotent: a retried finalize succeeds again. + +If a previous upload did not complete, finalize returns a `409`; re-send +the missing upload (with the same `request_id`), then finalize. +Finalizing a job with no items is a `400`.
@@ -4886,17 +5146,7 @@ stores a single version per canonical URL, so this replaces it in place.
```java -client.profiles().profiles().update( - "custom-patient", - ProfileUploadRequest - .builder() - .structureDefinition( - new HashMap() {{ - put("key", "value"); - }} - ) - .build() -); +client.lang2FhirBatch().finalize("job_id"); ```
@@ -4911,15 +5161,7 @@ client.profiles().profiles().update(
-**id:** `String` — The lowercase StructureDefinition id of the custom profile. - -
-
- -
-
- -**request:** `ProfileUploadRequest` +**jobId:** `String`
@@ -4931,7 +5173,7 @@ client.profiles().profiles().update(
-
client.profiles.profiles.delete(id) +
client.lang2FhirBatch.cancel(jobId) -> BatchJob
@@ -4943,7 +5185,18 @@ client.profiles().profiles().update(
-Permanently deletes a custom profile by id. +Drives a job to the terminal `canceled` state on request. Takes no +request body. + +Cancel does not delete the job: the job record and any results already +produced are preserved for the normal retention window, the same as a +`completed` or `failed` job. Items stop being processed and keep the state +they held at cancellation, so a canceled job's `counts` may show +unfinished items that never resolve. + +Cancel is idempotent: canceling an already-`canceled` job returns `200` +with the job. Canceling a job that has already `completed` or `failed` is +a `409`.
@@ -4958,7 +5211,7 @@ Permanently deletes a custom profile by id.
```java -client.profiles().profiles().delete("custom-patient"); +client.lang2FhirBatch().cancel("job_id"); ```
@@ -4973,7 +5226,874 @@ client.profiles().profiles().delete("custom-patient");
-**id:** `String` — The lowercase StructureDefinition id of the custom profile. +**jobId:** `String` + +
+
+ + + + + + +
+ +
client.lang2FhirBatch.get(jobId) -> JobDetailResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Returns a job's record, its per-status item counts, and one page of +per-item statuses. + +Items are listed in a stable order that is not upload order and is the +same across pages. Match each entry to your own records by its `id` +(your correlation label) or `item_id` (from the upload response), +never by position. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```java +client.lang2FhirBatch().get( + "job_id", + GetRequest + .builder() + .cursor("cursor") + .limit(1) + .build() +); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**jobId:** `String` + +
+
+ +
+
+ +**cursor:** `Optional` — Opaque pagination cursor from a previous page's next_cursor. + +
+
+ +
+
+ +**limit:** `Optional` — Page size for the item-status page. Defaults to 20; values above 100 are clamped to 100. + +
+
+
+
+ + +
+
+
+ +
client.lang2FhirBatch.getResults(jobId) -> ResultsPageResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +A lighter status page. Returns the same per-item status entries as +`GET /lang2fhir/batch/{job_id}`, but without the job record or counts, +and the entries carry `result_size` rather than any result content. Use +each entry's `item_id` to fetch that item's result from +`GET /lang2fhir/batch/{job_id}/results/{item_id}`. + +Entries are listed in a stable order that is not upload order and is +the same across pages. Match each entry to your own records by its `id` +(your correlation label) or `item_id` (from the upload response), +never by position. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```java +client.lang2FhirBatch().getResults( + "job_id", + GetResultsRequest + .builder() + .cursor("cursor") + .limit(1) + .build() +); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**jobId:** `String` + +
+
+ +
+
+ +**cursor:** `Optional` — Opaque pagination cursor from a previous page's next_cursor. + +
+
+ +
+
+ +**limit:** `Optional` — Page size. Defaults to 20; values above 100 are clamped to 100. + +
+
+
+
+ + +
+
+
+ +
client.lang2FhirBatch.getResult(jobId, itemId) -> Map&lt;String, Object&gt; +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Streams one item's stored result bytes verbatim as `application/json`. +The body is the response the item's synchronous multi endpoint would have +returned — a `DocumentMultiResponse` for a document item or a +`CreateMultiResponse` for a create item. + +Only a succeeded item has a result: an item that has not succeeded +(pending, processing, or failed) is a `409`, and a result that has +expired is a `404`. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```java +client.lang2FhirBatch().getResult("job_id", "item_id"); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**jobId:** `String` + +
+
+ +
+
+ +**itemId:** `String` + +
+
+
+
+ + +
+
+
+ +## Profiles +
client.profiles.profiles.list() -> ProfileListResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Returns metadata for every custom (uploaded) FHIR profile on this +instance, across all implementation guides. The full StructureDefinition +JSON is omitted from each entry; fetch a single profile by id to retrieve it. + +The `url` query parameter filters by canonical URL. The canonical URL is the +stable key other platform features use to reference a profile (FHIR's +`meta.profile`, `baseDefinition`), since StructureDefinition ids are only +unique within a package. An unpinned `url` filter returns metadata for +the profile's current StructureDefinition. Pinned `url|version` filters +resolve a retained version when present; otherwise they can fall back to +the profile's current StructureDefinition, whose content can change +through the profile update endpoint. A non-matching filter returns an +empty list, not a 404. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```java +client.profiles().profiles().list( + ListRequest + .builder() + .url("http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0") + .build() +); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**url:** `Optional` — Filter by canonical URL. Accepts the FHIR pinned form `url|version`; without a version pin, returns the profile's current StructureDefinition metadata. + +
+
+
+
+ + +
+
+
+ +
client.profiles.profiles.create(request) -> ProfileSummary +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Creates a custom profile from a FHIR StructureDefinition supplied as a JSON +object. Metadata such as version, resource type, and url is read from the +StructureDefinition; the lowercase StructureDefinition id becomes the +profile's lookup key. When id is omitted, a random UUID is assigned. +Optionally group the profile under a named implementation guide. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```java +client.profiles().profiles().create( + ProfileUploadRequest + .builder() + .structureDefinition( + new HashMap() {{ + put("resourceType", "StructureDefinition"); + put("id", "custom-patient"); + put("url", "http://phenoml.com/fhir/StructureDefinition/custom-patient"); + put("name", "CustomPatient"); + put("status", "active"); + put("fhirVersion", "4.0.1"); + put("kind", "resource"); + put("abstract", false); + put("type", "Patient"); + put("baseDefinition", "http://hl7.org/fhir/StructureDefinition/Patient"); + put("derivation", "constraint"); + put("snapshot", new + HashMap() {{put("element", new ArrayList(Arrays.asList(new + HashMap() {{put("id", "Patient"); + put("path", "Patient"); + put("min", 0); + put("max", "*"); + }}))); + }}); + }} + ) + .implementationGuide("acme-cardiology") + .build() +); +``` + + + + + +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `ProfileUploadRequest` + +
+
+
+
+ + + + + + +
client.profiles.profiles.get(id) -> ProfileGetResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Returns a single custom profile by id, including its full StructureDefinition +JSON. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```java +client.profiles().profiles().get("custom-patient"); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**id:** `String` — The lowercase StructureDefinition id of the custom profile. + +
+
+
+
+ + +
+
+
+ +
client.profiles.profiles.update(id, request) -> ProfileSummary +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Replaces an existing custom profile with a new StructureDefinition. The +`id` path parameter is authoritative: if the StructureDefinition includes +an `id` it must match the path parameter, and if it omits one the path +parameter is used. The FHIR resource type of the profile cannot change. +When `implementation_guide` is omitted, the profile keeps its existing +implementation guide. A retained version string is allowed only when +re-submitting the profile's current version with an unchanged +StructureDefinition; otherwise it returns a conflict. While the profile +has retained versions, its +canonical URL cannot be changed. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```java +client.profiles().profiles().update( + "custom-patient", + ProfileUploadRequest + .builder() + .structureDefinition( + new HashMap() {{ + put("resourceType", "StructureDefinition"); + put("id", "custom-patient"); + put("url", "http://phenoml.com/fhir/StructureDefinition/custom-patient"); + put("name", "CustomPatient"); + put("status", "active"); + put("fhirVersion", "4.0.1"); + put("kind", "resource"); + put("abstract", false); + put("type", "Patient"); + put("baseDefinition", "http://hl7.org/fhir/StructureDefinition/Patient"); + put("derivation", "constraint"); + put("snapshot", new + HashMap() {{put("element", new ArrayList(Arrays.asList(new + HashMap() {{put("id", "Patient"); + put("path", "Patient"); + put("min", 0); + put("max", "*"); + }}))); + }}); + }} + ) + .implementationGuide("acme-cardiology") + .build() +); +``` + + + + + +#### ⚙️ Parameters + +
+
+ +
+
+ +**id:** `String` — The lowercase StructureDefinition id of the custom profile. + +
+
+ +
+
+ +**request:** `ProfileUploadRequest` + +
+
+
+
+ + + + + + +
client.profiles.profiles.delete(id) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Permanently deletes a custom profile by id. This also deletes all retained +versions for that profile so the canonical URL can be reused by a later +upload. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```java +client.profiles().profiles().delete("custom-patient"); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**id:** `String` — The lowercase StructureDefinition id of the custom profile. + +
+
+
+
+ + +
+
+
+ +## Profiles Versions +
client.profiles.versions.list(id) -> ProfileVersionListResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Returns retained versions for a custom profile. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```java +client.profiles().versions().list("custom-patient"); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**id:** `String` — The lowercase StructureDefinition id of the custom profile. + +
+
+
+
+ + +
+
+
+ +
client.profiles.versions.create(id, request) -> ProfileSummary +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Adds an immutable StructureDefinition version to a custom profile. If +the profile does not exist, it is created from the submitted version. +The StructureDefinition must include a non-empty `version`; its +canonical URL and resource type must match the profile when one already +exists. If it includes an `id`, that id must match the path parameter; +if it omits `id`, the path parameter is used. Profiles created through +this endpoint are grouped under `custom`. Posting the profile's current +StructureDefinition unchanged retains it as a version. +Version strings may contain letters, numbers, and the punctuation +characters `.`, `_`, `~`, `+`, and `-`; they cannot be exactly `.` or +`..`. Each profile can retain up to 250 versions; delete old +versions before adding more. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```java +client.profiles().versions().create( + "custom-patient", + new HashMap() {{ + put("key", "value"); + }} +); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**id:** `String` — The lowercase StructureDefinition id of the custom profile. + +
+
+ +
+
+ +**request:** `Map` + +
+
+
+
+ + +
+
+
+ +
client.profiles.versions.get(id, version) -> ProfileGetResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Returns metadata and the full StructureDefinition for one retained +version. The returned StructureDefinition's id is the profile id. The +path version is the authored `StructureDefinition.version` value. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```java +client.profiles().versions().get("custom-patient", "2.0.0"); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**id:** `String` — The lowercase StructureDefinition id of the custom profile. + +
+
+ +
+
+ +**version:** `String` — The authored StructureDefinition.version. It may contain letters, numbers, and the punctuation characters `.`, `_`, `~`, `+`, and `-`; it cannot be exactly `.` or `..`. + +
+
+
+
+ + +
+
+
+ +
client.profiles.versions.delete(id, version) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Deletes one retained version from a custom profile. The path +version is the authored `StructureDefinition.version` value. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```java +client.profiles().versions().delete("custom-patient", "2.0.0"); +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**id:** `String` — The lowercase StructureDefinition id of the custom profile. + +
+
+ +
+
+ +**version:** `String` — The authored StructureDefinition.version. It may contain letters, numbers, and the punctuation characters `.`, `_`, `~`, `+`, and `-`; it cannot be exactly `.` or `..`.
diff --git a/src/main/java/com/phenoml/api/AsyncPhenomlClient.java b/src/main/java/com/phenoml/api/AsyncPhenomlClient.java index 68d40177..513815c4 100644 --- a/src/main/java/com/phenoml/api/AsyncPhenomlClient.java +++ b/src/main/java/com/phenoml/api/AsyncPhenomlClient.java @@ -14,6 +14,7 @@ import com.phenoml.api.resources.fhirprovider.AsyncFhirProviderClient; import com.phenoml.api.resources.implementationguides.AsyncImplementationGuidesClient; import com.phenoml.api.resources.lang2fhir.AsyncLang2FhirClient; +import com.phenoml.api.resources.lang2fhirbatch.AsyncLang2FhirBatchClient; import com.phenoml.api.resources.profiles.AsyncProfilesClient; import com.phenoml.api.resources.summary.AsyncSummaryClient; import com.phenoml.api.resources.tools.AsyncToolsClient; @@ -42,6 +43,8 @@ public class AsyncPhenomlClient { protected final Supplier lang2FhirClient; + protected final Supplier lang2FhirBatchClient; + protected final Supplier profilesClient; protected final Supplier summaryClient; @@ -63,6 +66,7 @@ public AsyncPhenomlClient(ClientOptions clientOptions) { this.fhirProviderClient = Suppliers.memoize(() -> new AsyncFhirProviderClient(clientOptions)); this.implementationGuidesClient = Suppliers.memoize(() -> new AsyncImplementationGuidesClient(clientOptions)); this.lang2FhirClient = Suppliers.memoize(() -> new AsyncLang2FhirClient(clientOptions)); + this.lang2FhirBatchClient = Suppliers.memoize(() -> new AsyncLang2FhirBatchClient(clientOptions)); this.profilesClient = Suppliers.memoize(() -> new AsyncProfilesClient(clientOptions)); this.summaryClient = Suppliers.memoize(() -> new AsyncSummaryClient(clientOptions)); this.toolsClient = Suppliers.memoize(() -> new AsyncToolsClient(clientOptions)); @@ -106,6 +110,10 @@ public AsyncLang2FhirClient lang2Fhir() { return this.lang2FhirClient.get(); } + public AsyncLang2FhirBatchClient lang2FhirBatch() { + return this.lang2FhirBatchClient.get(); + } + public AsyncProfilesClient profiles() { return this.profilesClient.get(); } diff --git a/src/main/java/com/phenoml/api/AsyncPhenomlClientBuilder.java b/src/main/java/com/phenoml/api/AsyncPhenomlClientBuilder.java index 6ef67046..d6d4fc9c 100644 --- a/src/main/java/com/phenoml/api/AsyncPhenomlClientBuilder.java +++ b/src/main/java/com/phenoml/api/AsyncPhenomlClientBuilder.java @@ -176,7 +176,15 @@ protected ClientOptions buildClientOptions() { protected void setEnvironment(ClientOptions.Builder builder) { if (this.instanceUrl != null) { String _instanceUrl = this.instanceUrl != null ? this.instanceUrl : "experiment.app.pheno.ml"; - this.environment = Environment.custom("https://{instanceUrl}".replace("{instanceUrl}", _instanceUrl)); + String _urlTemplate = null; + if (this.environment == null) { + _urlTemplate = "https://{instanceUrl}"; + } else if (this.environment.equals(Environment.DEFAULT)) { + _urlTemplate = "https://{instanceUrl}"; + } + if (_urlTemplate != null) { + this.environment = Environment.custom(_urlTemplate.replace("{instanceUrl}", _instanceUrl)); + } } builder.environment(this.environment); } @@ -314,30 +322,18 @@ public static final class _CredentialsAuth extends AsyncPhenomlClientBuilder { private final String clientSecret; - private Optional grantType = Optional.empty(); - _CredentialsAuth(String clientId, String clientSecret) { this.clientId = clientId; this.clientSecret = clientSecret; } - public _CredentialsAuth grantType(Optional grantType) { - this.grantType = grantType; - return this; - } - - public _CredentialsAuth grantType(String grantType) { - this.grantType = Optional.ofNullable(grantType); - return this; - } - @Override public AsyncPhenomlClient build() { validateConfiguration(); ClientOptions baseOptions = buildClientOptions(); AuthtokenClient authClient = new AuthtokenClient(baseOptions); OAuthTokenSupplier oAuthTokenSupplier = - new OAuthTokenSupplier(this.clientId, this.clientSecret, this.grantType, authClient); + new OAuthTokenSupplier(this.clientId, this.clientSecret, authClient); ClientOptions finalOptions = ClientOptions.Builder.from(baseOptions) .addHeader("Authorization", oAuthTokenSupplier) .build(); diff --git a/src/main/java/com/phenoml/api/PhenomlClient.java b/src/main/java/com/phenoml/api/PhenomlClient.java index 8a12aeb9..c96e61bc 100644 --- a/src/main/java/com/phenoml/api/PhenomlClient.java +++ b/src/main/java/com/phenoml/api/PhenomlClient.java @@ -14,6 +14,7 @@ import com.phenoml.api.resources.fhirprovider.FhirProviderClient; import com.phenoml.api.resources.implementationguides.ImplementationGuidesClient; import com.phenoml.api.resources.lang2fhir.Lang2FhirClient; +import com.phenoml.api.resources.lang2fhirbatch.Lang2FhirBatchClient; import com.phenoml.api.resources.profiles.ProfilesClient; import com.phenoml.api.resources.summary.SummaryClient; import com.phenoml.api.resources.tools.ToolsClient; @@ -42,6 +43,8 @@ public class PhenomlClient { protected final Supplier lang2FhirClient; + protected final Supplier lang2FhirBatchClient; + protected final Supplier profilesClient; protected final Supplier summaryClient; @@ -63,6 +66,7 @@ public PhenomlClient(ClientOptions clientOptions) { this.fhirProviderClient = Suppliers.memoize(() -> new FhirProviderClient(clientOptions)); this.implementationGuidesClient = Suppliers.memoize(() -> new ImplementationGuidesClient(clientOptions)); this.lang2FhirClient = Suppliers.memoize(() -> new Lang2FhirClient(clientOptions)); + this.lang2FhirBatchClient = Suppliers.memoize(() -> new Lang2FhirBatchClient(clientOptions)); this.profilesClient = Suppliers.memoize(() -> new ProfilesClient(clientOptions)); this.summaryClient = Suppliers.memoize(() -> new SummaryClient(clientOptions)); this.toolsClient = Suppliers.memoize(() -> new ToolsClient(clientOptions)); @@ -106,6 +110,10 @@ public Lang2FhirClient lang2Fhir() { return this.lang2FhirClient.get(); } + public Lang2FhirBatchClient lang2FhirBatch() { + return this.lang2FhirBatchClient.get(); + } + public ProfilesClient profiles() { return this.profilesClient.get(); } diff --git a/src/main/java/com/phenoml/api/PhenomlClientBuilder.java b/src/main/java/com/phenoml/api/PhenomlClientBuilder.java index 00d898be..42b2db88 100644 --- a/src/main/java/com/phenoml/api/PhenomlClientBuilder.java +++ b/src/main/java/com/phenoml/api/PhenomlClientBuilder.java @@ -176,7 +176,15 @@ protected ClientOptions buildClientOptions() { protected void setEnvironment(ClientOptions.Builder builder) { if (this.instanceUrl != null) { String _instanceUrl = this.instanceUrl != null ? this.instanceUrl : "experiment.app.pheno.ml"; - this.environment = Environment.custom("https://{instanceUrl}".replace("{instanceUrl}", _instanceUrl)); + String _urlTemplate = null; + if (this.environment == null) { + _urlTemplate = "https://{instanceUrl}"; + } else if (this.environment.equals(Environment.DEFAULT)) { + _urlTemplate = "https://{instanceUrl}"; + } + if (_urlTemplate != null) { + this.environment = Environment.custom(_urlTemplate.replace("{instanceUrl}", _instanceUrl)); + } } builder.environment(this.environment); } @@ -314,30 +322,18 @@ public static final class _CredentialsAuth extends PhenomlClientBuilder { private final String clientSecret; - private Optional grantType = Optional.empty(); - _CredentialsAuth(String clientId, String clientSecret) { this.clientId = clientId; this.clientSecret = clientSecret; } - public _CredentialsAuth grantType(Optional grantType) { - this.grantType = grantType; - return this; - } - - public _CredentialsAuth grantType(String grantType) { - this.grantType = Optional.ofNullable(grantType); - return this; - } - @Override public PhenomlClient build() { validateConfiguration(); ClientOptions baseOptions = buildClientOptions(); AuthtokenClient authClient = new AuthtokenClient(baseOptions); OAuthTokenSupplier oAuthTokenSupplier = - new OAuthTokenSupplier(this.clientId, this.clientSecret, this.grantType, authClient); + new OAuthTokenSupplier(this.clientId, this.clientSecret, authClient); ClientOptions finalOptions = ClientOptions.Builder.from(baseOptions) .addHeader("Authorization", oAuthTokenSupplier) .build(); diff --git a/src/main/java/com/phenoml/api/core/ClientOptions.java b/src/main/java/com/phenoml/api/core/ClientOptions.java index c7a9705f..b12427d2 100644 --- a/src/main/java/com/phenoml/api/core/ClientOptions.java +++ b/src/main/java/com/phenoml/api/core/ClientOptions.java @@ -47,10 +47,10 @@ private ClientOptions( this.headers.putAll(headers); this.headers.putAll(new HashMap() { { - put("User-Agent", "com.phenoml.maven.phenoml-java-sdk/17.13.0"); + put("User-Agent", "com.phenoml.maven.phenoml-java-sdk/17.13.1"); put("X-Fern-Language", "JAVA"); - put("X-Fern-SDK-Name", "com.phenoml.fern:api-sdk"); - put("X-Fern-SDK-Version", "17.13.0"); + put("X-Fern-SDK-Name", "com.phenoml.maven:phenoml-java-sdk"); + put("X-Fern-SDK-Version", "17.13.1"); } }); this.headerSuppliers = headerSuppliers; @@ -153,7 +153,9 @@ public Builder environment(Environment environment) { } public Builder addHeader(String key, String value) { - this.headers.put(key, value); + if (value != null) { + this.headers.put(key, value); + } return this; } diff --git a/src/main/java/com/phenoml/api/core/ConsoleLogger.java b/src/main/java/com/phenoml/api/core/ConsoleLogger.java index 92f79a63..d502dc13 100644 --- a/src/main/java/com/phenoml/api/core/ConsoleLogger.java +++ b/src/main/java/com/phenoml/api/core/ConsoleLogger.java @@ -23,6 +23,7 @@ public String format(java.util.logging.LogRecord record) { return record.getLevel() + " - " + record.getMessage() + System.lineSeparator(); } }); + handler.setLevel(Level.ALL); logger.addHandler(handler); logger.setUseParentHandlers(false); logger.setLevel(Level.ALL); diff --git a/src/main/java/com/phenoml/api/core/OAuthTokenSupplier.java b/src/main/java/com/phenoml/api/core/OAuthTokenSupplier.java index a293397a..cc79cc17 100644 --- a/src/main/java/com/phenoml/api/core/OAuthTokenSupplier.java +++ b/src/main/java/com/phenoml/api/core/OAuthTokenSupplier.java @@ -8,7 +8,6 @@ import com.phenoml.api.resources.authtoken.types.TokenResponse; import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.Optional; import java.util.function.Supplier; public final class OAuthTokenSupplier implements Supplier { @@ -18,8 +17,6 @@ public final class OAuthTokenSupplier implements Supplier { private final String clientSecret; - private final Optional grantType; - private final AuthtokenClient authClient; private final Object tokenLock = new Object(); @@ -28,11 +25,9 @@ public final class OAuthTokenSupplier implements Supplier { private volatile Instant expiresAt; - public OAuthTokenSupplier( - String clientId, String clientSecret, Optional grantType, AuthtokenClient authClient) { + public OAuthTokenSupplier(String clientId, String clientSecret, AuthtokenClient authClient) { this.clientId = clientId; this.clientSecret = clientSecret; - this.grantType = grantType; this.authClient = authClient; this.expiresAt = Instant.now(); } @@ -41,7 +36,7 @@ public TokenResponse fetchToken() { ClientCredentialsRequest getTokenRequest = ClientCredentialsRequest.builder() .clientId(clientId) .clientSecret(clientSecret) - .grantType(grantType) + .grantType("client_credentials") .build(); return authClient.getToken(getTokenRequest); } diff --git a/src/main/java/com/phenoml/api/resources/fhir2omop/AsyncFhir2OmopClient.java b/src/main/java/com/phenoml/api/resources/fhir2omop/AsyncFhir2OmopClient.java index 509fc772..e71d4fd7 100644 --- a/src/main/java/com/phenoml/api/resources/fhir2omop/AsyncFhir2OmopClient.java +++ b/src/main/java/com/phenoml/api/resources/fhir2omop/AsyncFhir2OmopClient.java @@ -27,116 +27,200 @@ public AsyncRawFhir2OmopClient withRawResponse() { } /** - * Maps a FHIR R4 resource or Bundle into OMOP Common Data Model v5.4 rows - * (person, visit_occurrence, condition_occurrence, drug_exposure, - * procedure_occurrence, measurement, observation). - *

Resource support is intentionally limited to the OMOP tables returned by - * this endpoint:

+ * Maps a FHIR R4 resource or Bundle into OMOP Common Data Model v5.4 rows, + * grouped by destination table in tables. + *

Current resource coverage:

*
    - *
  • Patient -> person
  • + *
  • Patient -> person; deceased[x] can also produce death, and the + * first address can produce location
  • + *
  • observation_period -> one derived row per person with dated visit, + * clinical, or death rows, spanning those dates
  • + *
  • Location -> location and care_site
  • + *
  • Organization -> care_site; its first address can produce location
  • + *
  • HealthcareService -> care_site
  • + *
  • Practitioner and PractitionerRole -> provider
  • *
  • Encounter -> visit_occurrence
  • *
  • Condition -> condition_occurrence
  • *
  • Procedure -> procedure_occurrence
  • *
  • MedicationRequest, MedicationStatement, and * MedicationAdministration -> drug_exposure
  • *
  • Immunization -> drug_exposure
  • - *
  • Observation with a numeric valueQuantity, valueInteger, or - * numeric-looking valueString (for example "<2") -> measurement
  • - *
  • non-numeric Observation -> observation
  • + *
  • Observation -> measurement or observation. For coded + * Observations, the resolved OMOP concept domain selects the table; value + * form only breaks ties. For text-only Observations, numeric values route + * to measurement and nonnumeric values to observation.
  • *
  • AllergyIntolerance -> observation
  • *
- *

Medication is supported only as reference data for medication - * resources; it is not emitted as its own row because OMOP CDM has no - * Medication table. Other reference/admin resources such as Practitioner, - * Organization, Location, Coverage, and Claim, and clinical - * workflow/document resources such as DiagnosticReport, ServiceRequest, - * CarePlan, DocumentReference, Composition, Specimen, and - * DeviceUseStatement, are currently accepted in a Bundle but are not - * shaped into OMOP rows. Unsupported resource types are ignored rather than - * listed under dropped; dropped is reserved for supported resource types - * that were missing the subject/patient, code, or medication reference data - * needed to produce a valid row.

- *

Each resource's primary clinical coding is resolved to a standard OMOP - * concept_id. Alongside the OMOP rows grouped by table (tables), the - * response carries mappings (how each source coding resolved, linked back - * to the row it produced), dropped (resources that could not be shaped - * into a row), vocab_version (the OMOP vocabulary release codes were - * resolved against), and a small summary of the resolution outcomes.

+ *

Medication is reference data for medication resources; it does not + * create its own row because OMOP CDM has no Medication table. Administrative + * linkages (provider, care site, and location) are best-effort and limited to + * references supplied in the request. Their supporting concepts, including + * provider specialty, country, and place of service, are not mapped.

+ *

DiagnosticReport, ServiceRequest, CarePlan, DocumentReference, + * Composition, Specimen, DeviceUseStatement, Coverage, Claim, and + * other unsupported resource types are accepted in a Bundle but ignored: they + * create no row and no dropped entry. dropped is reserved for supported + * row-producing resources that could not be shaped because the subject/patient, + * clinical code/text, or medication data was not usable. A single-Patient + * Bundle can attribute a supported clinical resource with a missing or + * unresolvable subject to that sole person; in a multi-Patient Bundle, that + * resource is dropped instead.

+ *

Coded Observation routing is selected from the resolved OMOP concept + * domain. Numeric and nonnumeric value[x] forms establish the preferred + * target only when the code is valid for both tables. A text-only + * Observation has no resolver target, so numeric values route to + * measurement and nonnumeric values to observation. Numeric values + * populate value_as_number in the selected row; nonnumeric values + * populate value_as_string for an observation or value_source_value + * for a measurement. valueCodeableConcept remains source text and does + * not populate value_as_concept_id; other unsupported value[x] forms + * and Observation components do not populate separate converted values. A + * numeric comparator (<, <=, >, >=) is represented only by a + * measurement's operator_concept_id; units remain source text and have + * unit_concept_id of 0.

+ *

A single standard OMOP concept_id is selected for each clinical row + * after considering all of the resource's supplied codings. Alongside the + * OMOP rows grouped by table (tables), the response carries mappings + * (an entry for every source coding, linked back to the row it produced), + * dropped (resources that could not be shaped into a row), + * vocab_version (the OMOP vocabulary release codes were resolved + * against), and a small summary of the resolution outcomes.

*

A concept_id of 0 is reported, not omitted (OMOP "no matching * concept" semantics): it covers both a coding with no standard match * (UNMAPPED) and an unverified suggestion for a text-only resource - * (UNCHECKED). Only the primary clinical coding is resolved, so - * gender/race/ethnicity/visit/value/unit concept_ids are - * always 0; the one populated non-resolved concept is measurement + * (UNCHECKED). Demographic, visit, categorical-value, and unit concept + * fields currently remain 0; the one populated non-resolved concept is + * measurement * operator_concept_id, set from a value comparator (<, <=, >, >=) - * rather than the resolver. Each *_source_value carries the verbatim FHIR - * coding (system#code), and *_type_concept_id is set to 32817 (EHR).

+ * rather than terminology resolution. Clinical *_source_value fields + * preserve the selected FHIR coding (system#code, or code when no + * system is supplied), falling back to source text for text-only resources. + * Known OID-form coding systems are accepted as either FHIR OID URNs (for + * example, urn:oid:2.16.840.1.113883.6.1 for LOINC) or bare OIDs, and + * are normalized to their canonical system URLs before terminology + * resolution. *_source_value and mappings[].source_system report that + * canonical URL, so the OID and URL forms produce the same mapping. An + * unknown OID is not rewritten and may be UNMAPPED. + * Other *_source_value fields preserve row-specific raw source values, + * such as resource identifiers, names, units, or status codes, and + * *_type_concept_id is set to 32817 (EHR).

*

Medication codes are resolved whether they appear inline * (medicationCodeableConcept) or via a medicationReference to a contained, * relative (Type/id), or bundle-entry (urn:uuid) Medication resource. * Resources that cannot be shaped into a row — a medication with no usable * code, resolvable reference, or display, or any clinical resource whose * subject/patient reference cannot be tied to a person — are reported under - * dropped rather than emitted as blank rows. The - * bundle must contain at least one Patient resource.

+ * dropped rather than emitted as blank rows. The Bundle must contain at + * least one Patient resource.

+ *

All row IDs start at 1 for each request and are not stable or global. + * For clinical conversion rows whose resource supplies an id, mappings + * associates each row with that source FHIR resource ID. A person row + * retains the Patient ID or its first identifier value in + * person_source_value, when present; other reference and derived rows do + * not uniformly carry a FHIR resource ID. Input resources without those + * source identifiers cannot be correlated across responses from the + * returned rows alone. Consumers combining responses need to establish + * their own stable keys and remap every primary and foreign key together.

*/ public CompletableFuture create(CreateOmopRequest request) { return this.rawClient.create(request).thenApply(response -> response.body()); } /** - * Maps a FHIR R4 resource or Bundle into OMOP Common Data Model v5.4 rows - * (person, visit_occurrence, condition_occurrence, drug_exposure, - * procedure_occurrence, measurement, observation). - *

Resource support is intentionally limited to the OMOP tables returned by - * this endpoint:

+ * Maps a FHIR R4 resource or Bundle into OMOP Common Data Model v5.4 rows, + * grouped by destination table in tables. + *

Current resource coverage:

*
    - *
  • Patient -> person
  • + *
  • Patient -> person; deceased[x] can also produce death, and the + * first address can produce location
  • + *
  • observation_period -> one derived row per person with dated visit, + * clinical, or death rows, spanning those dates
  • + *
  • Location -> location and care_site
  • + *
  • Organization -> care_site; its first address can produce location
  • + *
  • HealthcareService -> care_site
  • + *
  • Practitioner and PractitionerRole -> provider
  • *
  • Encounter -> visit_occurrence
  • *
  • Condition -> condition_occurrence
  • *
  • Procedure -> procedure_occurrence
  • *
  • MedicationRequest, MedicationStatement, and * MedicationAdministration -> drug_exposure
  • *
  • Immunization -> drug_exposure
  • - *
  • Observation with a numeric valueQuantity, valueInteger, or - * numeric-looking valueString (for example "<2") -> measurement
  • - *
  • non-numeric Observation -> observation
  • + *
  • Observation -> measurement or observation. For coded + * Observations, the resolved OMOP concept domain selects the table; value + * form only breaks ties. For text-only Observations, numeric values route + * to measurement and nonnumeric values to observation.
  • *
  • AllergyIntolerance -> observation
  • *
- *

Medication is supported only as reference data for medication - * resources; it is not emitted as its own row because OMOP CDM has no - * Medication table. Other reference/admin resources such as Practitioner, - * Organization, Location, Coverage, and Claim, and clinical - * workflow/document resources such as DiagnosticReport, ServiceRequest, - * CarePlan, DocumentReference, Composition, Specimen, and - * DeviceUseStatement, are currently accepted in a Bundle but are not - * shaped into OMOP rows. Unsupported resource types are ignored rather than - * listed under dropped; dropped is reserved for supported resource types - * that were missing the subject/patient, code, or medication reference data - * needed to produce a valid row.

- *

Each resource's primary clinical coding is resolved to a standard OMOP - * concept_id. Alongside the OMOP rows grouped by table (tables), the - * response carries mappings (how each source coding resolved, linked back - * to the row it produced), dropped (resources that could not be shaped - * into a row), vocab_version (the OMOP vocabulary release codes were - * resolved against), and a small summary of the resolution outcomes.

+ *

Medication is reference data for medication resources; it does not + * create its own row because OMOP CDM has no Medication table. Administrative + * linkages (provider, care site, and location) are best-effort and limited to + * references supplied in the request. Their supporting concepts, including + * provider specialty, country, and place of service, are not mapped.

+ *

DiagnosticReport, ServiceRequest, CarePlan, DocumentReference, + * Composition, Specimen, DeviceUseStatement, Coverage, Claim, and + * other unsupported resource types are accepted in a Bundle but ignored: they + * create no row and no dropped entry. dropped is reserved for supported + * row-producing resources that could not be shaped because the subject/patient, + * clinical code/text, or medication data was not usable. A single-Patient + * Bundle can attribute a supported clinical resource with a missing or + * unresolvable subject to that sole person; in a multi-Patient Bundle, that + * resource is dropped instead.

+ *

Coded Observation routing is selected from the resolved OMOP concept + * domain. Numeric and nonnumeric value[x] forms establish the preferred + * target only when the code is valid for both tables. A text-only + * Observation has no resolver target, so numeric values route to + * measurement and nonnumeric values to observation. Numeric values + * populate value_as_number in the selected row; nonnumeric values + * populate value_as_string for an observation or value_source_value + * for a measurement. valueCodeableConcept remains source text and does + * not populate value_as_concept_id; other unsupported value[x] forms + * and Observation components do not populate separate converted values. A + * numeric comparator (<, <=, >, >=) is represented only by a + * measurement's operator_concept_id; units remain source text and have + * unit_concept_id of 0.

+ *

A single standard OMOP concept_id is selected for each clinical row + * after considering all of the resource's supplied codings. Alongside the + * OMOP rows grouped by table (tables), the response carries mappings + * (an entry for every source coding, linked back to the row it produced), + * dropped (resources that could not be shaped into a row), + * vocab_version (the OMOP vocabulary release codes were resolved + * against), and a small summary of the resolution outcomes.

*

A concept_id of 0 is reported, not omitted (OMOP "no matching * concept" semantics): it covers both a coding with no standard match * (UNMAPPED) and an unverified suggestion for a text-only resource - * (UNCHECKED). Only the primary clinical coding is resolved, so - * gender/race/ethnicity/visit/value/unit concept_ids are - * always 0; the one populated non-resolved concept is measurement + * (UNCHECKED). Demographic, visit, categorical-value, and unit concept + * fields currently remain 0; the one populated non-resolved concept is + * measurement * operator_concept_id, set from a value comparator (<, <=, >, >=) - * rather than the resolver. Each *_source_value carries the verbatim FHIR - * coding (system#code), and *_type_concept_id is set to 32817 (EHR).

+ * rather than terminology resolution. Clinical *_source_value fields + * preserve the selected FHIR coding (system#code, or code when no + * system is supplied), falling back to source text for text-only resources. + * Known OID-form coding systems are accepted as either FHIR OID URNs (for + * example, urn:oid:2.16.840.1.113883.6.1 for LOINC) or bare OIDs, and + * are normalized to their canonical system URLs before terminology + * resolution. *_source_value and mappings[].source_system report that + * canonical URL, so the OID and URL forms produce the same mapping. An + * unknown OID is not rewritten and may be UNMAPPED. + * Other *_source_value fields preserve row-specific raw source values, + * such as resource identifiers, names, units, or status codes, and + * *_type_concept_id is set to 32817 (EHR).

*

Medication codes are resolved whether they appear inline * (medicationCodeableConcept) or via a medicationReference to a contained, * relative (Type/id), or bundle-entry (urn:uuid) Medication resource. * Resources that cannot be shaped into a row — a medication with no usable * code, resolvable reference, or display, or any clinical resource whose * subject/patient reference cannot be tied to a person — are reported under - * dropped rather than emitted as blank rows. The - * bundle must contain at least one Patient resource.

+ * dropped rather than emitted as blank rows. The Bundle must contain at + * least one Patient resource.

+ *

All row IDs start at 1 for each request and are not stable or global. + * For clinical conversion rows whose resource supplies an id, mappings + * associates each row with that source FHIR resource ID. A person row + * retains the Patient ID or its first identifier value in + * person_source_value, when present; other reference and derived rows do + * not uniformly carry a FHIR resource ID. Input resources without those + * source identifiers cannot be correlated across responses from the + * returned rows alone. Consumers combining responses need to establish + * their own stable keys and remap every primary and foreign key together.

*/ public CompletableFuture create(CreateOmopRequest request, RequestOptions requestOptions) { return this.rawClient.create(request, requestOptions).thenApply(response -> response.body()); diff --git a/src/main/java/com/phenoml/api/resources/fhir2omop/AsyncRawFhir2OmopClient.java b/src/main/java/com/phenoml/api/resources/fhir2omop/AsyncRawFhir2OmopClient.java index 3f362908..10d04c12 100644 --- a/src/main/java/com/phenoml/api/resources/fhir2omop/AsyncRawFhir2OmopClient.java +++ b/src/main/java/com/phenoml/api/resources/fhir2omop/AsyncRawFhir2OmopClient.java @@ -39,116 +39,200 @@ public AsyncRawFhir2OmopClient(ClientOptions clientOptions) { } /** - * Maps a FHIR R4 resource or Bundle into OMOP Common Data Model v5.4 rows - * (person, visit_occurrence, condition_occurrence, drug_exposure, - * procedure_occurrence, measurement, observation). - *

Resource support is intentionally limited to the OMOP tables returned by - * this endpoint:

+ * Maps a FHIR R4 resource or Bundle into OMOP Common Data Model v5.4 rows, + * grouped by destination table in tables. + *

Current resource coverage:

*
    - *
  • Patient -> person
  • + *
  • Patient -> person; deceased[x] can also produce death, and the + * first address can produce location
  • + *
  • observation_period -> one derived row per person with dated visit, + * clinical, or death rows, spanning those dates
  • + *
  • Location -> location and care_site
  • + *
  • Organization -> care_site; its first address can produce location
  • + *
  • HealthcareService -> care_site
  • + *
  • Practitioner and PractitionerRole -> provider
  • *
  • Encounter -> visit_occurrence
  • *
  • Condition -> condition_occurrence
  • *
  • Procedure -> procedure_occurrence
  • *
  • MedicationRequest, MedicationStatement, and * MedicationAdministration -> drug_exposure
  • *
  • Immunization -> drug_exposure
  • - *
  • Observation with a numeric valueQuantity, valueInteger, or - * numeric-looking valueString (for example "<2") -> measurement
  • - *
  • non-numeric Observation -> observation
  • + *
  • Observation -> measurement or observation. For coded + * Observations, the resolved OMOP concept domain selects the table; value + * form only breaks ties. For text-only Observations, numeric values route + * to measurement and nonnumeric values to observation.
  • *
  • AllergyIntolerance -> observation
  • *
- *

Medication is supported only as reference data for medication - * resources; it is not emitted as its own row because OMOP CDM has no - * Medication table. Other reference/admin resources such as Practitioner, - * Organization, Location, Coverage, and Claim, and clinical - * workflow/document resources such as DiagnosticReport, ServiceRequest, - * CarePlan, DocumentReference, Composition, Specimen, and - * DeviceUseStatement, are currently accepted in a Bundle but are not - * shaped into OMOP rows. Unsupported resource types are ignored rather than - * listed under dropped; dropped is reserved for supported resource types - * that were missing the subject/patient, code, or medication reference data - * needed to produce a valid row.

- *

Each resource's primary clinical coding is resolved to a standard OMOP - * concept_id. Alongside the OMOP rows grouped by table (tables), the - * response carries mappings (how each source coding resolved, linked back - * to the row it produced), dropped (resources that could not be shaped - * into a row), vocab_version (the OMOP vocabulary release codes were - * resolved against), and a small summary of the resolution outcomes.

+ *

Medication is reference data for medication resources; it does not + * create its own row because OMOP CDM has no Medication table. Administrative + * linkages (provider, care site, and location) are best-effort and limited to + * references supplied in the request. Their supporting concepts, including + * provider specialty, country, and place of service, are not mapped.

+ *

DiagnosticReport, ServiceRequest, CarePlan, DocumentReference, + * Composition, Specimen, DeviceUseStatement, Coverage, Claim, and + * other unsupported resource types are accepted in a Bundle but ignored: they + * create no row and no dropped entry. dropped is reserved for supported + * row-producing resources that could not be shaped because the subject/patient, + * clinical code/text, or medication data was not usable. A single-Patient + * Bundle can attribute a supported clinical resource with a missing or + * unresolvable subject to that sole person; in a multi-Patient Bundle, that + * resource is dropped instead.

+ *

Coded Observation routing is selected from the resolved OMOP concept + * domain. Numeric and nonnumeric value[x] forms establish the preferred + * target only when the code is valid for both tables. A text-only + * Observation has no resolver target, so numeric values route to + * measurement and nonnumeric values to observation. Numeric values + * populate value_as_number in the selected row; nonnumeric values + * populate value_as_string for an observation or value_source_value + * for a measurement. valueCodeableConcept remains source text and does + * not populate value_as_concept_id; other unsupported value[x] forms + * and Observation components do not populate separate converted values. A + * numeric comparator (<, <=, >, >=) is represented only by a + * measurement's operator_concept_id; units remain source text and have + * unit_concept_id of 0.

+ *

A single standard OMOP concept_id is selected for each clinical row + * after considering all of the resource's supplied codings. Alongside the + * OMOP rows grouped by table (tables), the response carries mappings + * (an entry for every source coding, linked back to the row it produced), + * dropped (resources that could not be shaped into a row), + * vocab_version (the OMOP vocabulary release codes were resolved + * against), and a small summary of the resolution outcomes.

*

A concept_id of 0 is reported, not omitted (OMOP "no matching * concept" semantics): it covers both a coding with no standard match * (UNMAPPED) and an unverified suggestion for a text-only resource - * (UNCHECKED). Only the primary clinical coding is resolved, so - * gender/race/ethnicity/visit/value/unit concept_ids are - * always 0; the one populated non-resolved concept is measurement + * (UNCHECKED). Demographic, visit, categorical-value, and unit concept + * fields currently remain 0; the one populated non-resolved concept is + * measurement * operator_concept_id, set from a value comparator (<, <=, >, >=) - * rather than the resolver. Each *_source_value carries the verbatim FHIR - * coding (system#code), and *_type_concept_id is set to 32817 (EHR).

+ * rather than terminology resolution. Clinical *_source_value fields + * preserve the selected FHIR coding (system#code, or code when no + * system is supplied), falling back to source text for text-only resources. + * Known OID-form coding systems are accepted as either FHIR OID URNs (for + * example, urn:oid:2.16.840.1.113883.6.1 for LOINC) or bare OIDs, and + * are normalized to their canonical system URLs before terminology + * resolution. *_source_value and mappings[].source_system report that + * canonical URL, so the OID and URL forms produce the same mapping. An + * unknown OID is not rewritten and may be UNMAPPED. + * Other *_source_value fields preserve row-specific raw source values, + * such as resource identifiers, names, units, or status codes, and + * *_type_concept_id is set to 32817 (EHR).

*

Medication codes are resolved whether they appear inline * (medicationCodeableConcept) or via a medicationReference to a contained, * relative (Type/id), or bundle-entry (urn:uuid) Medication resource. * Resources that cannot be shaped into a row — a medication with no usable * code, resolvable reference, or display, or any clinical resource whose * subject/patient reference cannot be tied to a person — are reported under - * dropped rather than emitted as blank rows. The - * bundle must contain at least one Patient resource.

+ * dropped rather than emitted as blank rows. The Bundle must contain at + * least one Patient resource.

+ *

All row IDs start at 1 for each request and are not stable or global. + * For clinical conversion rows whose resource supplies an id, mappings + * associates each row with that source FHIR resource ID. A person row + * retains the Patient ID or its first identifier value in + * person_source_value, when present; other reference and derived rows do + * not uniformly carry a FHIR resource ID. Input resources without those + * source identifiers cannot be correlated across responses from the + * returned rows alone. Consumers combining responses need to establish + * their own stable keys and remap every primary and foreign key together.

*/ public CompletableFuture> create(CreateOmopRequest request) { return create(request, null); } /** - * Maps a FHIR R4 resource or Bundle into OMOP Common Data Model v5.4 rows - * (person, visit_occurrence, condition_occurrence, drug_exposure, - * procedure_occurrence, measurement, observation). - *

Resource support is intentionally limited to the OMOP tables returned by - * this endpoint:

+ * Maps a FHIR R4 resource or Bundle into OMOP Common Data Model v5.4 rows, + * grouped by destination table in tables. + *

Current resource coverage:

*
    - *
  • Patient -> person
  • + *
  • Patient -> person; deceased[x] can also produce death, and the + * first address can produce location
  • + *
  • observation_period -> one derived row per person with dated visit, + * clinical, or death rows, spanning those dates
  • + *
  • Location -> location and care_site
  • + *
  • Organization -> care_site; its first address can produce location
  • + *
  • HealthcareService -> care_site
  • + *
  • Practitioner and PractitionerRole -> provider
  • *
  • Encounter -> visit_occurrence
  • *
  • Condition -> condition_occurrence
  • *
  • Procedure -> procedure_occurrence
  • *
  • MedicationRequest, MedicationStatement, and * MedicationAdministration -> drug_exposure
  • *
  • Immunization -> drug_exposure
  • - *
  • Observation with a numeric valueQuantity, valueInteger, or - * numeric-looking valueString (for example "<2") -> measurement
  • - *
  • non-numeric Observation -> observation
  • + *
  • Observation -> measurement or observation. For coded + * Observations, the resolved OMOP concept domain selects the table; value + * form only breaks ties. For text-only Observations, numeric values route + * to measurement and nonnumeric values to observation.
  • *
  • AllergyIntolerance -> observation
  • *
- *

Medication is supported only as reference data for medication - * resources; it is not emitted as its own row because OMOP CDM has no - * Medication table. Other reference/admin resources such as Practitioner, - * Organization, Location, Coverage, and Claim, and clinical - * workflow/document resources such as DiagnosticReport, ServiceRequest, - * CarePlan, DocumentReference, Composition, Specimen, and - * DeviceUseStatement, are currently accepted in a Bundle but are not - * shaped into OMOP rows. Unsupported resource types are ignored rather than - * listed under dropped; dropped is reserved for supported resource types - * that were missing the subject/patient, code, or medication reference data - * needed to produce a valid row.

- *

Each resource's primary clinical coding is resolved to a standard OMOP - * concept_id. Alongside the OMOP rows grouped by table (tables), the - * response carries mappings (how each source coding resolved, linked back - * to the row it produced), dropped (resources that could not be shaped - * into a row), vocab_version (the OMOP vocabulary release codes were - * resolved against), and a small summary of the resolution outcomes.

+ *

Medication is reference data for medication resources; it does not + * create its own row because OMOP CDM has no Medication table. Administrative + * linkages (provider, care site, and location) are best-effort and limited to + * references supplied in the request. Their supporting concepts, including + * provider specialty, country, and place of service, are not mapped.

+ *

DiagnosticReport, ServiceRequest, CarePlan, DocumentReference, + * Composition, Specimen, DeviceUseStatement, Coverage, Claim, and + * other unsupported resource types are accepted in a Bundle but ignored: they + * create no row and no dropped entry. dropped is reserved for supported + * row-producing resources that could not be shaped because the subject/patient, + * clinical code/text, or medication data was not usable. A single-Patient + * Bundle can attribute a supported clinical resource with a missing or + * unresolvable subject to that sole person; in a multi-Patient Bundle, that + * resource is dropped instead.

+ *

Coded Observation routing is selected from the resolved OMOP concept + * domain. Numeric and nonnumeric value[x] forms establish the preferred + * target only when the code is valid for both tables. A text-only + * Observation has no resolver target, so numeric values route to + * measurement and nonnumeric values to observation. Numeric values + * populate value_as_number in the selected row; nonnumeric values + * populate value_as_string for an observation or value_source_value + * for a measurement. valueCodeableConcept remains source text and does + * not populate value_as_concept_id; other unsupported value[x] forms + * and Observation components do not populate separate converted values. A + * numeric comparator (<, <=, >, >=) is represented only by a + * measurement's operator_concept_id; units remain source text and have + * unit_concept_id of 0.

+ *

A single standard OMOP concept_id is selected for each clinical row + * after considering all of the resource's supplied codings. Alongside the + * OMOP rows grouped by table (tables), the response carries mappings + * (an entry for every source coding, linked back to the row it produced), + * dropped (resources that could not be shaped into a row), + * vocab_version (the OMOP vocabulary release codes were resolved + * against), and a small summary of the resolution outcomes.

*

A concept_id of 0 is reported, not omitted (OMOP "no matching * concept" semantics): it covers both a coding with no standard match * (UNMAPPED) and an unverified suggestion for a text-only resource - * (UNCHECKED). Only the primary clinical coding is resolved, so - * gender/race/ethnicity/visit/value/unit concept_ids are - * always 0; the one populated non-resolved concept is measurement + * (UNCHECKED). Demographic, visit, categorical-value, and unit concept + * fields currently remain 0; the one populated non-resolved concept is + * measurement * operator_concept_id, set from a value comparator (<, <=, >, >=) - * rather than the resolver. Each *_source_value carries the verbatim FHIR - * coding (system#code), and *_type_concept_id is set to 32817 (EHR).

+ * rather than terminology resolution. Clinical *_source_value fields + * preserve the selected FHIR coding (system#code, or code when no + * system is supplied), falling back to source text for text-only resources. + * Known OID-form coding systems are accepted as either FHIR OID URNs (for + * example, urn:oid:2.16.840.1.113883.6.1 for LOINC) or bare OIDs, and + * are normalized to their canonical system URLs before terminology + * resolution. *_source_value and mappings[].source_system report that + * canonical URL, so the OID and URL forms produce the same mapping. An + * unknown OID is not rewritten and may be UNMAPPED. + * Other *_source_value fields preserve row-specific raw source values, + * such as resource identifiers, names, units, or status codes, and + * *_type_concept_id is set to 32817 (EHR).

*

Medication codes are resolved whether they appear inline * (medicationCodeableConcept) or via a medicationReference to a contained, * relative (Type/id), or bundle-entry (urn:uuid) Medication resource. * Resources that cannot be shaped into a row — a medication with no usable * code, resolvable reference, or display, or any clinical resource whose * subject/patient reference cannot be tied to a person — are reported under - * dropped rather than emitted as blank rows. The - * bundle must contain at least one Patient resource.

+ * dropped rather than emitted as blank rows. The Bundle must contain at + * least one Patient resource.

+ *

All row IDs start at 1 for each request and are not stable or global. + * For clinical conversion rows whose resource supplies an id, mappings + * associates each row with that source FHIR resource ID. A person row + * retains the Patient ID or its first identifier value in + * person_source_value, when present; other reference and derived rows do + * not uniformly carry a FHIR resource ID. Input resources without those + * source identifiers cannot be correlated across responses from the + * returned rows alone. Consumers combining responses need to establish + * their own stable keys and remap every primary and foreign key together.

*/ public CompletableFuture> create( CreateOmopRequest request, RequestOptions requestOptions) { diff --git a/src/main/java/com/phenoml/api/resources/fhir2omop/Fhir2OmopClient.java b/src/main/java/com/phenoml/api/resources/fhir2omop/Fhir2OmopClient.java index 5def4f52..200b662e 100644 --- a/src/main/java/com/phenoml/api/resources/fhir2omop/Fhir2OmopClient.java +++ b/src/main/java/com/phenoml/api/resources/fhir2omop/Fhir2OmopClient.java @@ -26,116 +26,200 @@ public RawFhir2OmopClient withRawResponse() { } /** - * Maps a FHIR R4 resource or Bundle into OMOP Common Data Model v5.4 rows - * (person, visit_occurrence, condition_occurrence, drug_exposure, - * procedure_occurrence, measurement, observation). - *

Resource support is intentionally limited to the OMOP tables returned by - * this endpoint:

+ * Maps a FHIR R4 resource or Bundle into OMOP Common Data Model v5.4 rows, + * grouped by destination table in tables. + *

Current resource coverage:

*
    - *
  • Patient -> person
  • + *
  • Patient -> person; deceased[x] can also produce death, and the + * first address can produce location
  • + *
  • observation_period -> one derived row per person with dated visit, + * clinical, or death rows, spanning those dates
  • + *
  • Location -> location and care_site
  • + *
  • Organization -> care_site; its first address can produce location
  • + *
  • HealthcareService -> care_site
  • + *
  • Practitioner and PractitionerRole -> provider
  • *
  • Encounter -> visit_occurrence
  • *
  • Condition -> condition_occurrence
  • *
  • Procedure -> procedure_occurrence
  • *
  • MedicationRequest, MedicationStatement, and * MedicationAdministration -> drug_exposure
  • *
  • Immunization -> drug_exposure
  • - *
  • Observation with a numeric valueQuantity, valueInteger, or - * numeric-looking valueString (for example "<2") -> measurement
  • - *
  • non-numeric Observation -> observation
  • + *
  • Observation -> measurement or observation. For coded + * Observations, the resolved OMOP concept domain selects the table; value + * form only breaks ties. For text-only Observations, numeric values route + * to measurement and nonnumeric values to observation.
  • *
  • AllergyIntolerance -> observation
  • *
- *

Medication is supported only as reference data for medication - * resources; it is not emitted as its own row because OMOP CDM has no - * Medication table. Other reference/admin resources such as Practitioner, - * Organization, Location, Coverage, and Claim, and clinical - * workflow/document resources such as DiagnosticReport, ServiceRequest, - * CarePlan, DocumentReference, Composition, Specimen, and - * DeviceUseStatement, are currently accepted in a Bundle but are not - * shaped into OMOP rows. Unsupported resource types are ignored rather than - * listed under dropped; dropped is reserved for supported resource types - * that were missing the subject/patient, code, or medication reference data - * needed to produce a valid row.

- *

Each resource's primary clinical coding is resolved to a standard OMOP - * concept_id. Alongside the OMOP rows grouped by table (tables), the - * response carries mappings (how each source coding resolved, linked back - * to the row it produced), dropped (resources that could not be shaped - * into a row), vocab_version (the OMOP vocabulary release codes were - * resolved against), and a small summary of the resolution outcomes.

+ *

Medication is reference data for medication resources; it does not + * create its own row because OMOP CDM has no Medication table. Administrative + * linkages (provider, care site, and location) are best-effort and limited to + * references supplied in the request. Their supporting concepts, including + * provider specialty, country, and place of service, are not mapped.

+ *

DiagnosticReport, ServiceRequest, CarePlan, DocumentReference, + * Composition, Specimen, DeviceUseStatement, Coverage, Claim, and + * other unsupported resource types are accepted in a Bundle but ignored: they + * create no row and no dropped entry. dropped is reserved for supported + * row-producing resources that could not be shaped because the subject/patient, + * clinical code/text, or medication data was not usable. A single-Patient + * Bundle can attribute a supported clinical resource with a missing or + * unresolvable subject to that sole person; in a multi-Patient Bundle, that + * resource is dropped instead.

+ *

Coded Observation routing is selected from the resolved OMOP concept + * domain. Numeric and nonnumeric value[x] forms establish the preferred + * target only when the code is valid for both tables. A text-only + * Observation has no resolver target, so numeric values route to + * measurement and nonnumeric values to observation. Numeric values + * populate value_as_number in the selected row; nonnumeric values + * populate value_as_string for an observation or value_source_value + * for a measurement. valueCodeableConcept remains source text and does + * not populate value_as_concept_id; other unsupported value[x] forms + * and Observation components do not populate separate converted values. A + * numeric comparator (<, <=, >, >=) is represented only by a + * measurement's operator_concept_id; units remain source text and have + * unit_concept_id of 0.

+ *

A single standard OMOP concept_id is selected for each clinical row + * after considering all of the resource's supplied codings. Alongside the + * OMOP rows grouped by table (tables), the response carries mappings + * (an entry for every source coding, linked back to the row it produced), + * dropped (resources that could not be shaped into a row), + * vocab_version (the OMOP vocabulary release codes were resolved + * against), and a small summary of the resolution outcomes.

*

A concept_id of 0 is reported, not omitted (OMOP "no matching * concept" semantics): it covers both a coding with no standard match * (UNMAPPED) and an unverified suggestion for a text-only resource - * (UNCHECKED). Only the primary clinical coding is resolved, so - * gender/race/ethnicity/visit/value/unit concept_ids are - * always 0; the one populated non-resolved concept is measurement + * (UNCHECKED). Demographic, visit, categorical-value, and unit concept + * fields currently remain 0; the one populated non-resolved concept is + * measurement * operator_concept_id, set from a value comparator (<, <=, >, >=) - * rather than the resolver. Each *_source_value carries the verbatim FHIR - * coding (system#code), and *_type_concept_id is set to 32817 (EHR).

+ * rather than terminology resolution. Clinical *_source_value fields + * preserve the selected FHIR coding (system#code, or code when no + * system is supplied), falling back to source text for text-only resources. + * Known OID-form coding systems are accepted as either FHIR OID URNs (for + * example, urn:oid:2.16.840.1.113883.6.1 for LOINC) or bare OIDs, and + * are normalized to their canonical system URLs before terminology + * resolution. *_source_value and mappings[].source_system report that + * canonical URL, so the OID and URL forms produce the same mapping. An + * unknown OID is not rewritten and may be UNMAPPED. + * Other *_source_value fields preserve row-specific raw source values, + * such as resource identifiers, names, units, or status codes, and + * *_type_concept_id is set to 32817 (EHR).

*

Medication codes are resolved whether they appear inline * (medicationCodeableConcept) or via a medicationReference to a contained, * relative (Type/id), or bundle-entry (urn:uuid) Medication resource. * Resources that cannot be shaped into a row — a medication with no usable * code, resolvable reference, or display, or any clinical resource whose * subject/patient reference cannot be tied to a person — are reported under - * dropped rather than emitted as blank rows. The - * bundle must contain at least one Patient resource.

+ * dropped rather than emitted as blank rows. The Bundle must contain at + * least one Patient resource.

+ *

All row IDs start at 1 for each request and are not stable or global. + * For clinical conversion rows whose resource supplies an id, mappings + * associates each row with that source FHIR resource ID. A person row + * retains the Patient ID or its first identifier value in + * person_source_value, when present; other reference and derived rows do + * not uniformly carry a FHIR resource ID. Input resources without those + * source identifiers cannot be correlated across responses from the + * returned rows alone. Consumers combining responses need to establish + * their own stable keys and remap every primary and foreign key together.

*/ public CreateOmopResponse create(CreateOmopRequest request) { return this.rawClient.create(request).body(); } /** - * Maps a FHIR R4 resource or Bundle into OMOP Common Data Model v5.4 rows - * (person, visit_occurrence, condition_occurrence, drug_exposure, - * procedure_occurrence, measurement, observation). - *

Resource support is intentionally limited to the OMOP tables returned by - * this endpoint:

+ * Maps a FHIR R4 resource or Bundle into OMOP Common Data Model v5.4 rows, + * grouped by destination table in tables. + *

Current resource coverage:

*
    - *
  • Patient -> person
  • + *
  • Patient -> person; deceased[x] can also produce death, and the + * first address can produce location
  • + *
  • observation_period -> one derived row per person with dated visit, + * clinical, or death rows, spanning those dates
  • + *
  • Location -> location and care_site
  • + *
  • Organization -> care_site; its first address can produce location
  • + *
  • HealthcareService -> care_site
  • + *
  • Practitioner and PractitionerRole -> provider
  • *
  • Encounter -> visit_occurrence
  • *
  • Condition -> condition_occurrence
  • *
  • Procedure -> procedure_occurrence
  • *
  • MedicationRequest, MedicationStatement, and * MedicationAdministration -> drug_exposure
  • *
  • Immunization -> drug_exposure
  • - *
  • Observation with a numeric valueQuantity, valueInteger, or - * numeric-looking valueString (for example "<2") -> measurement
  • - *
  • non-numeric Observation -> observation
  • + *
  • Observation -> measurement or observation. For coded + * Observations, the resolved OMOP concept domain selects the table; value + * form only breaks ties. For text-only Observations, numeric values route + * to measurement and nonnumeric values to observation.
  • *
  • AllergyIntolerance -> observation
  • *
- *

Medication is supported only as reference data for medication - * resources; it is not emitted as its own row because OMOP CDM has no - * Medication table. Other reference/admin resources such as Practitioner, - * Organization, Location, Coverage, and Claim, and clinical - * workflow/document resources such as DiagnosticReport, ServiceRequest, - * CarePlan, DocumentReference, Composition, Specimen, and - * DeviceUseStatement, are currently accepted in a Bundle but are not - * shaped into OMOP rows. Unsupported resource types are ignored rather than - * listed under dropped; dropped is reserved for supported resource types - * that were missing the subject/patient, code, or medication reference data - * needed to produce a valid row.

- *

Each resource's primary clinical coding is resolved to a standard OMOP - * concept_id. Alongside the OMOP rows grouped by table (tables), the - * response carries mappings (how each source coding resolved, linked back - * to the row it produced), dropped (resources that could not be shaped - * into a row), vocab_version (the OMOP vocabulary release codes were - * resolved against), and a small summary of the resolution outcomes.

+ *

Medication is reference data for medication resources; it does not + * create its own row because OMOP CDM has no Medication table. Administrative + * linkages (provider, care site, and location) are best-effort and limited to + * references supplied in the request. Their supporting concepts, including + * provider specialty, country, and place of service, are not mapped.

+ *

DiagnosticReport, ServiceRequest, CarePlan, DocumentReference, + * Composition, Specimen, DeviceUseStatement, Coverage, Claim, and + * other unsupported resource types are accepted in a Bundle but ignored: they + * create no row and no dropped entry. dropped is reserved for supported + * row-producing resources that could not be shaped because the subject/patient, + * clinical code/text, or medication data was not usable. A single-Patient + * Bundle can attribute a supported clinical resource with a missing or + * unresolvable subject to that sole person; in a multi-Patient Bundle, that + * resource is dropped instead.

+ *

Coded Observation routing is selected from the resolved OMOP concept + * domain. Numeric and nonnumeric value[x] forms establish the preferred + * target only when the code is valid for both tables. A text-only + * Observation has no resolver target, so numeric values route to + * measurement and nonnumeric values to observation. Numeric values + * populate value_as_number in the selected row; nonnumeric values + * populate value_as_string for an observation or value_source_value + * for a measurement. valueCodeableConcept remains source text and does + * not populate value_as_concept_id; other unsupported value[x] forms + * and Observation components do not populate separate converted values. A + * numeric comparator (<, <=, >, >=) is represented only by a + * measurement's operator_concept_id; units remain source text and have + * unit_concept_id of 0.

+ *

A single standard OMOP concept_id is selected for each clinical row + * after considering all of the resource's supplied codings. Alongside the + * OMOP rows grouped by table (tables), the response carries mappings + * (an entry for every source coding, linked back to the row it produced), + * dropped (resources that could not be shaped into a row), + * vocab_version (the OMOP vocabulary release codes were resolved + * against), and a small summary of the resolution outcomes.

*

A concept_id of 0 is reported, not omitted (OMOP "no matching * concept" semantics): it covers both a coding with no standard match * (UNMAPPED) and an unverified suggestion for a text-only resource - * (UNCHECKED). Only the primary clinical coding is resolved, so - * gender/race/ethnicity/visit/value/unit concept_ids are - * always 0; the one populated non-resolved concept is measurement + * (UNCHECKED). Demographic, visit, categorical-value, and unit concept + * fields currently remain 0; the one populated non-resolved concept is + * measurement * operator_concept_id, set from a value comparator (<, <=, >, >=) - * rather than the resolver. Each *_source_value carries the verbatim FHIR - * coding (system#code), and *_type_concept_id is set to 32817 (EHR).

+ * rather than terminology resolution. Clinical *_source_value fields + * preserve the selected FHIR coding (system#code, or code when no + * system is supplied), falling back to source text for text-only resources. + * Known OID-form coding systems are accepted as either FHIR OID URNs (for + * example, urn:oid:2.16.840.1.113883.6.1 for LOINC) or bare OIDs, and + * are normalized to their canonical system URLs before terminology + * resolution. *_source_value and mappings[].source_system report that + * canonical URL, so the OID and URL forms produce the same mapping. An + * unknown OID is not rewritten and may be UNMAPPED. + * Other *_source_value fields preserve row-specific raw source values, + * such as resource identifiers, names, units, or status codes, and + * *_type_concept_id is set to 32817 (EHR).

*

Medication codes are resolved whether they appear inline * (medicationCodeableConcept) or via a medicationReference to a contained, * relative (Type/id), or bundle-entry (urn:uuid) Medication resource. * Resources that cannot be shaped into a row — a medication with no usable * code, resolvable reference, or display, or any clinical resource whose * subject/patient reference cannot be tied to a person — are reported under - * dropped rather than emitted as blank rows. The - * bundle must contain at least one Patient resource.

+ * dropped rather than emitted as blank rows. The Bundle must contain at + * least one Patient resource.

+ *

All row IDs start at 1 for each request and are not stable or global. + * For clinical conversion rows whose resource supplies an id, mappings + * associates each row with that source FHIR resource ID. A person row + * retains the Patient ID or its first identifier value in + * person_source_value, when present; other reference and derived rows do + * not uniformly carry a FHIR resource ID. Input resources without those + * source identifiers cannot be correlated across responses from the + * returned rows alone. Consumers combining responses need to establish + * their own stable keys and remap every primary and foreign key together.

*/ public CreateOmopResponse create(CreateOmopRequest request, RequestOptions requestOptions) { return this.rawClient.create(request, requestOptions).body(); diff --git a/src/main/java/com/phenoml/api/resources/fhir2omop/RawFhir2OmopClient.java b/src/main/java/com/phenoml/api/resources/fhir2omop/RawFhir2OmopClient.java index f4f4e265..adca1be3 100644 --- a/src/main/java/com/phenoml/api/resources/fhir2omop/RawFhir2OmopClient.java +++ b/src/main/java/com/phenoml/api/resources/fhir2omop/RawFhir2OmopClient.java @@ -35,116 +35,200 @@ public RawFhir2OmopClient(ClientOptions clientOptions) { } /** - * Maps a FHIR R4 resource or Bundle into OMOP Common Data Model v5.4 rows - * (person, visit_occurrence, condition_occurrence, drug_exposure, - * procedure_occurrence, measurement, observation). - *

Resource support is intentionally limited to the OMOP tables returned by - * this endpoint:

+ * Maps a FHIR R4 resource or Bundle into OMOP Common Data Model v5.4 rows, + * grouped by destination table in tables. + *

Current resource coverage:

*
    - *
  • Patient -> person
  • + *
  • Patient -> person; deceased[x] can also produce death, and the + * first address can produce location
  • + *
  • observation_period -> one derived row per person with dated visit, + * clinical, or death rows, spanning those dates
  • + *
  • Location -> location and care_site
  • + *
  • Organization -> care_site; its first address can produce location
  • + *
  • HealthcareService -> care_site
  • + *
  • Practitioner and PractitionerRole -> provider
  • *
  • Encounter -> visit_occurrence
  • *
  • Condition -> condition_occurrence
  • *
  • Procedure -> procedure_occurrence
  • *
  • MedicationRequest, MedicationStatement, and * MedicationAdministration -> drug_exposure
  • *
  • Immunization -> drug_exposure
  • - *
  • Observation with a numeric valueQuantity, valueInteger, or - * numeric-looking valueString (for example "<2") -> measurement
  • - *
  • non-numeric Observation -> observation
  • + *
  • Observation -> measurement or observation. For coded + * Observations, the resolved OMOP concept domain selects the table; value + * form only breaks ties. For text-only Observations, numeric values route + * to measurement and nonnumeric values to observation.
  • *
  • AllergyIntolerance -> observation
  • *
- *

Medication is supported only as reference data for medication - * resources; it is not emitted as its own row because OMOP CDM has no - * Medication table. Other reference/admin resources such as Practitioner, - * Organization, Location, Coverage, and Claim, and clinical - * workflow/document resources such as DiagnosticReport, ServiceRequest, - * CarePlan, DocumentReference, Composition, Specimen, and - * DeviceUseStatement, are currently accepted in a Bundle but are not - * shaped into OMOP rows. Unsupported resource types are ignored rather than - * listed under dropped; dropped is reserved for supported resource types - * that were missing the subject/patient, code, or medication reference data - * needed to produce a valid row.

- *

Each resource's primary clinical coding is resolved to a standard OMOP - * concept_id. Alongside the OMOP rows grouped by table (tables), the - * response carries mappings (how each source coding resolved, linked back - * to the row it produced), dropped (resources that could not be shaped - * into a row), vocab_version (the OMOP vocabulary release codes were - * resolved against), and a small summary of the resolution outcomes.

+ *

Medication is reference data for medication resources; it does not + * create its own row because OMOP CDM has no Medication table. Administrative + * linkages (provider, care site, and location) are best-effort and limited to + * references supplied in the request. Their supporting concepts, including + * provider specialty, country, and place of service, are not mapped.

+ *

DiagnosticReport, ServiceRequest, CarePlan, DocumentReference, + * Composition, Specimen, DeviceUseStatement, Coverage, Claim, and + * other unsupported resource types are accepted in a Bundle but ignored: they + * create no row and no dropped entry. dropped is reserved for supported + * row-producing resources that could not be shaped because the subject/patient, + * clinical code/text, or medication data was not usable. A single-Patient + * Bundle can attribute a supported clinical resource with a missing or + * unresolvable subject to that sole person; in a multi-Patient Bundle, that + * resource is dropped instead.

+ *

Coded Observation routing is selected from the resolved OMOP concept + * domain. Numeric and nonnumeric value[x] forms establish the preferred + * target only when the code is valid for both tables. A text-only + * Observation has no resolver target, so numeric values route to + * measurement and nonnumeric values to observation. Numeric values + * populate value_as_number in the selected row; nonnumeric values + * populate value_as_string for an observation or value_source_value + * for a measurement. valueCodeableConcept remains source text and does + * not populate value_as_concept_id; other unsupported value[x] forms + * and Observation components do not populate separate converted values. A + * numeric comparator (<, <=, >, >=) is represented only by a + * measurement's operator_concept_id; units remain source text and have + * unit_concept_id of 0.

+ *

A single standard OMOP concept_id is selected for each clinical row + * after considering all of the resource's supplied codings. Alongside the + * OMOP rows grouped by table (tables), the response carries mappings + * (an entry for every source coding, linked back to the row it produced), + * dropped (resources that could not be shaped into a row), + * vocab_version (the OMOP vocabulary release codes were resolved + * against), and a small summary of the resolution outcomes.

*

A concept_id of 0 is reported, not omitted (OMOP "no matching * concept" semantics): it covers both a coding with no standard match * (UNMAPPED) and an unverified suggestion for a text-only resource - * (UNCHECKED). Only the primary clinical coding is resolved, so - * gender/race/ethnicity/visit/value/unit concept_ids are - * always 0; the one populated non-resolved concept is measurement + * (UNCHECKED). Demographic, visit, categorical-value, and unit concept + * fields currently remain 0; the one populated non-resolved concept is + * measurement * operator_concept_id, set from a value comparator (<, <=, >, >=) - * rather than the resolver. Each *_source_value carries the verbatim FHIR - * coding (system#code), and *_type_concept_id is set to 32817 (EHR).

+ * rather than terminology resolution. Clinical *_source_value fields + * preserve the selected FHIR coding (system#code, or code when no + * system is supplied), falling back to source text for text-only resources. + * Known OID-form coding systems are accepted as either FHIR OID URNs (for + * example, urn:oid:2.16.840.1.113883.6.1 for LOINC) or bare OIDs, and + * are normalized to their canonical system URLs before terminology + * resolution. *_source_value and mappings[].source_system report that + * canonical URL, so the OID and URL forms produce the same mapping. An + * unknown OID is not rewritten and may be UNMAPPED. + * Other *_source_value fields preserve row-specific raw source values, + * such as resource identifiers, names, units, or status codes, and + * *_type_concept_id is set to 32817 (EHR).

*

Medication codes are resolved whether they appear inline * (medicationCodeableConcept) or via a medicationReference to a contained, * relative (Type/id), or bundle-entry (urn:uuid) Medication resource. * Resources that cannot be shaped into a row — a medication with no usable * code, resolvable reference, or display, or any clinical resource whose * subject/patient reference cannot be tied to a person — are reported under - * dropped rather than emitted as blank rows. The - * bundle must contain at least one Patient resource.

+ * dropped rather than emitted as blank rows. The Bundle must contain at + * least one Patient resource.

+ *

All row IDs start at 1 for each request and are not stable or global. + * For clinical conversion rows whose resource supplies an id, mappings + * associates each row with that source FHIR resource ID. A person row + * retains the Patient ID or its first identifier value in + * person_source_value, when present; other reference and derived rows do + * not uniformly carry a FHIR resource ID. Input resources without those + * source identifiers cannot be correlated across responses from the + * returned rows alone. Consumers combining responses need to establish + * their own stable keys and remap every primary and foreign key together.

*/ public PhenomlClientHttpResponse create(CreateOmopRequest request) { return create(request, null); } /** - * Maps a FHIR R4 resource or Bundle into OMOP Common Data Model v5.4 rows - * (person, visit_occurrence, condition_occurrence, drug_exposure, - * procedure_occurrence, measurement, observation). - *

Resource support is intentionally limited to the OMOP tables returned by - * this endpoint:

+ * Maps a FHIR R4 resource or Bundle into OMOP Common Data Model v5.4 rows, + * grouped by destination table in tables. + *

Current resource coverage:

*
    - *
  • Patient -> person
  • + *
  • Patient -> person; deceased[x] can also produce death, and the + * first address can produce location
  • + *
  • observation_period -> one derived row per person with dated visit, + * clinical, or death rows, spanning those dates
  • + *
  • Location -> location and care_site
  • + *
  • Organization -> care_site; its first address can produce location
  • + *
  • HealthcareService -> care_site
  • + *
  • Practitioner and PractitionerRole -> provider
  • *
  • Encounter -> visit_occurrence
  • *
  • Condition -> condition_occurrence
  • *
  • Procedure -> procedure_occurrence
  • *
  • MedicationRequest, MedicationStatement, and * MedicationAdministration -> drug_exposure
  • *
  • Immunization -> drug_exposure
  • - *
  • Observation with a numeric valueQuantity, valueInteger, or - * numeric-looking valueString (for example "<2") -> measurement
  • - *
  • non-numeric Observation -> observation
  • + *
  • Observation -> measurement or observation. For coded + * Observations, the resolved OMOP concept domain selects the table; value + * form only breaks ties. For text-only Observations, numeric values route + * to measurement and nonnumeric values to observation.
  • *
  • AllergyIntolerance -> observation
  • *
- *

Medication is supported only as reference data for medication - * resources; it is not emitted as its own row because OMOP CDM has no - * Medication table. Other reference/admin resources such as Practitioner, - * Organization, Location, Coverage, and Claim, and clinical - * workflow/document resources such as DiagnosticReport, ServiceRequest, - * CarePlan, DocumentReference, Composition, Specimen, and - * DeviceUseStatement, are currently accepted in a Bundle but are not - * shaped into OMOP rows. Unsupported resource types are ignored rather than - * listed under dropped; dropped is reserved for supported resource types - * that were missing the subject/patient, code, or medication reference data - * needed to produce a valid row.

- *

Each resource's primary clinical coding is resolved to a standard OMOP - * concept_id. Alongside the OMOP rows grouped by table (tables), the - * response carries mappings (how each source coding resolved, linked back - * to the row it produced), dropped (resources that could not be shaped - * into a row), vocab_version (the OMOP vocabulary release codes were - * resolved against), and a small summary of the resolution outcomes.

+ *

Medication is reference data for medication resources; it does not + * create its own row because OMOP CDM has no Medication table. Administrative + * linkages (provider, care site, and location) are best-effort and limited to + * references supplied in the request. Their supporting concepts, including + * provider specialty, country, and place of service, are not mapped.

+ *

DiagnosticReport, ServiceRequest, CarePlan, DocumentReference, + * Composition, Specimen, DeviceUseStatement, Coverage, Claim, and + * other unsupported resource types are accepted in a Bundle but ignored: they + * create no row and no dropped entry. dropped is reserved for supported + * row-producing resources that could not be shaped because the subject/patient, + * clinical code/text, or medication data was not usable. A single-Patient + * Bundle can attribute a supported clinical resource with a missing or + * unresolvable subject to that sole person; in a multi-Patient Bundle, that + * resource is dropped instead.

+ *

Coded Observation routing is selected from the resolved OMOP concept + * domain. Numeric and nonnumeric value[x] forms establish the preferred + * target only when the code is valid for both tables. A text-only + * Observation has no resolver target, so numeric values route to + * measurement and nonnumeric values to observation. Numeric values + * populate value_as_number in the selected row; nonnumeric values + * populate value_as_string for an observation or value_source_value + * for a measurement. valueCodeableConcept remains source text and does + * not populate value_as_concept_id; other unsupported value[x] forms + * and Observation components do not populate separate converted values. A + * numeric comparator (<, <=, >, >=) is represented only by a + * measurement's operator_concept_id; units remain source text and have + * unit_concept_id of 0.

+ *

A single standard OMOP concept_id is selected for each clinical row + * after considering all of the resource's supplied codings. Alongside the + * OMOP rows grouped by table (tables), the response carries mappings + * (an entry for every source coding, linked back to the row it produced), + * dropped (resources that could not be shaped into a row), + * vocab_version (the OMOP vocabulary release codes were resolved + * against), and a small summary of the resolution outcomes.

*

A concept_id of 0 is reported, not omitted (OMOP "no matching * concept" semantics): it covers both a coding with no standard match * (UNMAPPED) and an unverified suggestion for a text-only resource - * (UNCHECKED). Only the primary clinical coding is resolved, so - * gender/race/ethnicity/visit/value/unit concept_ids are - * always 0; the one populated non-resolved concept is measurement + * (UNCHECKED). Demographic, visit, categorical-value, and unit concept + * fields currently remain 0; the one populated non-resolved concept is + * measurement * operator_concept_id, set from a value comparator (<, <=, >, >=) - * rather than the resolver. Each *_source_value carries the verbatim FHIR - * coding (system#code), and *_type_concept_id is set to 32817 (EHR).

+ * rather than terminology resolution. Clinical *_source_value fields + * preserve the selected FHIR coding (system#code, or code when no + * system is supplied), falling back to source text for text-only resources. + * Known OID-form coding systems are accepted as either FHIR OID URNs (for + * example, urn:oid:2.16.840.1.113883.6.1 for LOINC) or bare OIDs, and + * are normalized to their canonical system URLs before terminology + * resolution. *_source_value and mappings[].source_system report that + * canonical URL, so the OID and URL forms produce the same mapping. An + * unknown OID is not rewritten and may be UNMAPPED. + * Other *_source_value fields preserve row-specific raw source values, + * such as resource identifiers, names, units, or status codes, and + * *_type_concept_id is set to 32817 (EHR).

*

Medication codes are resolved whether they appear inline * (medicationCodeableConcept) or via a medicationReference to a contained, * relative (Type/id), or bundle-entry (urn:uuid) Medication resource. * Resources that cannot be shaped into a row — a medication with no usable * code, resolvable reference, or display, or any clinical resource whose * subject/patient reference cannot be tied to a person — are reported under - * dropped rather than emitted as blank rows. The - * bundle must contain at least one Patient resource.

+ * dropped rather than emitted as blank rows. The Bundle must contain at + * least one Patient resource.

+ *

All row IDs start at 1 for each request and are not stable or global. + * For clinical conversion rows whose resource supplies an id, mappings + * associates each row with that source FHIR resource ID. A person row + * retains the Patient ID or its first identifier value in + * person_source_value, when present; other reference and derived rows do + * not uniformly carry a FHIR resource ID. Input resources without those + * source identifiers cannot be correlated across responses from the + * returned rows alone. Consumers combining responses need to establish + * their own stable keys and remap every primary and foreign key together.

*/ public PhenomlClientHttpResponse create( CreateOmopRequest request, RequestOptions requestOptions) { diff --git a/src/main/java/com/phenoml/api/resources/fhir2omop/requests/CreateOmopRequest.java b/src/main/java/com/phenoml/api/resources/fhir2omop/requests/CreateOmopRequest.java index 34b8f6da..13327997 100644 --- a/src/main/java/com/phenoml/api/resources/fhir2omop/requests/CreateOmopRequest.java +++ b/src/main/java/com/phenoml/api/resources/fhir2omop/requests/CreateOmopRequest.java @@ -32,11 +32,12 @@ private CreateOmopRequest(Map fhirResources, Map /** * @return FHIR resources (single resource or Bundle). Must contain at least one * Patient resource. Supported row-producing resources are Patient, - * Encounter, Condition, Procedure, MedicationRequest, + * Location, Organization, HealthcareService, Practitioner, + * PractitionerRole, Encounter, Condition, Procedure, MedicationRequest, * MedicationStatement, MedicationAdministration, Immunization, * Observation, and AllergyIntolerance. Standalone Medication resources * are consumed by medication references rather than mapped to their own - * table. Other resource types are accepted but ignored. + * table. Unsupported resource types are accepted in a Bundle but ignored. */ @JsonProperty("fhir_resources") public Map getFhirResources() { @@ -89,11 +90,12 @@ public Builder from(CreateOmopRequest other) { /** *

FHIR resources (single resource or Bundle). Must contain at least one * Patient resource. Supported row-producing resources are Patient, - * Encounter, Condition, Procedure, MedicationRequest, + * Location, Organization, HealthcareService, Practitioner, + * PractitionerRole, Encounter, Condition, Procedure, MedicationRequest, * MedicationStatement, MedicationAdministration, Immunization, * Observation, and AllergyIntolerance. Standalone Medication resources * are consumed by medication references rather than mapped to their own - * table. Other resource types are accepted but ignored.

+ * table. Unsupported resource types are accepted in a Bundle but ignored.

*/ @JsonSetter(value = "fhir_resources", nulls = Nulls.SKIP) public Builder fhirResources(Map fhirResources) { diff --git a/src/main/java/com/phenoml/api/resources/fhir2omop/types/CreateOmopResponse.java b/src/main/java/com/phenoml/api/resources/fhir2omop/types/CreateOmopResponse.java index e365995f..3dad063f 100644 --- a/src/main/java/com/phenoml/api/resources/fhir2omop/types/CreateOmopResponse.java +++ b/src/main/java/com/phenoml/api/resources/fhir2omop/types/CreateOmopResponse.java @@ -91,9 +91,9 @@ public Optional> getDropped() { } /** - * @return The OMOP vocabulary release the clinical codes were resolved against - * (e.g. "v20240229"), for reproducibility. Present when at least one - * coded concept was resolved. + * @return The OMOP vocabulary release returned for coded concept resolution + * (for example, "v20240229"), for reproducibility. It is generally + * absent for requests containing only text-only resources. */ @JsonProperty("vocab_version") public Optional getVocabVersion() { @@ -238,9 +238,9 @@ public Builder dropped(List dropped) { } /** - *

The OMOP vocabulary release the clinical codes were resolved against - * (e.g. "v20240229"), for reproducibility. Present when at least one - * coded concept was resolved.

+ *

The OMOP vocabulary release returned for coded concept resolution + * (for example, "v20240229"), for reproducibility. It is generally + * absent for requests containing only text-only resources.

*/ @JsonSetter(value = "vocab_version", nulls = Nulls.SKIP) public Builder vocabVersion(Optional vocabVersion) { diff --git a/src/main/java/com/phenoml/api/resources/fhir2omop/types/MappingEntry.java b/src/main/java/com/phenoml/api/resources/fhir2omop/types/MappingEntry.java index 73bbb33f..ba7a725a 100644 --- a/src/main/java/com/phenoml/api/resources/fhir2omop/types/MappingEntry.java +++ b/src/main/java/com/phenoml/api/resources/fhir2omop/types/MappingEntry.java @@ -40,7 +40,7 @@ public final class MappingEntry { private final Optional targetName; - private final Optional mappingStatus; + private final Optional mappingStatus; private final Optional note; @@ -57,7 +57,7 @@ private MappingEntry( Optional targetVocabulary, Optional targetCode, Optional targetName, - Optional mappingStatus, + Optional mappingStatus, Optional note, Map additionalProperties) { this.resourceType = resourceType; @@ -144,7 +144,7 @@ public Optional getTargetName() { * UNMAPPED (no standard concept found). */ @JsonProperty("mapping_status") - public Optional getMappingStatus() { + public Optional getMappingStatus() { return mappingStatus; } @@ -227,7 +227,7 @@ public static final class Builder { private Optional targetName = Optional.empty(); - private Optional mappingStatus = Optional.empty(); + private Optional mappingStatus = Optional.empty(); private Optional note = Optional.empty(); @@ -381,12 +381,12 @@ public Builder targetName(String targetName) { * UNMAPPED (no standard concept found).

*/ @JsonSetter(value = "mapping_status", nulls = Nulls.SKIP) - public Builder mappingStatus(Optional mappingStatus) { + public Builder mappingStatus(Optional mappingStatus) { this.mappingStatus = mappingStatus; return this; } - public Builder mappingStatus(String mappingStatus) { + public Builder mappingStatus(MappingEntryMappingStatus mappingStatus) { this.mappingStatus = Optional.ofNullable(mappingStatus); return this; } diff --git a/src/main/java/com/phenoml/api/resources/fhir2omop/types/MappingEntryMappingStatus.java b/src/main/java/com/phenoml/api/resources/fhir2omop/types/MappingEntryMappingStatus.java new file mode 100644 index 00000000..c3b619f1 --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/fhir2omop/types/MappingEntryMappingStatus.java @@ -0,0 +1,106 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.fhir2omop.types; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public final class MappingEntryMappingStatus { + public static final MappingEntryMappingStatus ALREADY_STANDARD = + new MappingEntryMappingStatus(Value.ALREADY_STANDARD, "ALREADY_STANDARD"); + + public static final MappingEntryMappingStatus MAPPED = new MappingEntryMappingStatus(Value.MAPPED, "MAPPED"); + + public static final MappingEntryMappingStatus UNCHECKED = + new MappingEntryMappingStatus(Value.UNCHECKED, "UNCHECKED"); + + public static final MappingEntryMappingStatus UNMAPPED = new MappingEntryMappingStatus(Value.UNMAPPED, "UNMAPPED"); + + private final Value value; + + private final String string; + + MappingEntryMappingStatus(Value value, String string) { + this.value = value; + this.string = string; + } + + public Value getEnumValue() { + return value; + } + + @java.lang.Override + @JsonValue + public String toString() { + return this.string; + } + + @java.lang.Override + public boolean equals(Object other) { + return (this == other) + || (other instanceof MappingEntryMappingStatus + && this.string.equals(((MappingEntryMappingStatus) other).string)); + } + + @java.lang.Override + public int hashCode() { + return this.string.hashCode(); + } + + public T visit(Visitor visitor) { + switch (value) { + case ALREADY_STANDARD: + return visitor.visitAlreadyStandard(); + case MAPPED: + return visitor.visitMapped(); + case UNCHECKED: + return visitor.visitUnchecked(); + case UNMAPPED: + return visitor.visitUnmapped(); + case UNKNOWN: + default: + return visitor.visitUnknown(string); + } + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static MappingEntryMappingStatus valueOf(String value) { + switch (value) { + case "ALREADY_STANDARD": + return ALREADY_STANDARD; + case "MAPPED": + return MAPPED; + case "UNCHECKED": + return UNCHECKED; + case "UNMAPPED": + return UNMAPPED; + default: + return new MappingEntryMappingStatus(Value.UNKNOWN, value); + } + } + + public enum Value { + ALREADY_STANDARD, + + MAPPED, + + UNCHECKED, + + UNMAPPED, + + UNKNOWN + } + + public interface Visitor { + T visitAlreadyStandard(); + + T visitMapped(); + + T visitUnchecked(); + + T visitUnmapped(); + + T visitUnknown(String unknownType); + } +} diff --git a/src/main/java/com/phenoml/api/resources/fhir2omop/types/Summary.java b/src/main/java/com/phenoml/api/resources/fhir2omop/types/Summary.java index 5f018ef5..73f1875b 100644 --- a/src/main/java/com/phenoml/api/resources/fhir2omop/types/Summary.java +++ b/src/main/java/com/phenoml/api/resources/fhir2omop/types/Summary.java @@ -44,7 +44,7 @@ private Summary( } /** - * @return Coded concepts already a standard OMOP concept (ALREADY_STANDARD). + * @return Resolution outcomes already a standard OMOP concept (ALREADY_STANDARD). */ @JsonProperty("codes_already_standard") public Optional getCodesAlreadyStandard() { @@ -52,7 +52,7 @@ public Optional getCodesAlreadyStandard() { } /** - * @return Coded concepts mapped or suggested to a standard concept (MAPPED or UNCHECKED). + * @return Resolution outcomes mapped or suggested to a standard concept (MAPPED or UNCHECKED). */ @JsonProperty("codes_normalized") public Optional getCodesNormalized() { @@ -60,7 +60,7 @@ public Optional getCodesNormalized() { } /** - * @return Coded concepts with no standard concept found (UNMAPPED). + * @return Resolution outcomes with no standard concept found (UNMAPPED). */ @JsonProperty("codes_unmapped") public Optional getCodesUnmapped() { @@ -68,7 +68,7 @@ public Optional getCodesUnmapped() { } /** - * @return Share of coded concepts not already standard ((normalized + unmapped) / total). + * @return Share of resolution outcomes not already standard ((normalized + unmapped) / total). */ @JsonProperty("off_vocab_rate") public Optional getOffVocabRate() { @@ -131,7 +131,7 @@ public Builder from(Summary other) { } /** - *

Coded concepts already a standard OMOP concept (ALREADY_STANDARD).

+ *

Resolution outcomes already a standard OMOP concept (ALREADY_STANDARD).

*/ @JsonSetter(value = "codes_already_standard", nulls = Nulls.SKIP) public Builder codesAlreadyStandard(Optional codesAlreadyStandard) { @@ -145,7 +145,7 @@ public Builder codesAlreadyStandard(Integer codesAlreadyStandard) { } /** - *

Coded concepts mapped or suggested to a standard concept (MAPPED or UNCHECKED).

+ *

Resolution outcomes mapped or suggested to a standard concept (MAPPED or UNCHECKED).

*/ @JsonSetter(value = "codes_normalized", nulls = Nulls.SKIP) public Builder codesNormalized(Optional codesNormalized) { @@ -159,7 +159,7 @@ public Builder codesNormalized(Integer codesNormalized) { } /** - *

Coded concepts with no standard concept found (UNMAPPED).

+ *

Resolution outcomes with no standard concept found (UNMAPPED).

*/ @JsonSetter(value = "codes_unmapped", nulls = Nulls.SKIP) public Builder codesUnmapped(Optional codesUnmapped) { @@ -173,7 +173,7 @@ public Builder codesUnmapped(Integer codesUnmapped) { } /** - *

Share of coded concepts not already standard ((normalized + unmapped) / total).

+ *

Share of resolution outcomes not already standard ((normalized + unmapped) / total).

*/ @JsonSetter(value = "off_vocab_rate", nulls = Nulls.SKIP) public Builder offVocabRate(Optional offVocabRate) { diff --git a/src/main/java/com/phenoml/api/resources/implementationguides/errors/ConflictError.java b/src/main/java/com/phenoml/api/resources/implementationguides/errors/ConflictError.java new file mode 100644 index 00000000..8372ea40 --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/implementationguides/errors/ConflictError.java @@ -0,0 +1,32 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.implementationguides.errors; + +import com.phenoml.api.core.PhenomlClientApiException; +import okhttp3.Response; + +public final class ConflictError extends PhenomlClientApiException { + /** + * The body of the response that triggered the exception. + */ + private final Object body; + + public ConflictError(Object body) { + super("ConflictError", 409, body); + this.body = body; + } + + public ConflictError(Object body, Response rawResponse) { + super("ConflictError", 409, body, rawResponse); + this.body = body; + } + + /** + * @return the body + */ + @java.lang.Override + public Object body() { + return this.body; + } +} diff --git a/src/main/java/com/phenoml/api/resources/implementationguides/implementationguides/AsyncImplementationGuidesClient.java b/src/main/java/com/phenoml/api/resources/implementationguides/implementationguides/AsyncImplementationGuidesClient.java index 45d4ba57..f98d3864 100644 --- a/src/main/java/com/phenoml/api/resources/implementationguides/implementationguides/AsyncImplementationGuidesClient.java +++ b/src/main/java/com/phenoml/api/resources/implementationguides/implementationguides/AsyncImplementationGuidesClient.java @@ -5,10 +5,12 @@ import com.phenoml.api.core.ClientOptions; import com.phenoml.api.core.RequestOptions; +import com.phenoml.api.resources.implementationguides.implementationguides.requests.CreateCanonicalImplementationGuideRequest; import com.phenoml.api.resources.implementationguides.implementationguides.requests.UpdateImplementationGuideRequest; import com.phenoml.api.resources.implementationguides.types.ImplementationGuideDetail; import com.phenoml.api.resources.implementationguides.types.ImplementationGuideListResponse; import com.phenoml.api.resources.implementationguides.types.ImplementationGuideSummary; +import com.phenoml.api.resources.implementationguides.types.ImplementationGuideVersionDetail; import java.util.concurrent.CompletableFuture; public class AsyncImplementationGuidesClient { @@ -104,24 +106,49 @@ public CompletableFuture update( } /** - * Deletes the stored metadata for an implementation guide — its - * profile_context and timestamps. Member profiles keep their - * implementation_guide assignment, so a guide still referenced by at least - * one profile continues to appear in listings, just without context or - * timestamps. + * Deletes the stored name-level metadata and any exact canonical package + * versions beneath the guide. Legacy member profile assignments are not + * changed. */ public CompletableFuture delete(String name) { return this.rawClient.delete(name).thenApply(response -> response.body()); } /** - * Deletes the stored metadata for an implementation guide — its - * profile_context and timestamps. Member profiles keep their - * implementation_guide assignment, so a guide still referenced by at least - * one profile continues to appear in listings, just without context or - * timestamps. + * Deletes the stored name-level metadata and any exact canonical package + * versions beneath the guide. Legacy member profile assignments are not + * changed. */ public CompletableFuture delete(String name, RequestOptions requestOptions) { return this.rawClient.delete(name, requestOptions).thenApply(response -> response.body()); } + + /** + * Publishes an exact package beneath this guide family. PR 2 temporarily + * permits one exact package version per guide family; publishing another + * version returns 409 Conflict until multi-version package support lands. + */ + public CompletableFuture createVersion( + String name, CreateCanonicalImplementationGuideRequest request) { + return this.rawClient.createVersion(name, request).thenApply(response -> response.body()); + } + + /** + * Publishes an exact package beneath this guide family. PR 2 temporarily + * permits one exact package version per guide family; publishing another + * version returns 409 Conflict until multi-version package support lands. + */ + public CompletableFuture createVersion( + String name, CreateCanonicalImplementationGuideRequest request, RequestOptions requestOptions) { + return this.rawClient.createVersion(name, request, requestOptions).thenApply(response -> response.body()); + } + + public CompletableFuture getVersion(String name, String version) { + return this.rawClient.getVersion(name, version).thenApply(response -> response.body()); + } + + public CompletableFuture getVersion( + String name, String version, RequestOptions requestOptions) { + return this.rawClient.getVersion(name, version, requestOptions).thenApply(response -> response.body()); + } } diff --git a/src/main/java/com/phenoml/api/resources/implementationguides/implementationguides/AsyncRawImplementationGuidesClient.java b/src/main/java/com/phenoml/api/resources/implementationguides/implementationguides/AsyncRawImplementationGuidesClient.java index 8cbdab52..feb672f5 100644 --- a/src/main/java/com/phenoml/api/resources/implementationguides/implementationguides/AsyncRawImplementationGuidesClient.java +++ b/src/main/java/com/phenoml/api/resources/implementationguides/implementationguides/AsyncRawImplementationGuidesClient.java @@ -13,14 +13,17 @@ import com.phenoml.api.core.RequestOptions; import com.phenoml.api.core.RetryInterceptor; import com.phenoml.api.resources.implementationguides.errors.BadRequestError; +import com.phenoml.api.resources.implementationguides.errors.ConflictError; import com.phenoml.api.resources.implementationguides.errors.ForbiddenError; import com.phenoml.api.resources.implementationguides.errors.InternalServerError; import com.phenoml.api.resources.implementationguides.errors.NotFoundError; import com.phenoml.api.resources.implementationguides.errors.UnauthorizedError; +import com.phenoml.api.resources.implementationguides.implementationguides.requests.CreateCanonicalImplementationGuideRequest; import com.phenoml.api.resources.implementationguides.implementationguides.requests.UpdateImplementationGuideRequest; import com.phenoml.api.resources.implementationguides.types.ImplementationGuideDetail; import com.phenoml.api.resources.implementationguides.types.ImplementationGuideListResponse; import com.phenoml.api.resources.implementationguides.types.ImplementationGuideSummary; +import com.phenoml.api.resources.implementationguides.types.ImplementationGuideVersionDetail; import java.io.IOException; import java.util.concurrent.CompletableFuture; import okhttp3.Call; @@ -381,22 +384,18 @@ public void onFailure(@NotNull Call call, @NotNull IOException e) { } /** - * Deletes the stored metadata for an implementation guide — its - * profile_context and timestamps. Member profiles keep their - * implementation_guide assignment, so a guide still referenced by at least - * one profile continues to appear in listings, just without context or - * timestamps. + * Deletes the stored name-level metadata and any exact canonical package + * versions beneath the guide. Legacy member profile assignments are not + * changed. */ public CompletableFuture> delete(String name) { return delete(name, null); } /** - * Deletes the stored metadata for an implementation guide — its - * profile_context and timestamps. Member profiles keep their - * implementation_guide assignment, so a guide still referenced by at least - * one profile continues to appear in listings, just without context or - * timestamps. + * Deletes the stored name-level metadata and any exact canonical package + * versions beneath the guide. Legacy member profile assignments are not + * changed. */ public CompletableFuture> delete(String name, RequestOptions requestOptions) { HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) @@ -487,4 +486,200 @@ public void onFailure(@NotNull Call call, @NotNull IOException e) { }); return future; } + + /** + * Publishes an exact package beneath this guide family. PR 2 temporarily + * permits one exact package version per guide family; publishing another + * version returns 409 Conflict until multi-version package support lands. + */ + public CompletableFuture> createVersion( + String name, CreateCanonicalImplementationGuideRequest request) { + return createVersion(name, request, null); + } + + /** + * Publishes an exact package beneath this guide family. PR 2 temporarily + * permits one exact package version per guide family; publishing another + * version returns 409 Conflict until multi-version package support lands. + */ + public CompletableFuture> createVersion( + String name, CreateCanonicalImplementationGuideRequest request, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("fhir/implementation-guides") + .addPathSegment(name) + .addPathSegments("versions"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + CompletableFuture> future = + new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue( + responseBodyString, ImplementationGuideVersionDetail.class), + response)); + return; + } + try { + switch (response.code()) { + case 400: + future.completeExceptionally(new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 404: + future.completeExceptionally(new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 409: + future.completeExceptionally(new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + }); + return future; + } + + public CompletableFuture> getVersion( + String name, String version) { + return getVersion(name, version, null); + } + + public CompletableFuture> getVersion( + String name, String version, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("fhir/implementation-guides") + .addPathSegment(name) + .addPathSegments("versions") + .addPathSegment(version); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("GET", null) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + CompletableFuture> future = + new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue( + responseBodyString, ImplementationGuideVersionDetail.class), + response)); + return; + } + try { + switch (response.code()) { + case 400: + future.completeExceptionally(new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 404: + future.completeExceptionally(new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + }); + return future; + } } diff --git a/src/main/java/com/phenoml/api/resources/implementationguides/implementationguides/ImplementationGuidesClient.java b/src/main/java/com/phenoml/api/resources/implementationguides/implementationguides/ImplementationGuidesClient.java index 3e31f2f4..209471b2 100644 --- a/src/main/java/com/phenoml/api/resources/implementationguides/implementationguides/ImplementationGuidesClient.java +++ b/src/main/java/com/phenoml/api/resources/implementationguides/implementationguides/ImplementationGuidesClient.java @@ -5,10 +5,12 @@ import com.phenoml.api.core.ClientOptions; import com.phenoml.api.core.RequestOptions; +import com.phenoml.api.resources.implementationguides.implementationguides.requests.CreateCanonicalImplementationGuideRequest; import com.phenoml.api.resources.implementationguides.implementationguides.requests.UpdateImplementationGuideRequest; import com.phenoml.api.resources.implementationguides.types.ImplementationGuideDetail; import com.phenoml.api.resources.implementationguides.types.ImplementationGuideListResponse; import com.phenoml.api.resources.implementationguides.types.ImplementationGuideSummary; +import com.phenoml.api.resources.implementationguides.types.ImplementationGuideVersionDetail; public class ImplementationGuidesClient { protected final ClientOptions clientOptions; @@ -103,24 +105,48 @@ public ImplementationGuideSummary update( } /** - * Deletes the stored metadata for an implementation guide — its - * profile_context and timestamps. Member profiles keep their - * implementation_guide assignment, so a guide still referenced by at least - * one profile continues to appear in listings, just without context or - * timestamps. + * Deletes the stored name-level metadata and any exact canonical package + * versions beneath the guide. Legacy member profile assignments are not + * changed. */ public void delete(String name) { this.rawClient.delete(name).body(); } /** - * Deletes the stored metadata for an implementation guide — its - * profile_context and timestamps. Member profiles keep their - * implementation_guide assignment, so a guide still referenced by at least - * one profile continues to appear in listings, just without context or - * timestamps. + * Deletes the stored name-level metadata and any exact canonical package + * versions beneath the guide. Legacy member profile assignments are not + * changed. */ public void delete(String name, RequestOptions requestOptions) { this.rawClient.delete(name, requestOptions).body(); } + + /** + * Publishes an exact package beneath this guide family. PR 2 temporarily + * permits one exact package version per guide family; publishing another + * version returns 409 Conflict until multi-version package support lands. + */ + public ImplementationGuideVersionDetail createVersion( + String name, CreateCanonicalImplementationGuideRequest request) { + return this.rawClient.createVersion(name, request).body(); + } + + /** + * Publishes an exact package beneath this guide family. PR 2 temporarily + * permits one exact package version per guide family; publishing another + * version returns 409 Conflict until multi-version package support lands. + */ + public ImplementationGuideVersionDetail createVersion( + String name, CreateCanonicalImplementationGuideRequest request, RequestOptions requestOptions) { + return this.rawClient.createVersion(name, request, requestOptions).body(); + } + + public ImplementationGuideVersionDetail getVersion(String name, String version) { + return this.rawClient.getVersion(name, version).body(); + } + + public ImplementationGuideVersionDetail getVersion(String name, String version, RequestOptions requestOptions) { + return this.rawClient.getVersion(name, version, requestOptions).body(); + } } diff --git a/src/main/java/com/phenoml/api/resources/implementationguides/implementationguides/RawImplementationGuidesClient.java b/src/main/java/com/phenoml/api/resources/implementationguides/implementationguides/RawImplementationGuidesClient.java index 524cd281..e575dbeb 100644 --- a/src/main/java/com/phenoml/api/resources/implementationguides/implementationguides/RawImplementationGuidesClient.java +++ b/src/main/java/com/phenoml/api/resources/implementationguides/implementationguides/RawImplementationGuidesClient.java @@ -13,14 +13,17 @@ import com.phenoml.api.core.RequestOptions; import com.phenoml.api.core.RetryInterceptor; import com.phenoml.api.resources.implementationguides.errors.BadRequestError; +import com.phenoml.api.resources.implementationguides.errors.ConflictError; import com.phenoml.api.resources.implementationguides.errors.ForbiddenError; import com.phenoml.api.resources.implementationguides.errors.InternalServerError; import com.phenoml.api.resources.implementationguides.errors.NotFoundError; import com.phenoml.api.resources.implementationguides.errors.UnauthorizedError; +import com.phenoml.api.resources.implementationguides.implementationguides.requests.CreateCanonicalImplementationGuideRequest; import com.phenoml.api.resources.implementationguides.implementationguides.requests.UpdateImplementationGuideRequest; import com.phenoml.api.resources.implementationguides.types.ImplementationGuideDetail; import com.phenoml.api.resources.implementationguides.types.ImplementationGuideListResponse; import com.phenoml.api.resources.implementationguides.types.ImplementationGuideSummary; +import com.phenoml.api.resources.implementationguides.types.ImplementationGuideVersionDetail; import java.io.IOException; import okhttp3.Headers; import okhttp3.HttpUrl; @@ -304,22 +307,18 @@ public PhenomlClientHttpResponse update( } /** - * Deletes the stored metadata for an implementation guide — its - * profile_context and timestamps. Member profiles keep their - * implementation_guide assignment, so a guide still referenced by at least - * one profile continues to appear in listings, just without context or - * timestamps. + * Deletes the stored name-level metadata and any exact canonical package + * versions beneath the guide. Legacy member profile assignments are not + * changed. */ public PhenomlClientHttpResponse delete(String name) { return delete(name, null); } /** - * Deletes the stored metadata for an implementation guide — its - * profile_context and timestamps. Member profiles keep their - * implementation_guide assignment, so a guide still referenced by at least - * one profile continues to appear in listings, just without context or - * timestamps. + * Deletes the stored name-level metadata and any exact canonical package + * versions beneath the guide. Legacy member profile assignments are not + * changed. */ public PhenomlClientHttpResponse delete(String name, RequestOptions requestOptions) { HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) @@ -386,4 +385,157 @@ public PhenomlClientHttpResponse delete(String name, RequestOptions reques throw new PhenomlClientException("Network error executing HTTP request", e); } } + + /** + * Publishes an exact package beneath this guide family. PR 2 temporarily + * permits one exact package version per guide family; publishing another + * version returns 409 Conflict until multi-version package support lands. + */ + public PhenomlClientHttpResponse createVersion( + String name, CreateCanonicalImplementationGuideRequest request) { + return createVersion(name, request, null); + } + + /** + * Publishes an exact package beneath this guide family. PR 2 temporarily + * permits one exact package version per guide family; publishing another + * version returns 409 Conflict until multi-version package support lands. + */ + public PhenomlClientHttpResponse createVersion( + String name, CreateCanonicalImplementationGuideRequest request, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("fhir/implementation-guides") + .addPathSegment(name) + .addPathSegments("versions"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, ImplementationGuideVersionDetail.class), + response); + } + try { + switch (response.code()) { + case 400: + throw new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 404: + throw new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 409: + throw new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new PhenomlClientException("Network error executing HTTP request", e); + } + } + + public PhenomlClientHttpResponse getVersion(String name, String version) { + return getVersion(name, version, null); + } + + public PhenomlClientHttpResponse getVersion( + String name, String version, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("fhir/implementation-guides") + .addPathSegment(name) + .addPathSegments("versions") + .addPathSegment(version); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("GET", null) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, ImplementationGuideVersionDetail.class), + response); + } + try { + switch (response.code()) { + case 400: + throw new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 404: + throw new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new PhenomlClientException("Network error executing HTTP request", e); + } + } } diff --git a/src/main/java/com/phenoml/api/resources/implementationguides/implementationguides/requests/CreateCanonicalImplementationGuideRequest.java b/src/main/java/com/phenoml/api/resources/implementationguides/implementationguides/requests/CreateCanonicalImplementationGuideRequest.java new file mode 100644 index 00000000..2a516fbd --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/implementationguides/implementationguides/requests/CreateCanonicalImplementationGuideRequest.java @@ -0,0 +1,231 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.implementationguides.implementationguides.requests; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.phenoml.api.core.ObjectMappers; +import com.phenoml.api.resources.implementationguides.types.FhirImplementationGuide; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import org.jetbrains.annotations.NotNull; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = CreateCanonicalImplementationGuideRequest.Builder.class) +public final class CreateCanonicalImplementationGuideRequest { + private final FhirImplementationGuide implementationGuide; + + private final List profileRefs; + + private final Optional profileContext; + + private final Map additionalProperties; + + private CreateCanonicalImplementationGuideRequest( + FhirImplementationGuide implementationGuide, + List profileRefs, + Optional profileContext, + Map additionalProperties) { + this.implementationGuide = implementationGuide; + this.profileRefs = profileRefs; + this.profileContext = profileContext; + this.additionalProperties = additionalProperties; + } + + @JsonProperty("implementation_guide") + public FhirImplementationGuide getImplementationGuide() { + return implementationGuide; + } + + /** + * @return Exact canonical url|version references to builtin or custom profiles. A package can contain at most 250 references. + */ + @JsonProperty("profile_refs") + public List getProfileRefs() { + return profileRefs; + } + + /** + * @return Natural-language profile-selection context for this package. + */ + @JsonProperty("profile_context") + public Optional getProfileContext() { + return profileContext; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof CreateCanonicalImplementationGuideRequest + && equalTo((CreateCanonicalImplementationGuideRequest) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(CreateCanonicalImplementationGuideRequest other) { + return implementationGuide.equals(other.implementationGuide) + && profileRefs.equals(other.profileRefs) + && profileContext.equals(other.profileContext); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.implementationGuide, this.profileRefs, this.profileContext); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static ImplementationGuideStage builder() { + return new Builder(); + } + + public interface ImplementationGuideStage { + _FinalStage implementationGuide(@NotNull FhirImplementationGuide implementationGuide); + + Builder from(CreateCanonicalImplementationGuideRequest other); + } + + public interface _FinalStage { + CreateCanonicalImplementationGuideRequest build(); + + _FinalStage additionalProperty(String key, Object value); + + _FinalStage additionalProperties(Map additionalProperties); + + /** + *

Exact canonical url|version references to builtin or custom profiles. A package can contain at most 250 references.

+ */ + _FinalStage profileRefs(List profileRefs); + + _FinalStage addProfileRefs(String profileRefs); + + _FinalStage addAllProfileRefs(List profileRefs); + + /** + *

Natural-language profile-selection context for this package.

+ */ + _FinalStage profileContext(Optional profileContext); + + _FinalStage profileContext(String profileContext); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder implements ImplementationGuideStage, _FinalStage { + private FhirImplementationGuide implementationGuide; + + private Optional profileContext = Optional.empty(); + + private List profileRefs = new ArrayList<>(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + @java.lang.Override + public Builder from(CreateCanonicalImplementationGuideRequest other) { + implementationGuide(other.getImplementationGuide()); + profileRefs(other.getProfileRefs()); + profileContext(other.getProfileContext()); + return this; + } + + @java.lang.Override + @JsonSetter("implementation_guide") + public _FinalStage implementationGuide(@NotNull FhirImplementationGuide implementationGuide) { + this.implementationGuide = + Objects.requireNonNull(implementationGuide, "implementationGuide must not be null"); + return this; + } + + /** + *

Natural-language profile-selection context for this package.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage profileContext(String profileContext) { + this.profileContext = Optional.ofNullable(profileContext); + return this; + } + + /** + *

Natural-language profile-selection context for this package.

+ */ + @java.lang.Override + @JsonSetter(value = "profile_context", nulls = Nulls.SKIP) + public _FinalStage profileContext(Optional profileContext) { + this.profileContext = profileContext; + return this; + } + + /** + *

Exact canonical url|version references to builtin or custom profiles. A package can contain at most 250 references.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage addAllProfileRefs(List profileRefs) { + if (profileRefs != null) { + this.profileRefs.addAll(profileRefs); + } + return this; + } + + /** + *

Exact canonical url|version references to builtin or custom profiles. A package can contain at most 250 references.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage addProfileRefs(String profileRefs) { + this.profileRefs.add(profileRefs); + return this; + } + + /** + *

Exact canonical url|version references to builtin or custom profiles. A package can contain at most 250 references.

+ */ + @java.lang.Override + @JsonSetter(value = "profile_refs", nulls = Nulls.SKIP) + public _FinalStage profileRefs(List profileRefs) { + this.profileRefs.clear(); + if (profileRefs != null) { + this.profileRefs.addAll(profileRefs); + } + return this; + } + + @java.lang.Override + public CreateCanonicalImplementationGuideRequest build() { + return new CreateCanonicalImplementationGuideRequest( + implementationGuide, profileRefs, profileContext, additionalProperties); + } + + @java.lang.Override + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + @java.lang.Override + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/src/main/java/com/phenoml/api/resources/implementationguides/types/FhirImplementationGuide.java b/src/main/java/com/phenoml/api/resources/implementationguides/types/FhirImplementationGuide.java new file mode 100644 index 00000000..d2da8a87 --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/implementationguides/types/FhirImplementationGuide.java @@ -0,0 +1,303 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.implementationguides.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.phenoml.api.core.ObjectMappers; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import org.jetbrains.annotations.NotNull; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = FhirImplementationGuide.Builder.class) +public final class FhirImplementationGuide { + private final Optional id; + + private final String url; + + private final String version; + + private final Optional name; + + private final Optional status; + + private final Optional packageId; + + private final Optional> fhirVersion; + + private final Map additionalProperties; + + private FhirImplementationGuide( + Optional id, + String url, + String version, + Optional name, + Optional status, + Optional packageId, + Optional> fhirVersion, + Map additionalProperties) { + this.id = id; + this.url = url; + this.version = version; + this.name = name; + this.status = status; + this.packageId = packageId; + this.fhirVersion = fhirVersion; + this.additionalProperties = additionalProperties; + } + + @JsonProperty("resourceType") + public String getResourceType() { + return "ImplementationGuide"; + } + + @JsonProperty("id") + public Optional getId() { + return id; + } + + @JsonProperty("url") + public String getUrl() { + return url; + } + + @JsonProperty("version") + public String getVersion() { + return version; + } + + @JsonProperty("name") + public Optional getName() { + return name; + } + + @JsonProperty("status") + public Optional getStatus() { + return status; + } + + @JsonProperty("packageId") + public Optional getPackageId() { + return packageId; + } + + @JsonProperty("fhirVersion") + public Optional> getFhirVersion() { + return fhirVersion; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof FhirImplementationGuide && equalTo((FhirImplementationGuide) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(FhirImplementationGuide other) { + return id.equals(other.id) + && url.equals(other.url) + && version.equals(other.version) + && name.equals(other.name) + && status.equals(other.status) + && packageId.equals(other.packageId) + && fhirVersion.equals(other.fhirVersion); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.id, this.url, this.version, this.name, this.status, this.packageId, this.fhirVersion); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static UrlStage builder() { + return new Builder(); + } + + public interface UrlStage { + VersionStage url(@NotNull String url); + + Builder from(FhirImplementationGuide other); + } + + public interface VersionStage { + _FinalStage version(@NotNull String version); + } + + public interface _FinalStage { + FhirImplementationGuide build(); + + _FinalStage additionalProperty(String key, Object value); + + _FinalStage additionalProperties(Map additionalProperties); + + _FinalStage id(Optional id); + + _FinalStage id(String id); + + _FinalStage name(Optional name); + + _FinalStage name(String name); + + _FinalStage status(Optional status); + + _FinalStage status(String status); + + _FinalStage packageId(Optional packageId); + + _FinalStage packageId(String packageId); + + _FinalStage fhirVersion(Optional> fhirVersion); + + _FinalStage fhirVersion(List fhirVersion); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder implements UrlStage, VersionStage, _FinalStage { + private String url; + + private String version; + + private Optional> fhirVersion = Optional.empty(); + + private Optional packageId = Optional.empty(); + + private Optional status = Optional.empty(); + + private Optional name = Optional.empty(); + + private Optional id = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + @java.lang.Override + public Builder from(FhirImplementationGuide other) { + id(other.getId()); + url(other.getUrl()); + version(other.getVersion()); + name(other.getName()); + status(other.getStatus()); + packageId(other.getPackageId()); + fhirVersion(other.getFhirVersion()); + return this; + } + + @java.lang.Override + @JsonSetter("url") + public VersionStage url(@NotNull String url) { + this.url = Objects.requireNonNull(url, "url must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("version") + public _FinalStage version(@NotNull String version) { + this.version = Objects.requireNonNull(version, "version must not be null"); + return this; + } + + @java.lang.Override + public _FinalStage fhirVersion(List fhirVersion) { + this.fhirVersion = Optional.ofNullable(fhirVersion); + return this; + } + + @java.lang.Override + @JsonSetter(value = "fhirVersion", nulls = Nulls.SKIP) + public _FinalStage fhirVersion(Optional> fhirVersion) { + this.fhirVersion = fhirVersion; + return this; + } + + @java.lang.Override + public _FinalStage packageId(String packageId) { + this.packageId = Optional.ofNullable(packageId); + return this; + } + + @java.lang.Override + @JsonSetter(value = "packageId", nulls = Nulls.SKIP) + public _FinalStage packageId(Optional packageId) { + this.packageId = packageId; + return this; + } + + @java.lang.Override + public _FinalStage status(String status) { + this.status = Optional.ofNullable(status); + return this; + } + + @java.lang.Override + @JsonSetter(value = "status", nulls = Nulls.SKIP) + public _FinalStage status(Optional status) { + this.status = status; + return this; + } + + @java.lang.Override + public _FinalStage name(String name) { + this.name = Optional.ofNullable(name); + return this; + } + + @java.lang.Override + @JsonSetter(value = "name", nulls = Nulls.SKIP) + public _FinalStage name(Optional name) { + this.name = name; + return this; + } + + @java.lang.Override + public _FinalStage id(String id) { + this.id = Optional.ofNullable(id); + return this; + } + + @java.lang.Override + @JsonSetter(value = "id", nulls = Nulls.SKIP) + public _FinalStage id(Optional id) { + this.id = id; + return this; + } + + @java.lang.Override + public FhirImplementationGuide build() { + return new FhirImplementationGuide( + id, url, version, name, status, packageId, fhirVersion, additionalProperties); + } + + @java.lang.Override + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + @java.lang.Override + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/src/main/java/com/phenoml/api/resources/implementationguides/types/IImplementationGuideSummary.java b/src/main/java/com/phenoml/api/resources/implementationguides/types/IImplementationGuideSummary.java index 54b5f75e..2e0353d1 100644 --- a/src/main/java/com/phenoml/api/resources/implementationguides/types/IImplementationGuideSummary.java +++ b/src/main/java/com/phenoml/api/resources/implementationguides/types/IImplementationGuideSummary.java @@ -13,6 +13,10 @@ public interface IImplementationGuideSummary { Optional getProfileCount(); + Optional getCanonicalUrl(); + + Optional getVersionCount(); + Optional getCreatedAt(); Optional getUpdatedAt(); diff --git a/src/main/java/com/phenoml/api/resources/implementationguides/types/ImplementationGuideDetail.java b/src/main/java/com/phenoml/api/resources/implementationguides/types/ImplementationGuideDetail.java index b5f00b12..1dccf8cf 100644 --- a/src/main/java/com/phenoml/api/resources/implementationguides/types/ImplementationGuideDetail.java +++ b/src/main/java/com/phenoml/api/resources/implementationguides/types/ImplementationGuideDetail.java @@ -28,6 +28,10 @@ public final class ImplementationGuideDetail implements IImplementationGuideSumm private final Optional profileCount; + private final Optional canonicalUrl; + + private final Optional versionCount; + private final Optional createdAt; private final Optional updatedAt; @@ -40,6 +44,8 @@ private ImplementationGuideDetail( Optional name, Optional profileContext, Optional profileCount, + Optional canonicalUrl, + Optional versionCount, Optional createdAt, Optional updatedAt, Optional> profiles, @@ -47,6 +53,8 @@ private ImplementationGuideDetail( this.name = name; this.profileContext = profileContext; this.profileCount = profileCount; + this.canonicalUrl = canonicalUrl; + this.versionCount = versionCount; this.createdAt = createdAt; this.updatedAt = updatedAt; this.profiles = profiles; @@ -80,6 +88,24 @@ public Optional getProfileCount() { return profileCount; } + /** + * @return Canonical FHIR ImplementationGuide URL, when the family has an exact package. + */ + @JsonProperty("canonical_url") + @java.lang.Override + public Optional getCanonicalUrl() { + return canonicalUrl; + } + + /** + * @return Number of retained exact package versions. + */ + @JsonProperty("version_count") + @java.lang.Override + public Optional getVersionCount() { + return versionCount; + } + /** * @return Present only for guides that have stored metadata (a profile_context has been set). Omitted for guides that exist solely because a profile references them. */ @@ -121,6 +147,8 @@ private boolean equalTo(ImplementationGuideDetail other) { return name.equals(other.name) && profileContext.equals(other.profileContext) && profileCount.equals(other.profileCount) + && canonicalUrl.equals(other.canonicalUrl) + && versionCount.equals(other.versionCount) && createdAt.equals(other.createdAt) && updatedAt.equals(other.updatedAt) && profiles.equals(other.profiles); @@ -129,7 +157,14 @@ private boolean equalTo(ImplementationGuideDetail other) { @java.lang.Override public int hashCode() { return Objects.hash( - this.name, this.profileContext, this.profileCount, this.createdAt, this.updatedAt, this.profiles); + this.name, + this.profileContext, + this.profileCount, + this.canonicalUrl, + this.versionCount, + this.createdAt, + this.updatedAt, + this.profiles); } @java.lang.Override @@ -149,6 +184,10 @@ public static final class Builder { private Optional profileCount = Optional.empty(); + private Optional canonicalUrl = Optional.empty(); + + private Optional versionCount = Optional.empty(); + private Optional createdAt = Optional.empty(); private Optional updatedAt = Optional.empty(); @@ -164,6 +203,8 @@ public Builder from(ImplementationGuideDetail other) { name(other.getName()); profileContext(other.getProfileContext()); profileCount(other.getProfileCount()); + canonicalUrl(other.getCanonicalUrl()); + versionCount(other.getVersionCount()); createdAt(other.getCreatedAt()); updatedAt(other.getUpdatedAt()); profiles(other.getProfiles()); @@ -212,6 +253,34 @@ public Builder profileCount(Integer profileCount) { return this; } + /** + *

Canonical FHIR ImplementationGuide URL, when the family has an exact package.

+ */ + @JsonSetter(value = "canonical_url", nulls = Nulls.SKIP) + public Builder canonicalUrl(Optional canonicalUrl) { + this.canonicalUrl = canonicalUrl; + return this; + } + + public Builder canonicalUrl(String canonicalUrl) { + this.canonicalUrl = Optional.ofNullable(canonicalUrl); + return this; + } + + /** + *

Number of retained exact package versions.

+ */ + @JsonSetter(value = "version_count", nulls = Nulls.SKIP) + public Builder versionCount(Optional versionCount) { + this.versionCount = versionCount; + return this; + } + + public Builder versionCount(Integer versionCount) { + this.versionCount = Optional.ofNullable(versionCount); + return this; + } + /** *

Present only for guides that have stored metadata (a profile_context has been set). Omitted for guides that exist solely because a profile references them.

*/ @@ -256,7 +325,15 @@ public Builder profiles(List profiles) { public ImplementationGuideDetail build() { return new ImplementationGuideDetail( - name, profileContext, profileCount, createdAt, updatedAt, profiles, additionalProperties); + name, + profileContext, + profileCount, + canonicalUrl, + versionCount, + createdAt, + updatedAt, + profiles, + additionalProperties); } public Builder additionalProperty(String key, Object value) { diff --git a/src/main/java/com/phenoml/api/resources/implementationguides/types/ImplementationGuideSummary.java b/src/main/java/com/phenoml/api/resources/implementationguides/types/ImplementationGuideSummary.java index 33197052..38652ac1 100644 --- a/src/main/java/com/phenoml/api/resources/implementationguides/types/ImplementationGuideSummary.java +++ b/src/main/java/com/phenoml/api/resources/implementationguides/types/ImplementationGuideSummary.java @@ -27,6 +27,10 @@ public final class ImplementationGuideSummary implements IImplementationGuideSum private final Optional profileCount; + private final Optional canonicalUrl; + + private final Optional versionCount; + private final Optional createdAt; private final Optional updatedAt; @@ -37,12 +41,16 @@ private ImplementationGuideSummary( Optional name, Optional profileContext, Optional profileCount, + Optional canonicalUrl, + Optional versionCount, Optional createdAt, Optional updatedAt, Map additionalProperties) { this.name = name; this.profileContext = profileContext; this.profileCount = profileCount; + this.canonicalUrl = canonicalUrl; + this.versionCount = versionCount; this.createdAt = createdAt; this.updatedAt = updatedAt; this.additionalProperties = additionalProperties; @@ -75,6 +83,24 @@ public Optional getProfileCount() { return profileCount; } + /** + * @return Canonical FHIR ImplementationGuide URL, when the family has an exact package. + */ + @JsonProperty("canonical_url") + @java.lang.Override + public Optional getCanonicalUrl() { + return canonicalUrl; + } + + /** + * @return Number of retained exact package versions. + */ + @JsonProperty("version_count") + @java.lang.Override + public Optional getVersionCount() { + return versionCount; + } + /** * @return Present only for guides that have stored metadata (a profile_context has been set). Omitted for guides that exist solely because a profile references them. */ @@ -108,13 +134,22 @@ private boolean equalTo(ImplementationGuideSummary other) { return name.equals(other.name) && profileContext.equals(other.profileContext) && profileCount.equals(other.profileCount) + && canonicalUrl.equals(other.canonicalUrl) + && versionCount.equals(other.versionCount) && createdAt.equals(other.createdAt) && updatedAt.equals(other.updatedAt); } @java.lang.Override public int hashCode() { - return Objects.hash(this.name, this.profileContext, this.profileCount, this.createdAt, this.updatedAt); + return Objects.hash( + this.name, + this.profileContext, + this.profileCount, + this.canonicalUrl, + this.versionCount, + this.createdAt, + this.updatedAt); } @java.lang.Override @@ -134,6 +169,10 @@ public static final class Builder { private Optional profileCount = Optional.empty(); + private Optional canonicalUrl = Optional.empty(); + + private Optional versionCount = Optional.empty(); + private Optional createdAt = Optional.empty(); private Optional updatedAt = Optional.empty(); @@ -147,6 +186,8 @@ public Builder from(ImplementationGuideSummary other) { name(other.getName()); profileContext(other.getProfileContext()); profileCount(other.getProfileCount()); + canonicalUrl(other.getCanonicalUrl()); + versionCount(other.getVersionCount()); createdAt(other.getCreatedAt()); updatedAt(other.getUpdatedAt()); return this; @@ -194,6 +235,34 @@ public Builder profileCount(Integer profileCount) { return this; } + /** + *

Canonical FHIR ImplementationGuide URL, when the family has an exact package.

+ */ + @JsonSetter(value = "canonical_url", nulls = Nulls.SKIP) + public Builder canonicalUrl(Optional canonicalUrl) { + this.canonicalUrl = canonicalUrl; + return this; + } + + public Builder canonicalUrl(String canonicalUrl) { + this.canonicalUrl = Optional.ofNullable(canonicalUrl); + return this; + } + + /** + *

Number of retained exact package versions.

+ */ + @JsonSetter(value = "version_count", nulls = Nulls.SKIP) + public Builder versionCount(Optional versionCount) { + this.versionCount = versionCount; + return this; + } + + public Builder versionCount(Integer versionCount) { + this.versionCount = Optional.ofNullable(versionCount); + return this; + } + /** *

Present only for guides that have stored metadata (a profile_context has been set). Omitted for guides that exist solely because a profile references them.

*/ @@ -224,7 +293,14 @@ public Builder updatedAt(OffsetDateTime updatedAt) { public ImplementationGuideSummary build() { return new ImplementationGuideSummary( - name, profileContext, profileCount, createdAt, updatedAt, additionalProperties); + name, + profileContext, + profileCount, + canonicalUrl, + versionCount, + createdAt, + updatedAt, + additionalProperties); } public Builder additionalProperty(String key, Object value) { diff --git a/src/main/java/com/phenoml/api/resources/implementationguides/types/ImplementationGuideVersionDetail.java b/src/main/java/com/phenoml/api/resources/implementationguides/types/ImplementationGuideVersionDetail.java new file mode 100644 index 00000000..28c37db5 --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/implementationguides/types/ImplementationGuideVersionDetail.java @@ -0,0 +1,382 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.implementationguides.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.phenoml.api.core.ObjectMappers; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = ImplementationGuideVersionDetail.Builder.class) +public final class ImplementationGuideVersionDetail { + private final String name; + + private final String url; + + private final String version; + + private final String profileContext; + + private final List profiles; + + private final List profileRefs; + + private final FhirImplementationGuide implementationGuide; + + private final OffsetDateTime createdAt; + + private final OffsetDateTime updatedAt; + + private final Map additionalProperties; + + private ImplementationGuideVersionDetail( + String name, + String url, + String version, + String profileContext, + List profiles, + List profileRefs, + FhirImplementationGuide implementationGuide, + OffsetDateTime createdAt, + OffsetDateTime updatedAt, + Map additionalProperties) { + this.name = name; + this.url = url; + this.version = version; + this.profileContext = profileContext; + this.profiles = profiles; + this.profileRefs = profileRefs; + this.implementationGuide = implementationGuide; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + this.additionalProperties = additionalProperties; + } + + @JsonProperty("name") + public String getName() { + return name; + } + + @JsonProperty("url") + public String getUrl() { + return url; + } + + @JsonProperty("version") + public String getVersion() { + return version; + } + + @JsonProperty("profile_context") + public String getProfileContext() { + return profileContext; + } + + @JsonProperty("profiles") + public List getProfiles() { + return profiles; + } + + @JsonProperty("profile_refs") + public List getProfileRefs() { + return profileRefs; + } + + @JsonProperty("implementation_guide") + public FhirImplementationGuide getImplementationGuide() { + return implementationGuide; + } + + @JsonProperty("created_at") + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + @JsonProperty("updated_at") + public OffsetDateTime getUpdatedAt() { + return updatedAt; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof ImplementationGuideVersionDetail && equalTo((ImplementationGuideVersionDetail) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(ImplementationGuideVersionDetail other) { + return name.equals(other.name) + && url.equals(other.url) + && version.equals(other.version) + && profileContext.equals(other.profileContext) + && profiles.equals(other.profiles) + && profileRefs.equals(other.profileRefs) + && implementationGuide.equals(other.implementationGuide) + && createdAt.equals(other.createdAt) + && updatedAt.equals(other.updatedAt); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash( + this.name, + this.url, + this.version, + this.profileContext, + this.profiles, + this.profileRefs, + this.implementationGuide, + this.createdAt, + this.updatedAt); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static NameStage builder() { + return new Builder(); + } + + public interface NameStage { + UrlStage name(@NotNull String name); + + Builder from(ImplementationGuideVersionDetail other); + } + + public interface UrlStage { + VersionStage url(@NotNull String url); + } + + public interface VersionStage { + ProfileContextStage version(@NotNull String version); + } + + public interface ProfileContextStage { + ImplementationGuideStage profileContext(@NotNull String profileContext); + } + + public interface ImplementationGuideStage { + CreatedAtStage implementationGuide(@NotNull FhirImplementationGuide implementationGuide); + } + + public interface CreatedAtStage { + UpdatedAtStage createdAt(@NotNull OffsetDateTime createdAt); + } + + public interface UpdatedAtStage { + _FinalStage updatedAt(@NotNull OffsetDateTime updatedAt); + } + + public interface _FinalStage { + ImplementationGuideVersionDetail build(); + + _FinalStage additionalProperty(String key, Object value); + + _FinalStage additionalProperties(Map additionalProperties); + + _FinalStage profiles(List profiles); + + _FinalStage addProfiles(String profiles); + + _FinalStage addAllProfiles(List profiles); + + _FinalStage profileRefs(List profileRefs); + + _FinalStage addProfileRefs(String profileRefs); + + _FinalStage addAllProfileRefs(List profileRefs); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder + implements NameStage, + UrlStage, + VersionStage, + ProfileContextStage, + ImplementationGuideStage, + CreatedAtStage, + UpdatedAtStage, + _FinalStage { + private String name; + + private String url; + + private String version; + + private String profileContext; + + private FhirImplementationGuide implementationGuide; + + private OffsetDateTime createdAt; + + private OffsetDateTime updatedAt; + + private List profileRefs = new ArrayList<>(); + + private List profiles = new ArrayList<>(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + @java.lang.Override + public Builder from(ImplementationGuideVersionDetail other) { + name(other.getName()); + url(other.getUrl()); + version(other.getVersion()); + profileContext(other.getProfileContext()); + profiles(other.getProfiles()); + profileRefs(other.getProfileRefs()); + implementationGuide(other.getImplementationGuide()); + createdAt(other.getCreatedAt()); + updatedAt(other.getUpdatedAt()); + return this; + } + + @java.lang.Override + @JsonSetter("name") + public UrlStage name(@NotNull String name) { + this.name = Objects.requireNonNull(name, "name must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("url") + public VersionStage url(@NotNull String url) { + this.url = Objects.requireNonNull(url, "url must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("version") + public ProfileContextStage version(@NotNull String version) { + this.version = Objects.requireNonNull(version, "version must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("profile_context") + public ImplementationGuideStage profileContext(@NotNull String profileContext) { + this.profileContext = Objects.requireNonNull(profileContext, "profileContext must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("implementation_guide") + public CreatedAtStage implementationGuide(@NotNull FhirImplementationGuide implementationGuide) { + this.implementationGuide = + Objects.requireNonNull(implementationGuide, "implementationGuide must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("created_at") + public UpdatedAtStage createdAt(@NotNull OffsetDateTime createdAt) { + this.createdAt = Objects.requireNonNull(createdAt, "createdAt must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("updated_at") + public _FinalStage updatedAt(@NotNull OffsetDateTime updatedAt) { + this.updatedAt = Objects.requireNonNull(updatedAt, "updatedAt must not be null"); + return this; + } + + @java.lang.Override + public _FinalStage addAllProfileRefs(List profileRefs) { + if (profileRefs != null) { + this.profileRefs.addAll(profileRefs); + } + return this; + } + + @java.lang.Override + public _FinalStage addProfileRefs(String profileRefs) { + this.profileRefs.add(profileRefs); + return this; + } + + @java.lang.Override + @JsonSetter(value = "profile_refs", nulls = Nulls.SKIP) + public _FinalStage profileRefs(List profileRefs) { + this.profileRefs.clear(); + if (profileRefs != null) { + this.profileRefs.addAll(profileRefs); + } + return this; + } + + @java.lang.Override + public _FinalStage addAllProfiles(List profiles) { + if (profiles != null) { + this.profiles.addAll(profiles); + } + return this; + } + + @java.lang.Override + public _FinalStage addProfiles(String profiles) { + this.profiles.add(profiles); + return this; + } + + @java.lang.Override + @JsonSetter(value = "profiles", nulls = Nulls.SKIP) + public _FinalStage profiles(List profiles) { + this.profiles.clear(); + if (profiles != null) { + this.profiles.addAll(profiles); + } + return this; + } + + @java.lang.Override + public ImplementationGuideVersionDetail build() { + return new ImplementationGuideVersionDetail( + name, + url, + version, + profileContext, + profiles, + profileRefs, + implementationGuide, + createdAt, + updatedAt, + additionalProperties); + } + + @java.lang.Override + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + @java.lang.Override + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhir/AsyncLang2FhirClient.java b/src/main/java/com/phenoml/api/resources/lang2fhir/AsyncLang2FhirClient.java index 0b302210..cdc2b081 100644 --- a/src/main/java/com/phenoml/api/resources/lang2fhir/AsyncLang2FhirClient.java +++ b/src/main/java/com/phenoml/api/resources/lang2fhir/AsyncLang2FhirClient.java @@ -142,7 +142,7 @@ public CompletableFuture uploadProfile( } /** - * Extracts text from a document (PDF or image) and converts it into a structured FHIR resource. + * Extracts text from a PDF, image, RTF, or XML/C-CDA document and converts it into a structured FHIR resource. *

Patient identifier handling. When generating a patient (or patient-canvas) resource, US Core requires Patient.identifier (a business identifier such as an MRN). When the source text contains an identifier, it is extracted with an appropriate URI system. When the source text does not contain a detectable identifier, a synthetic one is generated with system: "urn:phenoml:lang2fhir-generated-id" and a UUID value so the resource remains FHIR-valid and US Core conformant. Callers who need a tenant-specific namespace should rewrite the synthetic system after extraction.

*/ public CompletableFuture> document(DocumentRequest request) { @@ -150,7 +150,7 @@ public CompletableFuture> document(DocumentRequest request) } /** - * Extracts text from a document (PDF or image) and converts it into a structured FHIR resource. + * Extracts text from a PDF, image, RTF, or XML/C-CDA document and converts it into a structured FHIR resource. *

Patient identifier handling. When generating a patient (or patient-canvas) resource, US Core requires Patient.identifier (a business identifier such as an MRN). When the source text contains an identifier, it is extracted with an appropriate URI system. When the source text does not contain a detectable identifier, a synthetic one is generated with system: "urn:phenoml:lang2fhir-generated-id" and a UUID value so the resource remains FHIR-valid and US Core conformant. Callers who need a tenant-specific namespace should rewrite the synthetic system after extraction.

*/ public CompletableFuture> document(DocumentRequest request, RequestOptions requestOptions) { @@ -158,7 +158,7 @@ public CompletableFuture> document(DocumentRequest request, } /** - * Extracts text from a document (PDF or image) and converts it into multiple FHIR resources, + * Extracts text from a PDF, image, RTF, or XML/C-CDA document and converts it into multiple FHIR resources, * returned as a transaction Bundle. Combines document text extraction with multi-resource detection. * Automatically detects Patient, Condition, MedicationRequest, Observation, and other resource types. * Resources are linked with proper references (e.g., Conditions reference the Patient). @@ -170,7 +170,7 @@ public CompletableFuture documentMulti(DocumentMultiReque } /** - * Extracts text from a document (PDF or image) and converts it into multiple FHIR resources, + * Extracts text from a PDF, image, RTF, or XML/C-CDA document and converts it into multiple FHIR resources, * returned as a transaction Bundle. Combines document text extraction with multi-resource detection. * Automatically detects Patient, Condition, MedicationRequest, Observation, and other resource types. * Resources are linked with proper references (e.g., Conditions reference the Patient). diff --git a/src/main/java/com/phenoml/api/resources/lang2fhir/AsyncRawLang2FhirClient.java b/src/main/java/com/phenoml/api/resources/lang2fhir/AsyncRawLang2FhirClient.java index 8b409dec..22c38f2a 100644 --- a/src/main/java/com/phenoml/api/resources/lang2fhir/AsyncRawLang2FhirClient.java +++ b/src/main/java/com/phenoml/api/resources/lang2fhir/AsyncRawLang2FhirClient.java @@ -528,7 +528,7 @@ public void onFailure(@NotNull Call call, @NotNull IOException e) { } /** - * Extracts text from a document (PDF or image) and converts it into a structured FHIR resource. + * Extracts text from a PDF, image, RTF, or XML/C-CDA document and converts it into a structured FHIR resource. *

Patient identifier handling. When generating a patient (or patient-canvas) resource, US Core requires Patient.identifier (a business identifier such as an MRN). When the source text contains an identifier, it is extracted with an appropriate URI system. When the source text does not contain a detectable identifier, a synthetic one is generated with system: "urn:phenoml:lang2fhir-generated-id" and a UUID value so the resource remains FHIR-valid and US Core conformant. Callers who need a tenant-specific namespace should rewrite the synthetic system after extraction.

*/ public CompletableFuture>> document(DocumentRequest request) { @@ -536,7 +536,7 @@ public CompletableFuture>> documen } /** - * Extracts text from a document (PDF or image) and converts it into a structured FHIR resource. + * Extracts text from a PDF, image, RTF, or XML/C-CDA document and converts it into a structured FHIR resource. *

Patient identifier handling. When generating a patient (or patient-canvas) resource, US Core requires Patient.identifier (a business identifier such as an MRN). When the source text contains an identifier, it is extracted with an appropriate URI system. When the source text does not contain a detectable identifier, a synthetic one is generated with system: "urn:phenoml:lang2fhir-generated-id" and a UUID value so the resource remains FHIR-valid and US Core conformant. Callers who need a tenant-specific namespace should rewrite the synthetic system after extraction.

*/ public CompletableFuture>> document( @@ -601,6 +601,11 @@ public void onResponse(@NotNull Call call, @NotNull Response response) throws IO ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response)); return; + case 403: + future.completeExceptionally(new ForbiddenError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; case 404: future.completeExceptionally(new NotFoundError( ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), @@ -651,7 +656,7 @@ public void onFailure(@NotNull Call call, @NotNull IOException e) { } /** - * Extracts text from a document (PDF or image) and converts it into multiple FHIR resources, + * Extracts text from a PDF, image, RTF, or XML/C-CDA document and converts it into multiple FHIR resources, * returned as a transaction Bundle. Combines document text extraction with multi-resource detection. * Automatically detects Patient, Condition, MedicationRequest, Observation, and other resource types. * Resources are linked with proper references (e.g., Conditions reference the Patient). @@ -664,7 +669,7 @@ public CompletableFuture> docum } /** - * Extracts text from a document (PDF or image) and converts it into multiple FHIR resources, + * Extracts text from a PDF, image, RTF, or XML/C-CDA document and converts it into multiple FHIR resources, * returned as a transaction Bundle. Combines document text extraction with multi-resource detection. * Automatically detects Patient, Condition, MedicationRequest, Observation, and other resource types. * Resources are linked with proper references (e.g., Conditions reference the Patient). @@ -732,6 +737,11 @@ public void onResponse(@NotNull Call call, @NotNull Response response) throws IO ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response)); return; + case 403: + future.completeExceptionally(new ForbiddenError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; case 404: future.completeExceptionally(new NotFoundError( ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), diff --git a/src/main/java/com/phenoml/api/resources/lang2fhir/Lang2FhirClient.java b/src/main/java/com/phenoml/api/resources/lang2fhir/Lang2FhirClient.java index 199a3693..2a599ee2 100644 --- a/src/main/java/com/phenoml/api/resources/lang2fhir/Lang2FhirClient.java +++ b/src/main/java/com/phenoml/api/resources/lang2fhir/Lang2FhirClient.java @@ -139,7 +139,7 @@ public UploadProfileResponse uploadProfile(ProfileUploadRequest request, Request } /** - * Extracts text from a document (PDF or image) and converts it into a structured FHIR resource. + * Extracts text from a PDF, image, RTF, or XML/C-CDA document and converts it into a structured FHIR resource. *

Patient identifier handling. When generating a patient (or patient-canvas) resource, US Core requires Patient.identifier (a business identifier such as an MRN). When the source text contains an identifier, it is extracted with an appropriate URI system. When the source text does not contain a detectable identifier, a synthetic one is generated with system: "urn:phenoml:lang2fhir-generated-id" and a UUID value so the resource remains FHIR-valid and US Core conformant. Callers who need a tenant-specific namespace should rewrite the synthetic system after extraction.

*/ public Map document(DocumentRequest request) { @@ -147,7 +147,7 @@ public Map document(DocumentRequest request) { } /** - * Extracts text from a document (PDF or image) and converts it into a structured FHIR resource. + * Extracts text from a PDF, image, RTF, or XML/C-CDA document and converts it into a structured FHIR resource. *

Patient identifier handling. When generating a patient (or patient-canvas) resource, US Core requires Patient.identifier (a business identifier such as an MRN). When the source text contains an identifier, it is extracted with an appropriate URI system. When the source text does not contain a detectable identifier, a synthetic one is generated with system: "urn:phenoml:lang2fhir-generated-id" and a UUID value so the resource remains FHIR-valid and US Core conformant. Callers who need a tenant-specific namespace should rewrite the synthetic system after extraction.

*/ public Map document(DocumentRequest request, RequestOptions requestOptions) { @@ -155,7 +155,7 @@ public Map document(DocumentRequest request, RequestOptions requ } /** - * Extracts text from a document (PDF or image) and converts it into multiple FHIR resources, + * Extracts text from a PDF, image, RTF, or XML/C-CDA document and converts it into multiple FHIR resources, * returned as a transaction Bundle. Combines document text extraction with multi-resource detection. * Automatically detects Patient, Condition, MedicationRequest, Observation, and other resource types. * Resources are linked with proper references (e.g., Conditions reference the Patient). @@ -167,7 +167,7 @@ public DocumentMultiResponse documentMulti(DocumentMultiRequest request) { } /** - * Extracts text from a document (PDF or image) and converts it into multiple FHIR resources, + * Extracts text from a PDF, image, RTF, or XML/C-CDA document and converts it into multiple FHIR resources, * returned as a transaction Bundle. Combines document text extraction with multi-resource detection. * Automatically detects Patient, Condition, MedicationRequest, Observation, and other resource types. * Resources are linked with proper references (e.g., Conditions reference the Patient). diff --git a/src/main/java/com/phenoml/api/resources/lang2fhir/RawLang2FhirClient.java b/src/main/java/com/phenoml/api/resources/lang2fhir/RawLang2FhirClient.java index 0cd0d46d..f1fed8fa 100644 --- a/src/main/java/com/phenoml/api/resources/lang2fhir/RawLang2FhirClient.java +++ b/src/main/java/com/phenoml/api/resources/lang2fhir/RawLang2FhirClient.java @@ -428,7 +428,7 @@ public PhenomlClientHttpResponse uploadProfile( } /** - * Extracts text from a document (PDF or image) and converts it into a structured FHIR resource. + * Extracts text from a PDF, image, RTF, or XML/C-CDA document and converts it into a structured FHIR resource. *

Patient identifier handling. When generating a patient (or patient-canvas) resource, US Core requires Patient.identifier (a business identifier such as an MRN). When the source text contains an identifier, it is extracted with an appropriate URI system. When the source text does not contain a detectable identifier, a synthetic one is generated with system: "urn:phenoml:lang2fhir-generated-id" and a UUID value so the resource remains FHIR-valid and US Core conformant. Callers who need a tenant-specific namespace should rewrite the synthetic system after extraction.

*/ public PhenomlClientHttpResponse> document(DocumentRequest request) { @@ -436,7 +436,7 @@ public PhenomlClientHttpResponse> document(DocumentRequest r } /** - * Extracts text from a document (PDF or image) and converts it into a structured FHIR resource. + * Extracts text from a PDF, image, RTF, or XML/C-CDA document and converts it into a structured FHIR resource. *

Patient identifier handling. When generating a patient (or patient-canvas) resource, US Core requires Patient.identifier (a business identifier such as an MRN). When the source text contains an identifier, it is extracted with an appropriate URI system. When the source text does not contain a detectable identifier, a synthetic one is generated with system: "urn:phenoml:lang2fhir-generated-id" and a UUID value so the resource remains FHIR-valid and US Core conformant. Callers who need a tenant-specific namespace should rewrite the synthetic system after extraction.

*/ public PhenomlClientHttpResponse> document( @@ -493,6 +493,9 @@ public PhenomlClientHttpResponse> document( case 401: throw new UnauthorizedError( ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 403: + throw new ForbiddenError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); case 404: throw new NotFoundError( ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); @@ -523,7 +526,7 @@ public PhenomlClientHttpResponse> document( } /** - * Extracts text from a document (PDF or image) and converts it into multiple FHIR resources, + * Extracts text from a PDF, image, RTF, or XML/C-CDA document and converts it into multiple FHIR resources, * returned as a transaction Bundle. Combines document text extraction with multi-resource detection. * Automatically detects Patient, Condition, MedicationRequest, Observation, and other resource types. * Resources are linked with proper references (e.g., Conditions reference the Patient). @@ -535,7 +538,7 @@ public PhenomlClientHttpResponse documentMulti(DocumentMu } /** - * Extracts text from a document (PDF or image) and converts it into multiple FHIR resources, + * Extracts text from a PDF, image, RTF, or XML/C-CDA document and converts it into multiple FHIR resources, * returned as a transaction Bundle. Combines document text extraction with multi-resource detection. * Automatically detects Patient, Condition, MedicationRequest, Observation, and other resource types. * Resources are linked with proper references (e.g., Conditions reference the Patient). @@ -594,6 +597,9 @@ public PhenomlClientHttpResponse documentMulti( case 401: throw new UnauthorizedError( ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 403: + throw new ForbiddenError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); case 404: throw new NotFoundError( ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); diff --git a/src/main/java/com/phenoml/api/resources/lang2fhir/requests/CreateMultiRequest.java b/src/main/java/com/phenoml/api/resources/lang2fhir/requests/CreateMultiRequest.java index a9f80013..fdc9d749 100644 --- a/src/main/java/com/phenoml/api/resources/lang2fhir/requests/CreateMultiRequest.java +++ b/src/main/java/com/phenoml/api/resources/lang2fhir/requests/CreateMultiRequest.java @@ -15,6 +15,7 @@ import com.phenoml.api.resources.lang2fhir.types.CreateMultiRequestDetectionEffort; import com.phenoml.api.resources.lang2fhir.types.CreateMultiRequestValidationMethod; import com.phenoml.api.resources.lang2fhir.types.PatientReference; +import com.phenoml.api.resources.lang2fhir.types.PrimaryPatient; import com.phenoml.api.resources.lang2fhir.types.ResourceReview; import java.util.HashMap; import java.util.Map; @@ -31,6 +32,8 @@ public final class CreateMultiRequest { private final Optional provider; + private final Optional primaryPatient; + private final Optional patientReference; private final Optional implementationGuide; @@ -47,6 +50,7 @@ private CreateMultiRequest( String text, Optional version, Optional provider, + Optional primaryPatient, Optional patientReference, Optional implementationGuide, Optional detectionEffort, @@ -56,6 +60,7 @@ private CreateMultiRequest( this.text = text; this.version = version; this.provider = provider; + this.primaryPatient = primaryPatient; this.patientReference = patientReference; this.implementationGuide = implementationGuide; this.detectionEffort = detectionEffort; @@ -88,13 +93,21 @@ public Optional getProvider() { return provider; } + @JsonProperty("primary_patient") + public Optional getPrimaryPatient() { + return primaryPatient; + } + + /** + * @return Deprecated compatibility alias for primary_patient.identifier. Cannot be combined with primary_patient. + */ @JsonProperty("patient_reference") public Optional getPatientReference() { return patientReference; } /** - * @return Custom Implementation Guide name. When specified, profiles from this IG are included alongside US Core profiles during resource detection. US Core is always the base layer; custom IG profiles are additive. + * @return Custom Implementation Guide name. When specified, profiles from this IG are included alongside the default profiles during resource detection. Default profiles are always the base layer; custom IG profiles are additive. */ @JsonProperty("implementation_guide") public Optional getImplementationGuide() { @@ -137,6 +150,7 @@ private boolean equalTo(CreateMultiRequest other) { return text.equals(other.text) && version.equals(other.version) && provider.equals(other.provider) + && primaryPatient.equals(other.primaryPatient) && patientReference.equals(other.patientReference) && implementationGuide.equals(other.implementationGuide) && detectionEffort.equals(other.detectionEffort) @@ -150,6 +164,7 @@ public int hashCode() { this.text, this.version, this.provider, + this.primaryPatient, this.patientReference, this.implementationGuide, this.detectionEffort, @@ -196,12 +211,19 @@ public interface _FinalStage { _FinalStage provider(String provider); + _FinalStage primaryPatient(Optional primaryPatient); + + _FinalStage primaryPatient(PrimaryPatient primaryPatient); + + /** + *

Deprecated compatibility alias for primary_patient.identifier. Cannot be combined with primary_patient.

+ */ _FinalStage patientReference(Optional patientReference); _FinalStage patientReference(PatientReference patientReference); /** - *

Custom Implementation Guide name. When specified, profiles from this IG are included alongside US Core profiles during resource detection. US Core is always the base layer; custom IG profiles are additive.

+ *

Custom Implementation Guide name. When specified, profiles from this IG are included alongside the default profiles during resource detection. Default profiles are always the base layer; custom IG profiles are additive.

*/ _FinalStage implementationGuide(Optional implementationGuide); @@ -240,6 +262,8 @@ public static final class Builder implements TextStage, _FinalStage { private Optional patientReference = Optional.empty(); + private Optional primaryPatient = Optional.empty(); + private Optional provider = Optional.empty(); private Optional version = Optional.empty(); @@ -254,6 +278,7 @@ public Builder from(CreateMultiRequest other) { text(other.getText()); version(other.getVersion()); provider(other.getProvider()); + primaryPatient(other.getPrimaryPatient()); patientReference(other.getPatientReference()); implementationGuide(other.getImplementationGuide()); detectionEffort(other.getDetectionEffort()); @@ -327,7 +352,7 @@ public _FinalStage detectionEffort(Optional d } /** - *

Custom Implementation Guide name. When specified, profiles from this IG are included alongside US Core profiles during resource detection. US Core is always the base layer; custom IG profiles are additive.

+ *

Custom Implementation Guide name. When specified, profiles from this IG are included alongside the default profiles during resource detection. Default profiles are always the base layer; custom IG profiles are additive.

* @return Reference to {@code this} so that method calls can be chained together. */ @java.lang.Override @@ -337,7 +362,7 @@ public _FinalStage implementationGuide(String implementationGuide) { } /** - *

Custom Implementation Guide name. When specified, profiles from this IG are included alongside US Core profiles during resource detection. US Core is always the base layer; custom IG profiles are additive.

+ *

Custom Implementation Guide name. When specified, profiles from this IG are included alongside the default profiles during resource detection. Default profiles are always the base layer; custom IG profiles are additive.

*/ @java.lang.Override @JsonSetter(value = "implementation_guide", nulls = Nulls.SKIP) @@ -346,12 +371,19 @@ public _FinalStage implementationGuide(Optional implementationGuide) { return this; } + /** + *

Deprecated compatibility alias for primary_patient.identifier. Cannot be combined with primary_patient.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ @java.lang.Override public _FinalStage patientReference(PatientReference patientReference) { this.patientReference = Optional.ofNullable(patientReference); return this; } + /** + *

Deprecated compatibility alias for primary_patient.identifier. Cannot be combined with primary_patient.

+ */ @java.lang.Override @JsonSetter(value = "patient_reference", nulls = Nulls.SKIP) public _FinalStage patientReference(Optional patientReference) { @@ -359,6 +391,19 @@ public _FinalStage patientReference(Optional patientReference) return this; } + @java.lang.Override + public _FinalStage primaryPatient(PrimaryPatient primaryPatient) { + this.primaryPatient = Optional.ofNullable(primaryPatient); + return this; + } + + @java.lang.Override + @JsonSetter(value = "primary_patient", nulls = Nulls.SKIP) + public _FinalStage primaryPatient(Optional primaryPatient) { + this.primaryPatient = primaryPatient; + return this; + } + /** *

Optional FHIR provider name for provider-specific profiles

* @return Reference to {@code this} so that method calls can be chained together. @@ -405,6 +450,7 @@ public CreateMultiRequest build() { text, version, provider, + primaryPatient, patientReference, implementationGuide, detectionEffort, diff --git a/src/main/java/com/phenoml/api/resources/lang2fhir/requests/CreateRequest.java b/src/main/java/com/phenoml/api/resources/lang2fhir/requests/CreateRequest.java index 2419de13..011acb69 100644 --- a/src/main/java/com/phenoml/api/resources/lang2fhir/requests/CreateRequest.java +++ b/src/main/java/com/phenoml/api/resources/lang2fhir/requests/CreateRequest.java @@ -45,7 +45,7 @@ public String getVersion() { } /** - * @return Type of FHIR resource to create. Use 'auto' for automatic resource type detection, or specify a supported US Core profile. Recommended to use the supported US Core Profiles for validated results but you can also use any custom profile you've uploaded (if you're a develop or launch customer) + * @return Type of FHIR resource to create. Use 'auto' for automatic resource type detection, or specify a supported profile. The default profile set includes US Core profiles and selected base R4 resources; you can also use any custom profile you've uploaded (if you're a develop or launch customer). */ @JsonProperty("resource") public CreateRequestResource getResource() { @@ -100,7 +100,7 @@ public interface VersionStage { public interface ResourceStage { /** - *

Type of FHIR resource to create. Use 'auto' for automatic resource type detection, or specify a supported US Core profile. Recommended to use the supported US Core Profiles for validated results but you can also use any custom profile you've uploaded (if you're a develop or launch customer)

+ *

Type of FHIR resource to create. Use 'auto' for automatic resource type detection, or specify a supported profile. The default profile set includes US Core profiles and selected base R4 resources; you can also use any custom profile you've uploaded (if you're a develop or launch customer).

*/ TextStage resource(@NotNull CreateRequestResource resource); } @@ -153,7 +153,7 @@ public ResourceStage version(@NotNull String version) { } /** - *

Type of FHIR resource to create. Use 'auto' for automatic resource type detection, or specify a supported US Core profile. Recommended to use the supported US Core Profiles for validated results but you can also use any custom profile you've uploaded (if you're a develop or launch customer)

+ *

Type of FHIR resource to create. Use 'auto' for automatic resource type detection, or specify a supported profile. The default profile set includes US Core profiles and selected base R4 resources; you can also use any custom profile you've uploaded (if you're a develop or launch customer).

* @return Reference to {@code this} so that method calls can be chained together. */ @java.lang.Override diff --git a/src/main/java/com/phenoml/api/resources/lang2fhir/requests/DocumentMultiRequest.java b/src/main/java/com/phenoml/api/resources/lang2fhir/requests/DocumentMultiRequest.java index 7033b7d4..bc6ef50b 100644 --- a/src/main/java/com/phenoml/api/resources/lang2fhir/requests/DocumentMultiRequest.java +++ b/src/main/java/com/phenoml/api/resources/lang2fhir/requests/DocumentMultiRequest.java @@ -16,6 +16,7 @@ import com.phenoml.api.resources.lang2fhir.types.DocumentMultiRequestDetectionEffort; import com.phenoml.api.resources.lang2fhir.types.DocumentMultiRequestValidationMethod; import com.phenoml.api.resources.lang2fhir.types.PatientReference; +import com.phenoml.api.resources.lang2fhir.types.PrimaryPatient; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -31,6 +32,8 @@ public final class DocumentMultiRequest { private final Optional provider; + private final Optional primaryPatient; + private final Optional patientReference; private final Optional implementationGuide; @@ -47,6 +50,7 @@ private DocumentMultiRequest( String version, String content, Optional provider, + Optional primaryPatient, Optional patientReference, Optional implementationGuide, Optional detectionEffort, @@ -56,6 +60,7 @@ private DocumentMultiRequest( this.version = version; this.content = content; this.provider = provider; + this.primaryPatient = primaryPatient; this.patientReference = patientReference; this.implementationGuide = implementationGuide; this.detectionEffort = detectionEffort; @@ -74,8 +79,11 @@ public String getVersion() { /** * @return Base64 encoded file content. - * Supported file types: PDF (application/pdf), PNG (image/png), JPEG (image/jpeg), TIFF (image/tiff). + * Supported file types: PDF (application/pdf), PNG (image/png), JPEG (image/jpeg), TIFF (image/tiff), RTF (application/rtf), XML/C-CDA (text/xml). + * RTF and XML/C-CDA uploads are available on dedicated instances only. * File type is auto-detected from content magic bytes. + * The decoded file must not exceed 20 MiB. RTF and XML/C-CDA documents whose extracted text exceeds 1 MiB are rejected. + * Generic XML must include an XML declaration; C-CDA documents rooted at ClinicalDocument may omit it. */ @JsonProperty("content") public String getContent() { @@ -90,13 +98,21 @@ public Optional getProvider() { return provider; } + @JsonProperty("primary_patient") + public Optional getPrimaryPatient() { + return primaryPatient; + } + + /** + * @return Deprecated compatibility alias for primary_patient.identifier. Cannot be combined with primary_patient. + */ @JsonProperty("patient_reference") public Optional getPatientReference() { return patientReference; } /** - * @return Custom Implementation Guide name. When specified, profiles from this IG are included alongside US Core profiles during resource detection. US Core is always the base layer; custom IG profiles are additive. + * @return Custom Implementation Guide name. When specified, profiles from this IG are included alongside the default profiles during resource detection. Default profiles are always the base layer; custom IG profiles are additive. */ @JsonProperty("implementation_guide") public Optional getImplementationGuide() { @@ -139,6 +155,7 @@ private boolean equalTo(DocumentMultiRequest other) { return version.equals(other.version) && content.equals(other.content) && provider.equals(other.provider) + && primaryPatient.equals(other.primaryPatient) && patientReference.equals(other.patientReference) && implementationGuide.equals(other.implementationGuide) && detectionEffort.equals(other.detectionEffort) @@ -152,6 +169,7 @@ public int hashCode() { this.version, this.content, this.provider, + this.primaryPatient, this.patientReference, this.implementationGuide, this.detectionEffort, @@ -180,8 +198,11 @@ public interface VersionStage { public interface ContentStage { /** *

Base64 encoded file content. - * Supported file types: PDF (application/pdf), PNG (image/png), JPEG (image/jpeg), TIFF (image/tiff). - * File type is auto-detected from content magic bytes.

+ * Supported file types: PDF (application/pdf), PNG (image/png), JPEG (image/jpeg), TIFF (image/tiff), RTF (application/rtf), XML/C-CDA (text/xml). + * RTF and XML/C-CDA uploads are available on dedicated instances only. + * File type is auto-detected from content magic bytes. + * The decoded file must not exceed 20 MiB. RTF and XML/C-CDA documents whose extracted text exceeds 1 MiB are rejected. + * Generic XML must include an XML declaration; C-CDA documents rooted at ClinicalDocument may omit it.

*/ _FinalStage content(@NotNull String content); } @@ -200,12 +221,19 @@ public interface _FinalStage { _FinalStage provider(String provider); + _FinalStage primaryPatient(Optional primaryPatient); + + _FinalStage primaryPatient(PrimaryPatient primaryPatient); + + /** + *

Deprecated compatibility alias for primary_patient.identifier. Cannot be combined with primary_patient.

+ */ _FinalStage patientReference(Optional patientReference); _FinalStage patientReference(PatientReference patientReference); /** - *

Custom Implementation Guide name. When specified, profiles from this IG are included alongside US Core profiles during resource detection. US Core is always the base layer; custom IG profiles are additive.

+ *

Custom Implementation Guide name. When specified, profiles from this IG are included alongside the default profiles during resource detection. Default profiles are always the base layer; custom IG profiles are additive.

*/ _FinalStage implementationGuide(Optional implementationGuide); @@ -246,6 +274,8 @@ public static final class Builder implements VersionStage, ContentStage, _FinalS private Optional patientReference = Optional.empty(); + private Optional primaryPatient = Optional.empty(); + private Optional provider = Optional.empty(); @JsonAnySetter @@ -258,6 +288,7 @@ public Builder from(DocumentMultiRequest other) { version(other.getVersion()); content(other.getContent()); provider(other.getProvider()); + primaryPatient(other.getPrimaryPatient()); patientReference(other.getPatientReference()); implementationGuide(other.getImplementationGuide()); detectionEffort(other.getDetectionEffort()); @@ -279,8 +310,11 @@ public ContentStage version(@NotNull String version) { /** *

Base64 encoded file content. - * Supported file types: PDF (application/pdf), PNG (image/png), JPEG (image/jpeg), TIFF (image/tiff). - * File type is auto-detected from content magic bytes.

+ * Supported file types: PDF (application/pdf), PNG (image/png), JPEG (image/jpeg), TIFF (image/tiff), RTF (application/rtf), XML/C-CDA (text/xml). + * RTF and XML/C-CDA uploads are available on dedicated instances only. + * File type is auto-detected from content magic bytes. + * The decoded file must not exceed 20 MiB. RTF and XML/C-CDA documents whose extracted text exceeds 1 MiB are rejected. + * Generic XML must include an XML declaration; C-CDA documents rooted at ClinicalDocument may omit it.

* @return Reference to {@code this} so that method calls can be chained together. */ @java.lang.Override @@ -344,7 +378,7 @@ public _FinalStage detectionEffort(Optional } /** - *

Custom Implementation Guide name. When specified, profiles from this IG are included alongside US Core profiles during resource detection. US Core is always the base layer; custom IG profiles are additive.

+ *

Custom Implementation Guide name. When specified, profiles from this IG are included alongside the default profiles during resource detection. Default profiles are always the base layer; custom IG profiles are additive.

* @return Reference to {@code this} so that method calls can be chained together. */ @java.lang.Override @@ -354,7 +388,7 @@ public _FinalStage implementationGuide(String implementationGuide) { } /** - *

Custom Implementation Guide name. When specified, profiles from this IG are included alongside US Core profiles during resource detection. US Core is always the base layer; custom IG profiles are additive.

+ *

Custom Implementation Guide name. When specified, profiles from this IG are included alongside the default profiles during resource detection. Default profiles are always the base layer; custom IG profiles are additive.

*/ @java.lang.Override @JsonSetter(value = "implementation_guide", nulls = Nulls.SKIP) @@ -363,12 +397,19 @@ public _FinalStage implementationGuide(Optional implementationGuide) { return this; } + /** + *

Deprecated compatibility alias for primary_patient.identifier. Cannot be combined with primary_patient.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ @java.lang.Override public _FinalStage patientReference(PatientReference patientReference) { this.patientReference = Optional.ofNullable(patientReference); return this; } + /** + *

Deprecated compatibility alias for primary_patient.identifier. Cannot be combined with primary_patient.

+ */ @java.lang.Override @JsonSetter(value = "patient_reference", nulls = Nulls.SKIP) public _FinalStage patientReference(Optional patientReference) { @@ -376,6 +417,19 @@ public _FinalStage patientReference(Optional patientReference) return this; } + @java.lang.Override + public _FinalStage primaryPatient(PrimaryPatient primaryPatient) { + this.primaryPatient = Optional.ofNullable(primaryPatient); + return this; + } + + @java.lang.Override + @JsonSetter(value = "primary_patient", nulls = Nulls.SKIP) + public _FinalStage primaryPatient(Optional primaryPatient) { + this.primaryPatient = primaryPatient; + return this; + } + /** *

Optional FHIR provider name for provider-specific profiles

* @return Reference to {@code this} so that method calls can be chained together. @@ -402,6 +456,7 @@ public DocumentMultiRequest build() { version, content, provider, + primaryPatient, patientReference, implementationGuide, detectionEffort, diff --git a/src/main/java/com/phenoml/api/resources/lang2fhir/requests/DocumentRequest.java b/src/main/java/com/phenoml/api/resources/lang2fhir/requests/DocumentRequest.java index c30cda92..f118cee2 100644 --- a/src/main/java/com/phenoml/api/resources/lang2fhir/requests/DocumentRequest.java +++ b/src/main/java/com/phenoml/api/resources/lang2fhir/requests/DocumentRequest.java @@ -63,8 +63,11 @@ public String getResource() { /** * @return Base64 encoded file content. - * Supported file types: PDF (application/pdf), PNG (image/png), JPEG (image/jpeg), TIFF (image/tiff). + * Supported file types: PDF (application/pdf), PNG (image/png), JPEG (image/jpeg), TIFF (image/tiff), RTF (application/rtf), XML/C-CDA (text/xml). + * RTF and XML/C-CDA uploads are available on dedicated instances only. * File type is auto-detected from content magic bytes. + * The decoded file must not exceed 20 MiB. RTF and XML/C-CDA documents whose extracted text exceeds 1 MiB are rejected. + * Generic XML must include an XML declaration; C-CDA documents rooted at ClinicalDocument may omit it. */ @JsonProperty("content") public String getContent() { @@ -127,8 +130,11 @@ public interface ResourceStage { public interface ContentStage { /** *

Base64 encoded file content. - * Supported file types: PDF (application/pdf), PNG (image/png), JPEG (image/jpeg), TIFF (image/tiff). - * File type is auto-detected from content magic bytes.

+ * Supported file types: PDF (application/pdf), PNG (image/png), JPEG (image/jpeg), TIFF (image/tiff), RTF (application/rtf), XML/C-CDA (text/xml). + * RTF and XML/C-CDA uploads are available on dedicated instances only. + * File type is auto-detected from content magic bytes. + * The decoded file must not exceed 20 MiB. RTF and XML/C-CDA documents whose extracted text exceeds 1 MiB are rejected. + * Generic XML must include an XML declaration; C-CDA documents rooted at ClinicalDocument may omit it.

*/ _FinalStage content(@NotNull String content); } @@ -193,8 +199,11 @@ public ContentStage resource(@NotNull String resource) { /** *

Base64 encoded file content. - * Supported file types: PDF (application/pdf), PNG (image/png), JPEG (image/jpeg), TIFF (image/tiff). - * File type is auto-detected from content magic bytes.

+ * Supported file types: PDF (application/pdf), PNG (image/png), JPEG (image/jpeg), TIFF (image/tiff), RTF (application/rtf), XML/C-CDA (text/xml). + * RTF and XML/C-CDA uploads are available on dedicated instances only. + * File type is auto-detected from content magic bytes. + * The decoded file must not exceed 20 MiB. RTF and XML/C-CDA documents whose extracted text exceeds 1 MiB are rejected. + * Generic XML must include an XML declaration; C-CDA documents rooted at ClinicalDocument may omit it.

* @return Reference to {@code this} so that method calls can be chained together. */ @java.lang.Override diff --git a/src/main/java/com/phenoml/api/resources/lang2fhir/types/CreateRequestResource.java b/src/main/java/com/phenoml/api/resources/lang2fhir/types/CreateRequestResource.java index 102460c3..e396abf1 100644 --- a/src/main/java/com/phenoml/api/resources/lang2fhir/types/CreateRequestResource.java +++ b/src/main/java/com/phenoml/api/resources/lang2fhir/types/CreateRequestResource.java @@ -12,6 +12,12 @@ public final class CreateRequestResource { public static final CreateRequestResource VITAL_SIGNS = new CreateRequestResource(Value.VITAL_SIGNS, "vital-signs"); + public static final CreateRequestResource MEDICATIONSTATEMENT = + new CreateRequestResource(Value.MEDICATIONSTATEMENT, "medicationstatement"); + + public static final CreateRequestResource MEDICATIONADMINISTRATION = + new CreateRequestResource(Value.MEDICATIONADMINISTRATION, "medicationadministration"); + public static final CreateRequestResource SIMPLE_OBSERVATION = new CreateRequestResource(Value.SIMPLE_OBSERVATION, "simple-observation"); @@ -31,6 +37,9 @@ public final class CreateRequestResource { public static final CreateRequestResource CAREPLAN = new CreateRequestResource(Value.CAREPLAN, "careplan"); + public static final CreateRequestResource FAMILYMEMBERHISTORY = + new CreateRequestResource(Value.FAMILYMEMBERHISTORY, "familymemberhistory"); + public static final CreateRequestResource SERVICEREQUEST = new CreateRequestResource(Value.SERVICEREQUEST, "servicerequest"); @@ -87,6 +96,10 @@ public T visit(Visitor visitor) { return visitor.visitMedicationrequest(); case VITAL_SIGNS: return visitor.visitVitalSigns(); + case MEDICATIONSTATEMENT: + return visitor.visitMedicationstatement(); + case MEDICATIONADMINISTRATION: + return visitor.visitMedicationadministration(); case SIMPLE_OBSERVATION: return visitor.visitSimpleObservation(); case QUESTIONNAIRE: @@ -101,6 +114,8 @@ public T visit(Visitor visitor) { return visitor.visitObservationClinicalResult(); case CAREPLAN: return visitor.visitCareplan(); + case FAMILYMEMBERHISTORY: + return visitor.visitFamilymemberhistory(); case SERVICEREQUEST: return visitor.visitServicerequest(); case COVERAGE: @@ -130,6 +145,10 @@ public static CreateRequestResource valueOf(String value) { return MEDICATIONREQUEST; case "vital-signs": return VITAL_SIGNS; + case "medicationstatement": + return MEDICATIONSTATEMENT; + case "medicationadministration": + return MEDICATIONADMINISTRATION; case "simple-observation": return SIMPLE_OBSERVATION; case "questionnaire": @@ -144,6 +163,8 @@ public static CreateRequestResource valueOf(String value) { return OBSERVATION_CLINICAL_RESULT; case "careplan": return CAREPLAN; + case "familymemberhistory": + return FAMILYMEMBERHISTORY; case "servicerequest": return SERVICEREQUEST; case "coverage": @@ -172,8 +193,14 @@ public enum Value { CONDITION_ENCOUNTER_DIAGNOSIS, + FAMILYMEMBERHISTORY, + + MEDICATIONADMINISTRATION, + MEDICATIONREQUEST, + MEDICATIONSTATEMENT, + CAREPLAN, CONDITION_PROBLEMS_HEALTH_CONCERNS, @@ -210,8 +237,14 @@ public interface Visitor { T visitConditionEncounterDiagnosis(); + T visitFamilymemberhistory(); + + T visitMedicationadministration(); + T visitMedicationrequest(); + T visitMedicationstatement(); + T visitCareplan(); T visitConditionProblemsHealthConcerns(); diff --git a/src/main/java/com/phenoml/api/resources/lang2fhir/types/PrimaryPatient.java b/src/main/java/com/phenoml/api/resources/lang2fhir/types/PrimaryPatient.java new file mode 100644 index 00000000..6d6a08f1 --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhir/types/PrimaryPatient.java @@ -0,0 +1,191 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhir.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.phenoml.api.core.ObjectMappers; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = PrimaryPatient.Builder.class) +public final class PrimaryPatient { + private final Optional identifier; + + private final Optional name; + + private final Optional birthDate; + + private final Optional gender; + + private final Map additionalProperties; + + private PrimaryPatient( + Optional identifier, + Optional name, + Optional birthDate, + Optional gender, + Map additionalProperties) { + this.identifier = identifier; + this.name = name; + this.birthDate = birthDate; + this.gender = gender; + this.additionalProperties = additionalProperties; + } + + @JsonProperty("identifier") + public Optional getIdentifier() { + return identifier; + } + + @JsonProperty("name") + public Optional getName() { + return name; + } + + /** + * @return Complete date of birth in YYYY-MM-DD format. + */ + @JsonProperty("birthDate") + public Optional getBirthDate() { + return birthDate; + } + + /** + * @return Administrative gender. This corroborates another match but does not identify a patient alone. + */ + @JsonProperty("gender") + public Optional getGender() { + return gender; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof PrimaryPatient && equalTo((PrimaryPatient) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(PrimaryPatient other) { + return identifier.equals(other.identifier) + && name.equals(other.name) + && birthDate.equals(other.birthDate) + && gender.equals(other.gender); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.identifier, this.name, this.birthDate, this.gender); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional identifier = Optional.empty(); + + private Optional name = Optional.empty(); + + private Optional birthDate = Optional.empty(); + + private Optional gender = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(PrimaryPatient other) { + identifier(other.getIdentifier()); + name(other.getName()); + birthDate(other.getBirthDate()); + gender(other.getGender()); + return this; + } + + @JsonSetter(value = "identifier", nulls = Nulls.SKIP) + public Builder identifier(Optional identifier) { + this.identifier = identifier; + return this; + } + + public Builder identifier(PatientReference identifier) { + this.identifier = Optional.ofNullable(identifier); + return this; + } + + @JsonSetter(value = "name", nulls = Nulls.SKIP) + public Builder name(Optional name) { + this.name = name; + return this; + } + + public Builder name(PrimaryPatientName name) { + this.name = Optional.ofNullable(name); + return this; + } + + /** + *

Complete date of birth in YYYY-MM-DD format.

+ */ + @JsonSetter(value = "birthDate", nulls = Nulls.SKIP) + public Builder birthDate(Optional birthDate) { + this.birthDate = birthDate; + return this; + } + + public Builder birthDate(String birthDate) { + this.birthDate = Optional.ofNullable(birthDate); + return this; + } + + /** + *

Administrative gender. This corroborates another match but does not identify a patient alone.

+ */ + @JsonSetter(value = "gender", nulls = Nulls.SKIP) + public Builder gender(Optional gender) { + this.gender = gender; + return this; + } + + public Builder gender(PrimaryPatientGender gender) { + this.gender = Optional.ofNullable(gender); + return this; + } + + public PrimaryPatient build() { + return new PrimaryPatient(identifier, name, birthDate, gender, additionalProperties); + } + + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhir/types/PrimaryPatientGender.java b/src/main/java/com/phenoml/api/resources/lang2fhir/types/PrimaryPatientGender.java new file mode 100644 index 00000000..67f5e09e --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhir/types/PrimaryPatientGender.java @@ -0,0 +1,103 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhir.types; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public final class PrimaryPatientGender { + public static final PrimaryPatientGender MALE = new PrimaryPatientGender(Value.MALE, "male"); + + public static final PrimaryPatientGender FEMALE = new PrimaryPatientGender(Value.FEMALE, "female"); + + public static final PrimaryPatientGender OTHER = new PrimaryPatientGender(Value.OTHER, "other"); + + public static final PrimaryPatientGender UNKNOWN = new PrimaryPatientGender(Value.UNKNOWN, "unknown"); + + private final Value value; + + private final String string; + + PrimaryPatientGender(Value value, String string) { + this.value = value; + this.string = string; + } + + public Value getEnumValue() { + return value; + } + + @java.lang.Override + @JsonValue + public String toString() { + return this.string; + } + + @java.lang.Override + public boolean equals(Object other) { + return (this == other) + || (other instanceof PrimaryPatientGender && this.string.equals(((PrimaryPatientGender) other).string)); + } + + @java.lang.Override + public int hashCode() { + return this.string.hashCode(); + } + + public T visit(Visitor visitor) { + switch (value) { + case MALE: + return visitor.visitMale(); + case FEMALE: + return visitor.visitFemale(); + case OTHER: + return visitor.visitOther(); + case UNKNOWN: + return visitor.visitUnknown(); + case _UNKNOWN: + default: + return visitor.visitUnknown(string); + } + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static PrimaryPatientGender valueOf(String value) { + switch (value) { + case "male": + return MALE; + case "female": + return FEMALE; + case "other": + return OTHER; + case "unknown": + return UNKNOWN; + default: + return new PrimaryPatientGender(Value._UNKNOWN, value); + } + } + + public enum Value { + MALE, + + FEMALE, + + OTHER, + + UNKNOWN, + + _UNKNOWN + } + + public interface Visitor { + T visitMale(); + + T visitFemale(); + + T visitOther(); + + T visitUnknown(); + + T visitUnknown(String unknownType); + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhir/types/PrimaryPatientName.java b/src/main/java/com/phenoml/api/resources/lang2fhir/types/PrimaryPatientName.java new file mode 100644 index 00000000..9fae91bf --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhir/types/PrimaryPatientName.java @@ -0,0 +1,141 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhir.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.phenoml.api.core.ObjectMappers; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = PrimaryPatientName.Builder.class) +public final class PrimaryPatientName { + private final Optional family; + + private final Optional> given; + + private final Map additionalProperties; + + private PrimaryPatientName( + Optional family, Optional> given, Map additionalProperties) { + this.family = family; + this.given = given; + this.additionalProperties = additionalProperties; + } + + /** + * @return Family name. + */ + @JsonProperty("family") + public Optional getFamily() { + return family; + } + + /** + * @return Given names. Matching succeeds when a generated name has a supplied given name. + */ + @JsonProperty("given") + public Optional> getGiven() { + return given; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof PrimaryPatientName && equalTo((PrimaryPatientName) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(PrimaryPatientName other) { + return family.equals(other.family) && given.equals(other.given); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.family, this.given); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional family = Optional.empty(); + + private Optional> given = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(PrimaryPatientName other) { + family(other.getFamily()); + given(other.getGiven()); + return this; + } + + /** + *

Family name.

+ */ + @JsonSetter(value = "family", nulls = Nulls.SKIP) + public Builder family(Optional family) { + this.family = family; + return this; + } + + public Builder family(String family) { + this.family = Optional.ofNullable(family); + return this; + } + + /** + *

Given names. Matching succeeds when a generated name has a supplied given name.

+ */ + @JsonSetter(value = "given", nulls = Nulls.SKIP) + public Builder given(Optional> given) { + this.given = given; + return this; + } + + public Builder given(List given) { + this.given = Optional.ofNullable(given); + return this; + } + + public PrimaryPatientName build() { + return new PrimaryPatientName(family, given, additionalProperties); + } + + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhir/types/ResourceReviewFinding.java b/src/main/java/com/phenoml/api/resources/lang2fhir/types/ResourceReviewFinding.java index eda3e0b1..ae840aef 100644 --- a/src/main/java/com/phenoml/api/resources/lang2fhir/types/ResourceReviewFinding.java +++ b/src/main/java/com/phenoml/api/resources/lang2fhir/types/ResourceReviewFinding.java @@ -28,6 +28,8 @@ public final class ResourceReviewFinding { private final Optional supported; + private final Optional unaudited; + private final Optional rationale; private final Map additionalProperties; @@ -37,12 +39,14 @@ private ResourceReviewFinding( Optional fieldPath, Optional value, Optional supported, + Optional unaudited, Optional rationale, Map additionalProperties) { this.fieldKind = fieldKind; this.fieldPath = fieldPath; this.value = value; this.supported = supported; + this.unaudited = unaudited; this.rationale = rationale; this.additionalProperties = additionalProperties; } @@ -66,13 +70,21 @@ public Optional getValue() { } /** - * @return Always false for a flagged finding. + * @return False when the reviewer found the field unsupported. Do not treat this field as a verdict when unaudited is true. */ @JsonProperty("supported") public Optional getSupported() { return supported; } + /** + * @return True when the reviewer did not return a verdict for this field; the resource was quarantined without treating the finding as evidence that the value is unsupported. + */ + @JsonProperty("unaudited") + public Optional getUnaudited() { + return unaudited; + } + /** * @return Short explanation of why the value is not supported by the source. */ @@ -97,12 +109,13 @@ private boolean equalTo(ResourceReviewFinding other) { && fieldPath.equals(other.fieldPath) && value.equals(other.value) && supported.equals(other.supported) + && unaudited.equals(other.unaudited) && rationale.equals(other.rationale); } @java.lang.Override public int hashCode() { - return Objects.hash(this.fieldKind, this.fieldPath, this.value, this.supported, this.rationale); + return Objects.hash(this.fieldKind, this.fieldPath, this.value, this.supported, this.unaudited, this.rationale); } @java.lang.Override @@ -124,6 +137,8 @@ public static final class Builder { private Optional supported = Optional.empty(); + private Optional unaudited = Optional.empty(); + private Optional rationale = Optional.empty(); @JsonAnySetter @@ -136,6 +151,7 @@ public Builder from(ResourceReviewFinding other) { fieldPath(other.getFieldPath()); value(other.getValue()); supported(other.getSupported()); + unaudited(other.getUnaudited()); rationale(other.getRationale()); return this; } @@ -177,7 +193,7 @@ public Builder value(String value) { } /** - *

Always false for a flagged finding.

+ *

False when the reviewer found the field unsupported. Do not treat this field as a verdict when unaudited is true.

*/ @JsonSetter(value = "supported", nulls = Nulls.SKIP) public Builder supported(Optional supported) { @@ -190,6 +206,20 @@ public Builder supported(Boolean supported) { return this; } + /** + *

True when the reviewer did not return a verdict for this field; the resource was quarantined without treating the finding as evidence that the value is unsupported.

+ */ + @JsonSetter(value = "unaudited", nulls = Nulls.SKIP) + public Builder unaudited(Optional unaudited) { + this.unaudited = unaudited; + return this; + } + + public Builder unaudited(Boolean unaudited) { + this.unaudited = Optional.ofNullable(unaudited); + return this; + } + /** *

Short explanation of why the value is not supported by the source.

*/ @@ -205,7 +235,8 @@ public Builder rationale(String rationale) { } public ResourceReviewFinding build() { - return new ResourceReviewFinding(fieldKind, fieldPath, value, supported, rationale, additionalProperties); + return new ResourceReviewFinding( + fieldKind, fieldPath, value, supported, unaudited, rationale, additionalProperties); } public Builder additionalProperty(String key, Object value) { diff --git a/src/main/java/com/phenoml/api/resources/lang2fhir/types/ResourceReviewFlagged.java b/src/main/java/com/phenoml/api/resources/lang2fhir/types/ResourceReviewFlagged.java index 8fc7653e..1fc06296 100644 --- a/src/main/java/com/phenoml/api/resources/lang2fhir/types/ResourceReviewFlagged.java +++ b/src/main/java/com/phenoml/api/resources/lang2fhir/types/ResourceReviewFlagged.java @@ -66,7 +66,7 @@ public Optional> getResource() { } /** - * @return The unsupported fields that caused the resource to be flagged. + * @return The findings that caused the resource to be quarantined. */ @JsonProperty("findings") public Optional> getFindings() { @@ -168,7 +168,7 @@ public Builder resource(Map resource) { } /** - *

The unsupported fields that caused the resource to be flagged.

+ *

The findings that caused the resource to be quarantined.

*/ @JsonSetter(value = "findings", nulls = Nulls.SKIP) public Builder findings(Optional> findings) { diff --git a/src/main/java/com/phenoml/api/resources/lang2fhir/types/ResourceReviewRemediated.java b/src/main/java/com/phenoml/api/resources/lang2fhir/types/ResourceReviewRemediated.java new file mode 100644 index 00000000..baaf8d0f --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhir/types/ResourceReviewRemediated.java @@ -0,0 +1,198 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhir.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.phenoml.api.core.ObjectMappers; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = ResourceReviewRemediated.Builder.class) +public final class ResourceReviewRemediated { + private final Optional tempId; + + private final Optional resourceType; + + private final Optional action; + + private final Optional> findings; + + private final Map additionalProperties; + + private ResourceReviewRemediated( + Optional tempId, + Optional resourceType, + Optional action, + Optional> findings, + Map additionalProperties) { + this.tempId = tempId; + this.resourceType = resourceType; + this.action = action; + this.findings = findings; + this.additionalProperties = additionalProperties; + } + + /** + * @return The urn:uuid of the remediated resource (its bundle fullUrl). + */ + @JsonProperty("tempId") + public Optional getTempId() { + return tempId; + } + + @JsonProperty("resourceType") + public Optional getResourceType() { + return resourceType; + } + + /** + * @return The safe change applied to the resource in the returned bundle. + */ + @JsonProperty("action") + public Optional getAction() { + return action; + } + + /** + * @return Findings for fields in the pre-remediation resource that caused this action. + */ + @JsonProperty("findings") + public Optional> getFindings() { + return findings; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof ResourceReviewRemediated && equalTo((ResourceReviewRemediated) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(ResourceReviewRemediated other) { + return tempId.equals(other.tempId) + && resourceType.equals(other.resourceType) + && action.equals(other.action) + && findings.equals(other.findings); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.tempId, this.resourceType, this.action, this.findings); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional tempId = Optional.empty(); + + private Optional resourceType = Optional.empty(); + + private Optional action = Optional.empty(); + + private Optional> findings = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(ResourceReviewRemediated other) { + tempId(other.getTempId()); + resourceType(other.getResourceType()); + action(other.getAction()); + findings(other.getFindings()); + return this; + } + + /** + *

The urn:uuid of the remediated resource (its bundle fullUrl).

+ */ + @JsonSetter(value = "tempId", nulls = Nulls.SKIP) + public Builder tempId(Optional tempId) { + this.tempId = tempId; + return this; + } + + public Builder tempId(String tempId) { + this.tempId = Optional.ofNullable(tempId); + return this; + } + + @JsonSetter(value = "resourceType", nulls = Nulls.SKIP) + public Builder resourceType(Optional resourceType) { + this.resourceType = resourceType; + return this; + } + + public Builder resourceType(String resourceType) { + this.resourceType = Optional.ofNullable(resourceType); + return this; + } + + /** + *

The safe change applied to the resource in the returned bundle.

+ */ + @JsonSetter(value = "action", nulls = Nulls.SKIP) + public Builder action(Optional action) { + this.action = action; + return this; + } + + public Builder action(String action) { + this.action = Optional.ofNullable(action); + return this; + } + + /** + *

Findings for fields in the pre-remediation resource that caused this action.

+ */ + @JsonSetter(value = "findings", nulls = Nulls.SKIP) + public Builder findings(Optional> findings) { + this.findings = findings; + return this; + } + + public Builder findings(List findings) { + this.findings = Optional.ofNullable(findings); + return this; + } + + public ResourceReviewRemediated build() { + return new ResourceReviewRemediated(tempId, resourceType, action, findings, additionalProperties); + } + + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhir/types/ResourceReviewResult.java b/src/main/java/com/phenoml/api/resources/lang2fhir/types/ResourceReviewResult.java index 2807b69d..316d528c 100644 --- a/src/main/java/com/phenoml/api/resources/lang2fhir/types/ResourceReviewResult.java +++ b/src/main/java/com/phenoml/api/resources/lang2fhir/types/ResourceReviewResult.java @@ -23,22 +23,35 @@ public final class ResourceReviewResult { private final Optional> flagged; + private final Optional> remediated; + private final Map additionalProperties; private ResourceReviewResult( - Optional> flagged, Map additionalProperties) { + Optional> flagged, + Optional> remediated, + Map additionalProperties) { this.flagged = flagged; + this.remediated = remediated; this.additionalProperties = additionalProperties; } /** - * @return Resources pulled from the bundle because a reviewed field was not supported by the source. + * @return Resources pulled from the bundle because an unsupported finding could not be safely repaired. */ @JsonProperty("flagged") public Optional> getFlagged() { return flagged; } + /** + * @return Resources retained in the bundle after unsupported codings were safely removed. + */ + @JsonProperty("remediated") + public Optional> getRemediated() { + return remediated; + } + @java.lang.Override public boolean equals(Object other) { if (this == other) return true; @@ -51,12 +64,12 @@ public Map getAdditionalProperties() { } private boolean equalTo(ResourceReviewResult other) { - return flagged.equals(other.flagged); + return flagged.equals(other.flagged) && remediated.equals(other.remediated); } @java.lang.Override public int hashCode() { - return Objects.hash(this.flagged); + return Objects.hash(this.flagged, this.remediated); } @java.lang.Override @@ -72,6 +85,8 @@ public static Builder builder() { public static final class Builder { private Optional> flagged = Optional.empty(); + private Optional> remediated = Optional.empty(); + @JsonAnySetter private Map additionalProperties = new HashMap<>(); @@ -79,11 +94,12 @@ private Builder() {} public Builder from(ResourceReviewResult other) { flagged(other.getFlagged()); + remediated(other.getRemediated()); return this; } /** - *

Resources pulled from the bundle because a reviewed field was not supported by the source.

+ *

Resources pulled from the bundle because an unsupported finding could not be safely repaired.

*/ @JsonSetter(value = "flagged", nulls = Nulls.SKIP) public Builder flagged(Optional> flagged) { @@ -96,8 +112,22 @@ public Builder flagged(List flagged) { return this; } + /** + *

Resources retained in the bundle after unsupported codings were safely removed.

+ */ + @JsonSetter(value = "remediated", nulls = Nulls.SKIP) + public Builder remediated(Optional> remediated) { + this.remediated = remediated; + return this; + } + + public Builder remediated(List remediated) { + this.remediated = Optional.ofNullable(remediated); + return this; + } + public ResourceReviewResult build() { - return new ResourceReviewResult(flagged, additionalProperties); + return new ResourceReviewResult(flagged, remediated, additionalProperties); } public Builder additionalProperty(String key, Object value) { diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/AsyncLang2FhirBatchClient.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/AsyncLang2FhirBatchClient.java new file mode 100644 index 00000000..d40d952e --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/AsyncLang2FhirBatchClient.java @@ -0,0 +1,529 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch; + +import com.phenoml.api.core.ClientOptions; +import com.phenoml.api.core.RequestOptions; +import com.phenoml.api.resources.lang2fhirbatch.requests.CreateBatchRequest; +import com.phenoml.api.resources.lang2fhirbatch.requests.GetRequest; +import com.phenoml.api.resources.lang2fhirbatch.requests.GetResultsRequest; +import com.phenoml.api.resources.lang2fhirbatch.requests.ListRequest; +import com.phenoml.api.resources.lang2fhirbatch.requests.UploadItemRequest; +import com.phenoml.api.resources.lang2fhirbatch.types.BatchJob; +import com.phenoml.api.resources.lang2fhirbatch.types.JobDetailResponse; +import com.phenoml.api.resources.lang2fhirbatch.types.JobListResponse; +import com.phenoml.api.resources.lang2fhirbatch.types.ResultsPageResponse; +import com.phenoml.api.resources.lang2fhirbatch.types.UploadItemResponse; +import java.io.File; +import java.io.InputStream; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import okhttp3.MediaType; + +public class AsyncLang2FhirBatchClient { + protected final ClientOptions clientOptions; + + private final AsyncRawLang2FhirBatchClient rawClient; + + public AsyncLang2FhirBatchClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.rawClient = new AsyncRawLang2FhirBatchClient(clientOptions); + } + + /** + * Get responses with HTTP metadata like headers + */ + public AsyncRawLang2FhirBatchClient withRawResponse() { + return this.rawClient; + } + + /** + * Returns a page of the instance's batch jobs, newest first, without + * per-job counts. Jobs are shared across the instance's credentials, so + * this lists every batch job on the instance, not just the calling + * credential's. + */ + public CompletableFuture list() { + return this.rawClient.list().thenApply(response -> response.body()); + } + + /** + * Returns a page of the instance's batch jobs, newest first, without + * per-job counts. Jobs are shared across the instance's credentials, so + * this lists every batch job on the instance, not just the calling + * credential's. + */ + public CompletableFuture list(RequestOptions requestOptions) { + return this.rawClient.list(requestOptions).thenApply(response -> response.body()); + } + + /** + * Returns a page of the instance's batch jobs, newest first, without + * per-job counts. Jobs are shared across the instance's credentials, so + * this lists every batch job on the instance, not just the calling + * credential's. + */ + public CompletableFuture list(ListRequest request) { + return this.rawClient.list(request).thenApply(response -> response.body()); + } + + /** + * Returns a page of the instance's batch jobs, newest first, without + * per-job counts. Jobs are shared across the instance's credentials, so + * this lists every batch job on the instance, not just the calling + * credential's. + */ + public CompletableFuture list(ListRequest request, RequestOptions requestOptions) { + return this.rawClient.list(request, requestOptions).thenApply(response -> response.body()); + } + + /** + * Opens an empty batch job. Items arrive on later upload calls and the set + * is sealed at finalize. + *

Supplying request_id makes the create idempotent on that token: a + * retried submit whose response was lost returns the original job rather + * than opening a second one. This dedupe is scoped to the calling + * credential. A request_id whose job was canceled or failed before it + * finalized is released for a fresh replay; once a job is finalized, its + * request_id keeps resolving to it even after cancellation.

+ *

There is no limit on how many jobs an instance may hold at once; how many + * items run in parallel is a property of the instance, not of the job count.

+ */ + public CompletableFuture create() { + return this.rawClient.create().thenApply(response -> response.body()); + } + + /** + * Opens an empty batch job. Items arrive on later upload calls and the set + * is sealed at finalize. + *

Supplying request_id makes the create idempotent on that token: a + * retried submit whose response was lost returns the original job rather + * than opening a second one. This dedupe is scoped to the calling + * credential. A request_id whose job was canceled or failed before it + * finalized is released for a fresh replay; once a job is finalized, its + * request_id keeps resolving to it even after cancellation.

+ *

There is no limit on how many jobs an instance may hold at once; how many + * items run in parallel is a property of the instance, not of the job count.

+ */ + public CompletableFuture create(RequestOptions requestOptions) { + return this.rawClient.create(requestOptions).thenApply(response -> response.body()); + } + + /** + * Opens an empty batch job. Items arrive on later upload calls and the set + * is sealed at finalize. + *

Supplying request_id makes the create idempotent on that token: a + * retried submit whose response was lost returns the original job rather + * than opening a second one. This dedupe is scoped to the calling + * credential. A request_id whose job was canceled or failed before it + * finalized is released for a fresh replay; once a job is finalized, its + * request_id keeps resolving to it even after cancellation.

+ *

There is no limit on how many jobs an instance may hold at once; how many + * items run in parallel is a property of the instance, not of the job count.

+ */ + public CompletableFuture create(CreateBatchRequest request) { + return this.rawClient.create(request).thenApply(response -> response.body()); + } + + /** + * Opens an empty batch job. Items arrive on later upload calls and the set + * is sealed at finalize. + *

Supplying request_id makes the create idempotent on that token: a + * retried submit whose response was lost returns the original job rather + * than opening a second one. This dedupe is scoped to the calling + * credential. A request_id whose job was canceled or failed before it + * finalized is released for a fresh replay; once a job is finalized, its + * request_id keeps resolving to it even after cancellation.

+ *

There is no limit on how many jobs an instance may hold at once; how many + * items run in parallel is a property of the instance, not of the job count.

+ */ + public CompletableFuture create(CreateBatchRequest request, RequestOptions requestOptions) { + return this.rawClient.create(request, requestOptions).thenApply(response -> response.body()); + } + + /** + * Stores one item of a job from a multipart upload. A batch's items arrive + * one per request. The item carries either a document extraction + * (whose input file rides as raw bytes in the file part) or a + * create extraction (JSON only, no file). + *

The upload enforces these rules:

+ *
    + *
  • Set exactly one of document or create. Setting both, or + * neither, is a 400.
  • + *
  • When document is set, file is required — it supplies the + * document's file content (PDF, image, RTF, or XML/C-CDA).
  • + *
  • When create is set, file is forbidden — a create item carries + * no file.
  • + *
  • document and create must each be a JSON object.
  • + *
+ *

Only the item's structure is checked here: the fields inside document + * or create are not validated at upload. A body that is well-formed JSON + * but not a valid request for its endpoint is still accepted with 202 + * and fails later during processing, recorded as an item error. A + * wrong-typed field the endpoint cannot decode fails as invalid_input; a + * body that decodes but the pipeline rejects (for example, a missing + * required field) fails as processing_failed.

+ *

Supplying request_id makes the upload idempotent on that token. A + * re-upload under the same token overwrites the same item rather than + * adding a second, so a client that lost an upload's response can safely + * re-send it. The response's deduplicated is true only when the + * re-uploaded payload matches the one already stored; a same-token upload + * with a changed payload overwrites in place and returns false.

+ *

Set a request_id on every upload: re-sending under the same token + * is the only way to repair a lost or incomplete upload, including the one + * a finalize 409 reports. Without one, a re-send adds a new item instead + * of replacing the missing one, and the job cannot be finalized.

+ *

Uploads are rejected once the job has been finalized (409), once it + * holds its 500-item limit (409), or when the item is too large (413 — + * see the raw-file limit in the API description).

+ */ + public CompletableFuture uploadItem(String jobId, Optional file) { + return this.rawClient.uploadItem(jobId, file).thenApply(response -> response.body()); + } + + /** + * Stores one item of a job from a multipart upload. A batch's items arrive + * one per request. The item carries either a document extraction + * (whose input file rides as raw bytes in the file part) or a + * create extraction (JSON only, no file). + *

The upload enforces these rules:

+ *
    + *
  • Set exactly one of document or create. Setting both, or + * neither, is a 400.
  • + *
  • When document is set, file is required — it supplies the + * document's file content (PDF, image, RTF, or XML/C-CDA).
  • + *
  • When create is set, file is forbidden — a create item carries + * no file.
  • + *
  • document and create must each be a JSON object.
  • + *
+ *

Only the item's structure is checked here: the fields inside document + * or create are not validated at upload. A body that is well-formed JSON + * but not a valid request for its endpoint is still accepted with 202 + * and fails later during processing, recorded as an item error. A + * wrong-typed field the endpoint cannot decode fails as invalid_input; a + * body that decodes but the pipeline rejects (for example, a missing + * required field) fails as processing_failed.

+ *

Supplying request_id makes the upload idempotent on that token. A + * re-upload under the same token overwrites the same item rather than + * adding a second, so a client that lost an upload's response can safely + * re-send it. The response's deduplicated is true only when the + * re-uploaded payload matches the one already stored; a same-token upload + * with a changed payload overwrites in place and returns false.

+ *

Set a request_id on every upload: re-sending under the same token + * is the only way to repair a lost or incomplete upload, including the one + * a finalize 409 reports. Without one, a re-send adds a new item instead + * of replacing the missing one, and the job cannot be finalized.

+ *

Uploads are rejected once the job has been finalized (409), once it + * holds its 500-item limit (409), or when the item is too large (413 — + * see the raw-file limit in the API description).

+ */ + public CompletableFuture uploadItem( + String jobId, Optional file, RequestOptions requestOptions) { + return this.rawClient.uploadItem(jobId, file, requestOptions).thenApply(response -> response.body()); + } + + /** + * Stores one item of a job from a multipart upload. A batch's items arrive + * one per request. The item carries either a document extraction + * (whose input file rides as raw bytes in the file part) or a + * create extraction (JSON only, no file). + *

The upload enforces these rules:

+ *
    + *
  • Set exactly one of document or create. Setting both, or + * neither, is a 400.
  • + *
  • When document is set, file is required — it supplies the + * document's file content (PDF, image, RTF, or XML/C-CDA).
  • + *
  • When create is set, file is forbidden — a create item carries + * no file.
  • + *
  • document and create must each be a JSON object.
  • + *
+ *

Only the item's structure is checked here: the fields inside document + * or create are not validated at upload. A body that is well-formed JSON + * but not a valid request for its endpoint is still accepted with 202 + * and fails later during processing, recorded as an item error. A + * wrong-typed field the endpoint cannot decode fails as invalid_input; a + * body that decodes but the pipeline rejects (for example, a missing + * required field) fails as processing_failed.

+ *

Supplying request_id makes the upload idempotent on that token. A + * re-upload under the same token overwrites the same item rather than + * adding a second, so a client that lost an upload's response can safely + * re-send it. The response's deduplicated is true only when the + * re-uploaded payload matches the one already stored; a same-token upload + * with a changed payload overwrites in place and returns false.

+ *

Set a request_id on every upload: re-sending under the same token + * is the only way to repair a lost or incomplete upload, including the one + * a finalize 409 reports. Without one, a re-send adds a new item instead + * of replacing the missing one, and the job cannot be finalized.

+ *

Uploads are rejected once the job has been finalized (409), once it + * holds its 500-item limit (409), or when the item is too large (413 — + * see the raw-file limit in the API description).

+ */ + public CompletableFuture uploadItem( + String jobId, Optional file, UploadItemRequest request) { + return this.rawClient.uploadItem(jobId, file, request).thenApply(response -> response.body()); + } + + /** + * Stores one item of a job from a multipart upload. A batch's items arrive + * one per request. The item carries either a document extraction + * (whose input file rides as raw bytes in the file part) or a + * create extraction (JSON only, no file). + *

The upload enforces these rules:

+ *
    + *
  • Set exactly one of document or create. Setting both, or + * neither, is a 400.
  • + *
  • When document is set, file is required — it supplies the + * document's file content (PDF, image, RTF, or XML/C-CDA).
  • + *
  • When create is set, file is forbidden — a create item carries + * no file.
  • + *
  • document and create must each be a JSON object.
  • + *
+ *

Only the item's structure is checked here: the fields inside document + * or create are not validated at upload. A body that is well-formed JSON + * but not a valid request for its endpoint is still accepted with 202 + * and fails later during processing, recorded as an item error. A + * wrong-typed field the endpoint cannot decode fails as invalid_input; a + * body that decodes but the pipeline rejects (for example, a missing + * required field) fails as processing_failed.

+ *

Supplying request_id makes the upload idempotent on that token. A + * re-upload under the same token overwrites the same item rather than + * adding a second, so a client that lost an upload's response can safely + * re-send it. The response's deduplicated is true only when the + * re-uploaded payload matches the one already stored; a same-token upload + * with a changed payload overwrites in place and returns false.

+ *

Set a request_id on every upload: re-sending under the same token + * is the only way to repair a lost or incomplete upload, including the one + * a finalize 409 reports. Without one, a re-send adds a new item instead + * of replacing the missing one, and the job cannot be finalized.

+ *

Uploads are rejected once the job has been finalized (409), once it + * holds its 500-item limit (409), or when the item is too large (413 — + * see the raw-file limit in the API description).

+ */ + public CompletableFuture uploadItem( + String jobId, Optional file, UploadItemRequest request, RequestOptions requestOptions) { + return this.rawClient.uploadItem(jobId, file, request, requestOptions).thenApply(response -> response.body()); + } + + public CompletableFuture uploadItem( + String jobId, Optional file, InputStream stream, String filename) { + return this.rawClient.uploadItem(jobId, file, stream, filename).thenApply(response -> response.body()); + } + + public CompletableFuture uploadItem( + String jobId, Optional file, InputStream stream, String filename, MediaType mediaType) { + return this.rawClient + .uploadItem(jobId, file, stream, filename, mediaType) + .thenApply(response -> response.body()); + } + + public CompletableFuture uploadItem( + String jobId, Optional file, InputStream stream, String filename, RequestOptions requestOptions) { + return this.rawClient + .uploadItem(jobId, file, stream, filename, requestOptions) + .thenApply(response -> response.body()); + } + + public CompletableFuture uploadItem( + String jobId, + Optional file, + InputStream stream, + String filename, + MediaType mediaType, + RequestOptions requestOptions) { + return this.rawClient + .uploadItem(jobId, file, stream, filename, mediaType, requestOptions) + .thenApply(response -> response.body()); + } + + /** + * Seals the job's item set and starts processing. Takes no request body. + * Finalize is idempotent: a retried finalize succeeds again. + *

If a previous upload did not complete, finalize returns a 409; re-send + * the missing upload (with the same request_id), then finalize. + * Finalizing a job with no items is a 400.

+ */ + public CompletableFuture finalize(String jobId) { + return this.rawClient.finalize(jobId).thenApply(response -> response.body()); + } + + /** + * Seals the job's item set and starts processing. Takes no request body. + * Finalize is idempotent: a retried finalize succeeds again. + *

If a previous upload did not complete, finalize returns a 409; re-send + * the missing upload (with the same request_id), then finalize. + * Finalizing a job with no items is a 400.

+ */ + public CompletableFuture finalize(String jobId, RequestOptions requestOptions) { + return this.rawClient.finalize(jobId, requestOptions).thenApply(response -> response.body()); + } + + /** + * Drives a job to the terminal canceled state on request. Takes no + * request body. + *

Cancel does not delete the job: the job record and any results already + * produced are preserved for the normal retention window, the same as a + * completed or failed job. Items stop being processed and keep the state + * they held at cancellation, so a canceled job's counts may show + * unfinished items that never resolve.

+ *

Cancel is idempotent: canceling an already-canceled job returns 200 + * with the job. Canceling a job that has already completed or failed is + * a 409.

+ */ + public CompletableFuture cancel(String jobId) { + return this.rawClient.cancel(jobId).thenApply(response -> response.body()); + } + + /** + * Drives a job to the terminal canceled state on request. Takes no + * request body. + *

Cancel does not delete the job: the job record and any results already + * produced are preserved for the normal retention window, the same as a + * completed or failed job. Items stop being processed and keep the state + * they held at cancellation, so a canceled job's counts may show + * unfinished items that never resolve.

+ *

Cancel is idempotent: canceling an already-canceled job returns 200 + * with the job. Canceling a job that has already completed or failed is + * a 409.

+ */ + public CompletableFuture cancel(String jobId, RequestOptions requestOptions) { + return this.rawClient.cancel(jobId, requestOptions).thenApply(response -> response.body()); + } + + /** + * Returns a job's record, its per-status item counts, and one page of + * per-item statuses. + *

Items are listed in a stable order that is not upload order and is the + * same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public CompletableFuture get(String jobId) { + return this.rawClient.get(jobId).thenApply(response -> response.body()); + } + + /** + * Returns a job's record, its per-status item counts, and one page of + * per-item statuses. + *

Items are listed in a stable order that is not upload order and is the + * same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public CompletableFuture get(String jobId, RequestOptions requestOptions) { + return this.rawClient.get(jobId, requestOptions).thenApply(response -> response.body()); + } + + /** + * Returns a job's record, its per-status item counts, and one page of + * per-item statuses. + *

Items are listed in a stable order that is not upload order and is the + * same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public CompletableFuture get(String jobId, GetRequest request) { + return this.rawClient.get(jobId, request).thenApply(response -> response.body()); + } + + /** + * Returns a job's record, its per-status item counts, and one page of + * per-item statuses. + *

Items are listed in a stable order that is not upload order and is the + * same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public CompletableFuture get(String jobId, GetRequest request, RequestOptions requestOptions) { + return this.rawClient.get(jobId, request, requestOptions).thenApply(response -> response.body()); + } + + /** + * A lighter status page. Returns the same per-item status entries as + * GET /lang2fhir/batch/{job_id}, but without the job record or counts, + * and the entries carry result_size rather than any result content. Use + * each entry's item_id to fetch that item's result from + * GET /lang2fhir/batch/{job_id}/results/{item_id}. + *

Entries are listed in a stable order that is not upload order and is + * the same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public CompletableFuture getResults(String jobId) { + return this.rawClient.getResults(jobId).thenApply(response -> response.body()); + } + + /** + * A lighter status page. Returns the same per-item status entries as + * GET /lang2fhir/batch/{job_id}, but without the job record or counts, + * and the entries carry result_size rather than any result content. Use + * each entry's item_id to fetch that item's result from + * GET /lang2fhir/batch/{job_id}/results/{item_id}. + *

Entries are listed in a stable order that is not upload order and is + * the same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public CompletableFuture getResults(String jobId, RequestOptions requestOptions) { + return this.rawClient.getResults(jobId, requestOptions).thenApply(response -> response.body()); + } + + /** + * A lighter status page. Returns the same per-item status entries as + * GET /lang2fhir/batch/{job_id}, but without the job record or counts, + * and the entries carry result_size rather than any result content. Use + * each entry's item_id to fetch that item's result from + * GET /lang2fhir/batch/{job_id}/results/{item_id}. + *

Entries are listed in a stable order that is not upload order and is + * the same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public CompletableFuture getResults(String jobId, GetResultsRequest request) { + return this.rawClient.getResults(jobId, request).thenApply(response -> response.body()); + } + + /** + * A lighter status page. Returns the same per-item status entries as + * GET /lang2fhir/batch/{job_id}, but without the job record or counts, + * and the entries carry result_size rather than any result content. Use + * each entry's item_id to fetch that item's result from + * GET /lang2fhir/batch/{job_id}/results/{item_id}. + *

Entries are listed in a stable order that is not upload order and is + * the same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public CompletableFuture getResults( + String jobId, GetResultsRequest request, RequestOptions requestOptions) { + return this.rawClient.getResults(jobId, request, requestOptions).thenApply(response -> response.body()); + } + + /** + * Streams one item's stored result bytes verbatim as application/json. + * The body is the response the item's synchronous multi endpoint would have + * returned — a DocumentMultiResponse for a document item or a + * CreateMultiResponse for a create item. + *

Only a succeeded item has a result: an item that has not succeeded + * (pending, processing, or failed) is a 409, and a result that has + * expired is a 404.

+ */ + public CompletableFuture> getResult(String jobId, String itemId) { + return this.rawClient.getResult(jobId, itemId).thenApply(response -> response.body()); + } + + /** + * Streams one item's stored result bytes verbatim as application/json. + * The body is the response the item's synchronous multi endpoint would have + * returned — a DocumentMultiResponse for a document item or a + * CreateMultiResponse for a create item. + *

Only a succeeded item has a result: an item that has not succeeded + * (pending, processing, or failed) is a 409, and a result that has + * expired is a 404.

+ */ + public CompletableFuture> getResult( + String jobId, String itemId, RequestOptions requestOptions) { + return this.rawClient.getResult(jobId, itemId, requestOptions).thenApply(response -> response.body()); + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/AsyncRawLang2FhirBatchClient.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/AsyncRawLang2FhirBatchClient.java new file mode 100644 index 00000000..fcb5e2c1 --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/AsyncRawLang2FhirBatchClient.java @@ -0,0 +1,1764 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.phenoml.api.core.ClientOptions; +import com.phenoml.api.core.FileStream; +import com.phenoml.api.core.MediaTypes; +import com.phenoml.api.core.ObjectMappers; +import com.phenoml.api.core.PhenomlClientApiException; +import com.phenoml.api.core.PhenomlClientException; +import com.phenoml.api.core.PhenomlClientHttpResponse; +import com.phenoml.api.core.QueryStringMapper; +import com.phenoml.api.core.RequestOptions; +import com.phenoml.api.core.RetryInterceptor; +import com.phenoml.api.resources.lang2fhirbatch.errors.BadRequestError; +import com.phenoml.api.resources.lang2fhirbatch.errors.ClientClosedRequestError; +import com.phenoml.api.resources.lang2fhirbatch.errors.ConflictError; +import com.phenoml.api.resources.lang2fhirbatch.errors.ContentTooLargeError; +import com.phenoml.api.resources.lang2fhirbatch.errors.GatewayTimeoutError; +import com.phenoml.api.resources.lang2fhirbatch.errors.InternalServerError; +import com.phenoml.api.resources.lang2fhirbatch.errors.NotFoundError; +import com.phenoml.api.resources.lang2fhirbatch.errors.UnauthorizedError; +import com.phenoml.api.resources.lang2fhirbatch.requests.CreateBatchRequest; +import com.phenoml.api.resources.lang2fhirbatch.requests.GetRequest; +import com.phenoml.api.resources.lang2fhirbatch.requests.GetResultsRequest; +import com.phenoml.api.resources.lang2fhirbatch.requests.ListRequest; +import com.phenoml.api.resources.lang2fhirbatch.requests.UploadItemRequest; +import com.phenoml.api.resources.lang2fhirbatch.types.BatchJob; +import com.phenoml.api.resources.lang2fhirbatch.types.JobDetailResponse; +import com.phenoml.api.resources.lang2fhirbatch.types.JobListResponse; +import com.phenoml.api.resources.lang2fhirbatch.types.ResultsPageResponse; +import com.phenoml.api.resources.lang2fhirbatch.types.UploadItemResponse; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Headers; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.jetbrains.annotations.NotNull; + +public class AsyncRawLang2FhirBatchClient { + protected final ClientOptions clientOptions; + + public AsyncRawLang2FhirBatchClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + } + + /** + * Returns a page of the instance's batch jobs, newest first, without + * per-job counts. Jobs are shared across the instance's credentials, so + * this lists every batch job on the instance, not just the calling + * credential's. + */ + public CompletableFuture> list() { + return list(ListRequest.builder().build()); + } + + /** + * Returns a page of the instance's batch jobs, newest first, without + * per-job counts. Jobs are shared across the instance's credentials, so + * this lists every batch job on the instance, not just the calling + * credential's. + */ + public CompletableFuture> list(RequestOptions requestOptions) { + return list(ListRequest.builder().build(), requestOptions); + } + + /** + * Returns a page of the instance's batch jobs, newest first, without + * per-job counts. Jobs are shared across the instance's credentials, so + * this lists every batch job on the instance, not just the calling + * credential's. + */ + public CompletableFuture> list(ListRequest request) { + return list(request, null); + } + + /** + * Returns a page of the instance's batch jobs, newest first, without + * per-job counts. Jobs are shared across the instance's credentials, so + * this lists every batch job on the instance, not just the calling + * credential's. + */ + public CompletableFuture> list( + ListRequest request, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch"); + if (request.getCursor().isPresent()) { + QueryStringMapper.addQueryParameter( + httpUrl, "cursor", request.getCursor().get(), false); + } + if (request.getLimit().isPresent()) { + QueryStringMapper.addQueryParameter( + httpUrl, "limit", request.getLimit().get(), false); + } + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request.Builder _requestBuilder = new Request.Builder() + .url(httpUrl.build()) + .method("GET", null) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json"); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, JobListResponse.class), + response)); + return; + } + try { + switch (response.code()) { + case 400: + future.completeExceptionally(new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 401: + future.completeExceptionally(new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 499: + future.completeExceptionally(new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 500: + future.completeExceptionally(new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 504: + future.completeExceptionally(new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + }); + return future; + } + + /** + * Opens an empty batch job. Items arrive on later upload calls and the set + * is sealed at finalize. + *

Supplying request_id makes the create idempotent on that token: a + * retried submit whose response was lost returns the original job rather + * than opening a second one. This dedupe is scoped to the calling + * credential. A request_id whose job was canceled or failed before it + * finalized is released for a fresh replay; once a job is finalized, its + * request_id keeps resolving to it even after cancellation.

+ *

There is no limit on how many jobs an instance may hold at once; how many + * items run in parallel is a property of the instance, not of the job count.

+ */ + public CompletableFuture> create() { + return create(CreateBatchRequest.builder().build()); + } + + /** + * Opens an empty batch job. Items arrive on later upload calls and the set + * is sealed at finalize. + *

Supplying request_id makes the create idempotent on that token: a + * retried submit whose response was lost returns the original job rather + * than opening a second one. This dedupe is scoped to the calling + * credential. A request_id whose job was canceled or failed before it + * finalized is released for a fresh replay; once a job is finalized, its + * request_id keeps resolving to it even after cancellation.

+ *

There is no limit on how many jobs an instance may hold at once; how many + * items run in parallel is a property of the instance, not of the job count.

+ */ + public CompletableFuture> create(RequestOptions requestOptions) { + return create(CreateBatchRequest.builder().build(), requestOptions); + } + + /** + * Opens an empty batch job. Items arrive on later upload calls and the set + * is sealed at finalize. + *

Supplying request_id makes the create idempotent on that token: a + * retried submit whose response was lost returns the original job rather + * than opening a second one. This dedupe is scoped to the calling + * credential. A request_id whose job was canceled or failed before it + * finalized is released for a fresh replay; once a job is finalized, its + * request_id keeps resolving to it even after cancellation.

+ *

There is no limit on how many jobs an instance may hold at once; how many + * items run in parallel is a property of the instance, not of the job count.

+ */ + public CompletableFuture> create(CreateBatchRequest request) { + return create(request, null); + } + + /** + * Opens an empty batch job. Items arrive on later upload calls and the set + * is sealed at finalize. + *

Supplying request_id makes the create idempotent on that token: a + * retried submit whose response was lost returns the original job rather + * than opening a second one. This dedupe is scoped to the calling + * credential. A request_id whose job was canceled or failed before it + * finalized is released for a fresh replay; once a job is finalized, its + * request_id keeps resolving to it even after cancellation.

+ *

There is no limit on how many jobs an instance may hold at once; how many + * items run in parallel is a property of the instance, not of the job count.

+ */ + public CompletableFuture> create( + CreateBatchRequest request, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, BatchJob.class), response)); + return; + } + try { + switch (response.code()) { + case 400: + future.completeExceptionally(new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 401: + future.completeExceptionally(new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 499: + future.completeExceptionally(new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 500: + future.completeExceptionally(new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 504: + future.completeExceptionally(new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + }); + return future; + } + + /** + * Stores one item of a job from a multipart upload. A batch's items arrive + * one per request. The item carries either a document extraction + * (whose input file rides as raw bytes in the file part) or a + * create extraction (JSON only, no file). + *

The upload enforces these rules:

+ *
    + *
  • Set exactly one of document or create. Setting both, or + * neither, is a 400.
  • + *
  • When document is set, file is required — it supplies the + * document's file content (PDF, image, RTF, or XML/C-CDA).
  • + *
  • When create is set, file is forbidden — a create item carries + * no file.
  • + *
  • document and create must each be a JSON object.
  • + *
+ *

Only the item's structure is checked here: the fields inside document + * or create are not validated at upload. A body that is well-formed JSON + * but not a valid request for its endpoint is still accepted with 202 + * and fails later during processing, recorded as an item error. A + * wrong-typed field the endpoint cannot decode fails as invalid_input; a + * body that decodes but the pipeline rejects (for example, a missing + * required field) fails as processing_failed.

+ *

Supplying request_id makes the upload idempotent on that token. A + * re-upload under the same token overwrites the same item rather than + * adding a second, so a client that lost an upload's response can safely + * re-send it. The response's deduplicated is true only when the + * re-uploaded payload matches the one already stored; a same-token upload + * with a changed payload overwrites in place and returns false.

+ *

Set a request_id on every upload: re-sending under the same token + * is the only way to repair a lost or incomplete upload, including the one + * a finalize 409 reports. Without one, a re-send adds a new item instead + * of replacing the missing one, and the job cannot be finalized.

+ *

Uploads are rejected once the job has been finalized (409), once it + * holds its 500-item limit (409), or when the item is too large (413 — + * see the raw-file limit in the API description).

+ */ + public CompletableFuture> uploadItem( + String jobId, Optional file) { + return uploadItem(jobId, file, UploadItemRequest.builder().build()); + } + + /** + * Stores one item of a job from a multipart upload. A batch's items arrive + * one per request. The item carries either a document extraction + * (whose input file rides as raw bytes in the file part) or a + * create extraction (JSON only, no file). + *

The upload enforces these rules:

+ *
    + *
  • Set exactly one of document or create. Setting both, or + * neither, is a 400.
  • + *
  • When document is set, file is required — it supplies the + * document's file content (PDF, image, RTF, or XML/C-CDA).
  • + *
  • When create is set, file is forbidden — a create item carries + * no file.
  • + *
  • document and create must each be a JSON object.
  • + *
+ *

Only the item's structure is checked here: the fields inside document + * or create are not validated at upload. A body that is well-formed JSON + * but not a valid request for its endpoint is still accepted with 202 + * and fails later during processing, recorded as an item error. A + * wrong-typed field the endpoint cannot decode fails as invalid_input; a + * body that decodes but the pipeline rejects (for example, a missing + * required field) fails as processing_failed.

+ *

Supplying request_id makes the upload idempotent on that token. A + * re-upload under the same token overwrites the same item rather than + * adding a second, so a client that lost an upload's response can safely + * re-send it. The response's deduplicated is true only when the + * re-uploaded payload matches the one already stored; a same-token upload + * with a changed payload overwrites in place and returns false.

+ *

Set a request_id on every upload: re-sending under the same token + * is the only way to repair a lost or incomplete upload, including the one + * a finalize 409 reports. Without one, a re-send adds a new item instead + * of replacing the missing one, and the job cannot be finalized.

+ *

Uploads are rejected once the job has been finalized (409), once it + * holds its 500-item limit (409), or when the item is too large (413 — + * see the raw-file limit in the API description).

+ */ + public CompletableFuture> uploadItem( + String jobId, Optional file, RequestOptions requestOptions) { + return uploadItem(jobId, file, UploadItemRequest.builder().build(), requestOptions); + } + + /** + * Stores one item of a job from a multipart upload. A batch's items arrive + * one per request. The item carries either a document extraction + * (whose input file rides as raw bytes in the file part) or a + * create extraction (JSON only, no file). + *

The upload enforces these rules:

+ *
    + *
  • Set exactly one of document or create. Setting both, or + * neither, is a 400.
  • + *
  • When document is set, file is required — it supplies the + * document's file content (PDF, image, RTF, or XML/C-CDA).
  • + *
  • When create is set, file is forbidden — a create item carries + * no file.
  • + *
  • document and create must each be a JSON object.
  • + *
+ *

Only the item's structure is checked here: the fields inside document + * or create are not validated at upload. A body that is well-formed JSON + * but not a valid request for its endpoint is still accepted with 202 + * and fails later during processing, recorded as an item error. A + * wrong-typed field the endpoint cannot decode fails as invalid_input; a + * body that decodes but the pipeline rejects (for example, a missing + * required field) fails as processing_failed.

+ *

Supplying request_id makes the upload idempotent on that token. A + * re-upload under the same token overwrites the same item rather than + * adding a second, so a client that lost an upload's response can safely + * re-send it. The response's deduplicated is true only when the + * re-uploaded payload matches the one already stored; a same-token upload + * with a changed payload overwrites in place and returns false.

+ *

Set a request_id on every upload: re-sending under the same token + * is the only way to repair a lost or incomplete upload, including the one + * a finalize 409 reports. Without one, a re-send adds a new item instead + * of replacing the missing one, and the job cannot be finalized.

+ *

Uploads are rejected once the job has been finalized (409), once it + * holds its 500-item limit (409), or when the item is too large (413 — + * see the raw-file limit in the API description).

+ */ + public CompletableFuture> uploadItem( + String jobId, Optional file, UploadItemRequest request) { + return uploadItem(jobId, file, request, null); + } + + /** + * Stores one item of a job from a multipart upload. A batch's items arrive + * one per request. The item carries either a document extraction + * (whose input file rides as raw bytes in the file part) or a + * create extraction (JSON only, no file). + *

The upload enforces these rules:

+ *
    + *
  • Set exactly one of document or create. Setting both, or + * neither, is a 400.
  • + *
  • When document is set, file is required — it supplies the + * document's file content (PDF, image, RTF, or XML/C-CDA).
  • + *
  • When create is set, file is forbidden — a create item carries + * no file.
  • + *
  • document and create must each be a JSON object.
  • + *
+ *

Only the item's structure is checked here: the fields inside document + * or create are not validated at upload. A body that is well-formed JSON + * but not a valid request for its endpoint is still accepted with 202 + * and fails later during processing, recorded as an item error. A + * wrong-typed field the endpoint cannot decode fails as invalid_input; a + * body that decodes but the pipeline rejects (for example, a missing + * required field) fails as processing_failed.

+ *

Supplying request_id makes the upload idempotent on that token. A + * re-upload under the same token overwrites the same item rather than + * adding a second, so a client that lost an upload's response can safely + * re-send it. The response's deduplicated is true only when the + * re-uploaded payload matches the one already stored; a same-token upload + * with a changed payload overwrites in place and returns false.

+ *

Set a request_id on every upload: re-sending under the same token + * is the only way to repair a lost or incomplete upload, including the one + * a finalize 409 reports. Without one, a re-send adds a new item instead + * of replacing the missing one, and the job cannot be finalized.

+ *

Uploads are rejected once the job has been finalized (409), once it + * holds its 500-item limit (409), or when the item is too large (413 — + * see the raw-file limit in the API description).

+ */ + public CompletableFuture> uploadItem( + String jobId, Optional file, UploadItemRequest request, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch") + .addPathSegment(jobId) + .addPathSegments("items"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + try { + if (request.getDocument().isPresent()) { + multipartBodyBuilder.addFormDataPart( + "document", + ObjectMappers.JSON_MAPPER.writeValueAsString( + request.getDocument().get())); + } + if (request.getCreate().isPresent()) { + multipartBodyBuilder.addFormDataPart( + "create", + ObjectMappers.JSON_MAPPER.writeValueAsString( + request.getCreate().get())); + } + if (file.isPresent()) { + String fileMimeType = Files.probeContentType(file.get().toPath()); + MediaType fileMimeTypeMediaType = fileMimeType != null ? MediaType.parse(fileMimeType) : null; + multipartBodyBuilder.addFormDataPart( + "file", file.get().getName(), RequestBody.create(file.get(), fileMimeTypeMediaType)); + } + if (request.getRequestId().isPresent()) { + multipartBodyBuilder.addFormDataPart( + "request_id", request.getRequestId().get()); + } + if (request.getId().isPresent()) { + multipartBodyBuilder.addFormDataPart("id", request.getId().get()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + Request.Builder _requestBuilder = new Request.Builder() + .url(httpUrl.build()) + .method("POST", multipartBodyBuilder.build()) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json"); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, UploadItemResponse.class), + response)); + return; + } + try { + switch (response.code()) { + case 400: + future.completeExceptionally(new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 401: + future.completeExceptionally(new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 404: + future.completeExceptionally(new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 409: + future.completeExceptionally(new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 413: + future.completeExceptionally(new ContentTooLargeError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 499: + future.completeExceptionally(new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 500: + future.completeExceptionally(new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 504: + future.completeExceptionally(new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + }); + return future; + } + + public CompletableFuture> uploadItem( + String jobId, Optional file, InputStream stream, String filename) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch") + .addPathSegment(jobId) + .addPathSegments("items"); + FileStream fs = new FileStream(stream, filename, null); + MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + multipartBodyBuilder.addFormDataPart("file", filename, fs.toRequestBody()); + RequestBody body = multipartBodyBuilder.build(); + Request.Builder _requestBuilder = new Request.Builder(); + _requestBuilder.url(httpUrl.build()); + _requestBuilder.method("POST", body); + _requestBuilder.headers(Headers.of(this.clientOptions.headers((RequestOptions) null))); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, UploadItemResponse.class), + response)); + return; + } + try { + switch (response.code()) { + case 400: + future.completeExceptionally(new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 401: + future.completeExceptionally(new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 404: + future.completeExceptionally(new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 409: + future.completeExceptionally(new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 413: + future.completeExceptionally(new ContentTooLargeError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 499: + future.completeExceptionally(new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 500: + future.completeExceptionally(new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 504: + future.completeExceptionally(new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + }); + return future; + } + + public CompletableFuture> uploadItem( + String jobId, Optional file, InputStream stream, String filename, MediaType mediaType) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch") + .addPathSegment(jobId) + .addPathSegments("items"); + FileStream fs = new FileStream(stream, filename, mediaType); + MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + multipartBodyBuilder.addFormDataPart("file", filename, fs.toRequestBody()); + RequestBody body = multipartBodyBuilder.build(); + Request.Builder _requestBuilder = new Request.Builder(); + _requestBuilder.url(httpUrl.build()); + _requestBuilder.method("POST", body); + _requestBuilder.headers(Headers.of(this.clientOptions.headers((RequestOptions) null))); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, UploadItemResponse.class), + response)); + return; + } + try { + switch (response.code()) { + case 400: + future.completeExceptionally(new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 401: + future.completeExceptionally(new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 404: + future.completeExceptionally(new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 409: + future.completeExceptionally(new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 413: + future.completeExceptionally(new ContentTooLargeError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 499: + future.completeExceptionally(new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 500: + future.completeExceptionally(new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 504: + future.completeExceptionally(new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + }); + return future; + } + + public CompletableFuture> uploadItem( + String jobId, Optional file, InputStream stream, String filename, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch") + .addPathSegment(jobId) + .addPathSegments("items"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + FileStream fs = new FileStream(stream, filename, null); + MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + multipartBodyBuilder.addFormDataPart("file", filename, fs.toRequestBody()); + RequestBody body = multipartBodyBuilder.build(); + Request.Builder _requestBuilder = new Request.Builder(); + _requestBuilder.url(httpUrl.build()); + _requestBuilder.method("POST", body); + _requestBuilder.headers(Headers.of(this.clientOptions.headers(requestOptions))); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, UploadItemResponse.class), + response)); + return; + } + try { + switch (response.code()) { + case 400: + future.completeExceptionally(new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 401: + future.completeExceptionally(new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 404: + future.completeExceptionally(new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 409: + future.completeExceptionally(new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 413: + future.completeExceptionally(new ContentTooLargeError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 499: + future.completeExceptionally(new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 500: + future.completeExceptionally(new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 504: + future.completeExceptionally(new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + }); + return future; + } + + public CompletableFuture> uploadItem( + String jobId, + Optional file, + InputStream stream, + String filename, + MediaType mediaType, + RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch") + .addPathSegment(jobId) + .addPathSegments("items"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + FileStream fs = new FileStream(stream, filename, mediaType); + MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + multipartBodyBuilder.addFormDataPart("file", filename, fs.toRequestBody()); + RequestBody body = multipartBodyBuilder.build(); + Request.Builder _requestBuilder = new Request.Builder(); + _requestBuilder.url(httpUrl.build()); + _requestBuilder.method("POST", body); + _requestBuilder.headers(Headers.of(this.clientOptions.headers(requestOptions))); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, UploadItemResponse.class), + response)); + return; + } + try { + switch (response.code()) { + case 400: + future.completeExceptionally(new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 401: + future.completeExceptionally(new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 404: + future.completeExceptionally(new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 409: + future.completeExceptionally(new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 413: + future.completeExceptionally(new ContentTooLargeError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 499: + future.completeExceptionally(new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 500: + future.completeExceptionally(new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 504: + future.completeExceptionally(new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + }); + return future; + } + + /** + * Seals the job's item set and starts processing. Takes no request body. + * Finalize is idempotent: a retried finalize succeeds again. + *

If a previous upload did not complete, finalize returns a 409; re-send + * the missing upload (with the same request_id), then finalize. + * Finalizing a job with no items is a 400.

+ */ + public CompletableFuture> finalize(String jobId) { + return finalize(jobId, null); + } + + /** + * Seals the job's item set and starts processing. Takes no request body. + * Finalize is idempotent: a retried finalize succeeds again. + *

If a previous upload did not complete, finalize returns a 409; re-send + * the missing upload (with the same request_id), then finalize. + * Finalizing a job with no items is a 400.

+ */ + public CompletableFuture> finalize( + String jobId, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch") + .addPathSegment(jobId) + .addPathSegments("finalize"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("POST", RequestBody.create("", null)) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, BatchJob.class), response)); + return; + } + try { + switch (response.code()) { + case 400: + future.completeExceptionally(new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 401: + future.completeExceptionally(new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 404: + future.completeExceptionally(new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 409: + future.completeExceptionally(new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 499: + future.completeExceptionally(new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 500: + future.completeExceptionally(new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 504: + future.completeExceptionally(new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + }); + return future; + } + + /** + * Drives a job to the terminal canceled state on request. Takes no + * request body. + *

Cancel does not delete the job: the job record and any results already + * produced are preserved for the normal retention window, the same as a + * completed or failed job. Items stop being processed and keep the state + * they held at cancellation, so a canceled job's counts may show + * unfinished items that never resolve.

+ *

Cancel is idempotent: canceling an already-canceled job returns 200 + * with the job. Canceling a job that has already completed or failed is + * a 409.

+ */ + public CompletableFuture> cancel(String jobId) { + return cancel(jobId, null); + } + + /** + * Drives a job to the terminal canceled state on request. Takes no + * request body. + *

Cancel does not delete the job: the job record and any results already + * produced are preserved for the normal retention window, the same as a + * completed or failed job. Items stop being processed and keep the state + * they held at cancellation, so a canceled job's counts may show + * unfinished items that never resolve.

+ *

Cancel is idempotent: canceling an already-canceled job returns 200 + * with the job. Canceling a job that has already completed or failed is + * a 409.

+ */ + public CompletableFuture> cancel(String jobId, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch") + .addPathSegment(jobId) + .addPathSegments("cancel"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("POST", RequestBody.create("", null)) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, BatchJob.class), response)); + return; + } + try { + switch (response.code()) { + case 401: + future.completeExceptionally(new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 404: + future.completeExceptionally(new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 409: + future.completeExceptionally(new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 499: + future.completeExceptionally(new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 500: + future.completeExceptionally(new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 504: + future.completeExceptionally(new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + }); + return future; + } + + /** + * Returns a job's record, its per-status item counts, and one page of + * per-item statuses. + *

Items are listed in a stable order that is not upload order and is the + * same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public CompletableFuture> get(String jobId) { + return get(jobId, GetRequest.builder().build()); + } + + /** + * Returns a job's record, its per-status item counts, and one page of + * per-item statuses. + *

Items are listed in a stable order that is not upload order and is the + * same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public CompletableFuture> get( + String jobId, RequestOptions requestOptions) { + return get(jobId, GetRequest.builder().build(), requestOptions); + } + + /** + * Returns a job's record, its per-status item counts, and one page of + * per-item statuses. + *

Items are listed in a stable order that is not upload order and is the + * same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public CompletableFuture> get(String jobId, GetRequest request) { + return get(jobId, request, null); + } + + /** + * Returns a job's record, its per-status item counts, and one page of + * per-item statuses. + *

Items are listed in a stable order that is not upload order and is the + * same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public CompletableFuture> get( + String jobId, GetRequest request, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch") + .addPathSegment(jobId); + if (request.getCursor().isPresent()) { + QueryStringMapper.addQueryParameter( + httpUrl, "cursor", request.getCursor().get(), false); + } + if (request.getLimit().isPresent()) { + QueryStringMapper.addQueryParameter( + httpUrl, "limit", request.getLimit().get(), false); + } + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request.Builder _requestBuilder = new Request.Builder() + .url(httpUrl.build()) + .method("GET", null) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json"); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, JobDetailResponse.class), + response)); + return; + } + try { + switch (response.code()) { + case 400: + future.completeExceptionally(new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 401: + future.completeExceptionally(new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 404: + future.completeExceptionally(new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 499: + future.completeExceptionally(new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 500: + future.completeExceptionally(new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 504: + future.completeExceptionally(new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + }); + return future; + } + + /** + * A lighter status page. Returns the same per-item status entries as + * GET /lang2fhir/batch/{job_id}, but without the job record or counts, + * and the entries carry result_size rather than any result content. Use + * each entry's item_id to fetch that item's result from + * GET /lang2fhir/batch/{job_id}/results/{item_id}. + *

Entries are listed in a stable order that is not upload order and is + * the same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public CompletableFuture> getResults(String jobId) { + return getResults(jobId, GetResultsRequest.builder().build()); + } + + /** + * A lighter status page. Returns the same per-item status entries as + * GET /lang2fhir/batch/{job_id}, but without the job record or counts, + * and the entries carry result_size rather than any result content. Use + * each entry's item_id to fetch that item's result from + * GET /lang2fhir/batch/{job_id}/results/{item_id}. + *

Entries are listed in a stable order that is not upload order and is + * the same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public CompletableFuture> getResults( + String jobId, RequestOptions requestOptions) { + return getResults(jobId, GetResultsRequest.builder().build(), requestOptions); + } + + /** + * A lighter status page. Returns the same per-item status entries as + * GET /lang2fhir/batch/{job_id}, but without the job record or counts, + * and the entries carry result_size rather than any result content. Use + * each entry's item_id to fetch that item's result from + * GET /lang2fhir/batch/{job_id}/results/{item_id}. + *

Entries are listed in a stable order that is not upload order and is + * the same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public CompletableFuture> getResults( + String jobId, GetResultsRequest request) { + return getResults(jobId, request, null); + } + + /** + * A lighter status page. Returns the same per-item status entries as + * GET /lang2fhir/batch/{job_id}, but without the job record or counts, + * and the entries carry result_size rather than any result content. Use + * each entry's item_id to fetch that item's result from + * GET /lang2fhir/batch/{job_id}/results/{item_id}. + *

Entries are listed in a stable order that is not upload order and is + * the same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public CompletableFuture> getResults( + String jobId, GetResultsRequest request, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch") + .addPathSegment(jobId) + .addPathSegments("results"); + if (request.getCursor().isPresent()) { + QueryStringMapper.addQueryParameter( + httpUrl, "cursor", request.getCursor().get(), false); + } + if (request.getLimit().isPresent()) { + QueryStringMapper.addQueryParameter( + httpUrl, "limit", request.getLimit().get(), false); + } + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request.Builder _requestBuilder = new Request.Builder() + .url(httpUrl.build()) + .method("GET", null) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json"); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, ResultsPageResponse.class), + response)); + return; + } + try { + switch (response.code()) { + case 400: + future.completeExceptionally(new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 401: + future.completeExceptionally(new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 404: + future.completeExceptionally(new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 499: + future.completeExceptionally(new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 500: + future.completeExceptionally(new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 504: + future.completeExceptionally(new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + }); + return future; + } + + /** + * Streams one item's stored result bytes verbatim as application/json. + * The body is the response the item's synchronous multi endpoint would have + * returned — a DocumentMultiResponse for a document item or a + * CreateMultiResponse for a create item. + *

Only a succeeded item has a result: an item that has not succeeded + * (pending, processing, or failed) is a 409, and a result that has + * expired is a 404.

+ */ + public CompletableFuture>> getResult(String jobId, String itemId) { + return getResult(jobId, itemId, null); + } + + /** + * Streams one item's stored result bytes verbatim as application/json. + * The body is the response the item's synchronous multi endpoint would have + * returned — a DocumentMultiResponse for a document item or a + * CreateMultiResponse for a create item. + *

Only a succeeded item has a result: an item that has not succeeded + * (pending, processing, or failed) is a 409, and a result that has + * expired is a 404.

+ */ + public CompletableFuture>> getResult( + String jobId, String itemId, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch") + .addPathSegment(jobId) + .addPathSegments("results") + .addPathSegment(itemId); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("GET", null) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + CompletableFuture>> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue( + responseBodyString, new TypeReference>() {}), + response)); + return; + } + try { + switch (response.code()) { + case 400: + future.completeExceptionally(new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 401: + future.completeExceptionally(new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 404: + future.completeExceptionally(new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 409: + future.completeExceptionally(new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 499: + future.completeExceptionally(new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 500: + future.completeExceptionally(new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 504: + future.completeExceptionally(new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + }); + return future; + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/Lang2FhirBatchClient.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/Lang2FhirBatchClient.java new file mode 100644 index 00000000..0206b00c --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/Lang2FhirBatchClient.java @@ -0,0 +1,523 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch; + +import com.phenoml.api.core.ClientOptions; +import com.phenoml.api.core.RequestOptions; +import com.phenoml.api.resources.lang2fhirbatch.requests.CreateBatchRequest; +import com.phenoml.api.resources.lang2fhirbatch.requests.GetRequest; +import com.phenoml.api.resources.lang2fhirbatch.requests.GetResultsRequest; +import com.phenoml.api.resources.lang2fhirbatch.requests.ListRequest; +import com.phenoml.api.resources.lang2fhirbatch.requests.UploadItemRequest; +import com.phenoml.api.resources.lang2fhirbatch.types.BatchJob; +import com.phenoml.api.resources.lang2fhirbatch.types.JobDetailResponse; +import com.phenoml.api.resources.lang2fhirbatch.types.JobListResponse; +import com.phenoml.api.resources.lang2fhirbatch.types.ResultsPageResponse; +import com.phenoml.api.resources.lang2fhirbatch.types.UploadItemResponse; +import java.io.File; +import java.io.InputStream; +import java.util.Map; +import java.util.Optional; +import okhttp3.MediaType; + +public class Lang2FhirBatchClient { + protected final ClientOptions clientOptions; + + private final RawLang2FhirBatchClient rawClient; + + public Lang2FhirBatchClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.rawClient = new RawLang2FhirBatchClient(clientOptions); + } + + /** + * Get responses with HTTP metadata like headers + */ + public RawLang2FhirBatchClient withRawResponse() { + return this.rawClient; + } + + /** + * Returns a page of the instance's batch jobs, newest first, without + * per-job counts. Jobs are shared across the instance's credentials, so + * this lists every batch job on the instance, not just the calling + * credential's. + */ + public JobListResponse list() { + return this.rawClient.list().body(); + } + + /** + * Returns a page of the instance's batch jobs, newest first, without + * per-job counts. Jobs are shared across the instance's credentials, so + * this lists every batch job on the instance, not just the calling + * credential's. + */ + public JobListResponse list(RequestOptions requestOptions) { + return this.rawClient.list(requestOptions).body(); + } + + /** + * Returns a page of the instance's batch jobs, newest first, without + * per-job counts. Jobs are shared across the instance's credentials, so + * this lists every batch job on the instance, not just the calling + * credential's. + */ + public JobListResponse list(ListRequest request) { + return this.rawClient.list(request).body(); + } + + /** + * Returns a page of the instance's batch jobs, newest first, without + * per-job counts. Jobs are shared across the instance's credentials, so + * this lists every batch job on the instance, not just the calling + * credential's. + */ + public JobListResponse list(ListRequest request, RequestOptions requestOptions) { + return this.rawClient.list(request, requestOptions).body(); + } + + /** + * Opens an empty batch job. Items arrive on later upload calls and the set + * is sealed at finalize. + *

Supplying request_id makes the create idempotent on that token: a + * retried submit whose response was lost returns the original job rather + * than opening a second one. This dedupe is scoped to the calling + * credential. A request_id whose job was canceled or failed before it + * finalized is released for a fresh replay; once a job is finalized, its + * request_id keeps resolving to it even after cancellation.

+ *

There is no limit on how many jobs an instance may hold at once; how many + * items run in parallel is a property of the instance, not of the job count.

+ */ + public BatchJob create() { + return this.rawClient.create().body(); + } + + /** + * Opens an empty batch job. Items arrive on later upload calls and the set + * is sealed at finalize. + *

Supplying request_id makes the create idempotent on that token: a + * retried submit whose response was lost returns the original job rather + * than opening a second one. This dedupe is scoped to the calling + * credential. A request_id whose job was canceled or failed before it + * finalized is released for a fresh replay; once a job is finalized, its + * request_id keeps resolving to it even after cancellation.

+ *

There is no limit on how many jobs an instance may hold at once; how many + * items run in parallel is a property of the instance, not of the job count.

+ */ + public BatchJob create(RequestOptions requestOptions) { + return this.rawClient.create(requestOptions).body(); + } + + /** + * Opens an empty batch job. Items arrive on later upload calls and the set + * is sealed at finalize. + *

Supplying request_id makes the create idempotent on that token: a + * retried submit whose response was lost returns the original job rather + * than opening a second one. This dedupe is scoped to the calling + * credential. A request_id whose job was canceled or failed before it + * finalized is released for a fresh replay; once a job is finalized, its + * request_id keeps resolving to it even after cancellation.

+ *

There is no limit on how many jobs an instance may hold at once; how many + * items run in parallel is a property of the instance, not of the job count.

+ */ + public BatchJob create(CreateBatchRequest request) { + return this.rawClient.create(request).body(); + } + + /** + * Opens an empty batch job. Items arrive on later upload calls and the set + * is sealed at finalize. + *

Supplying request_id makes the create idempotent on that token: a + * retried submit whose response was lost returns the original job rather + * than opening a second one. This dedupe is scoped to the calling + * credential. A request_id whose job was canceled or failed before it + * finalized is released for a fresh replay; once a job is finalized, its + * request_id keeps resolving to it even after cancellation.

+ *

There is no limit on how many jobs an instance may hold at once; how many + * items run in parallel is a property of the instance, not of the job count.

+ */ + public BatchJob create(CreateBatchRequest request, RequestOptions requestOptions) { + return this.rawClient.create(request, requestOptions).body(); + } + + /** + * Stores one item of a job from a multipart upload. A batch's items arrive + * one per request. The item carries either a document extraction + * (whose input file rides as raw bytes in the file part) or a + * create extraction (JSON only, no file). + *

The upload enforces these rules:

+ *
    + *
  • Set exactly one of document or create. Setting both, or + * neither, is a 400.
  • + *
  • When document is set, file is required — it supplies the + * document's file content (PDF, image, RTF, or XML/C-CDA).
  • + *
  • When create is set, file is forbidden — a create item carries + * no file.
  • + *
  • document and create must each be a JSON object.
  • + *
+ *

Only the item's structure is checked here: the fields inside document + * or create are not validated at upload. A body that is well-formed JSON + * but not a valid request for its endpoint is still accepted with 202 + * and fails later during processing, recorded as an item error. A + * wrong-typed field the endpoint cannot decode fails as invalid_input; a + * body that decodes but the pipeline rejects (for example, a missing + * required field) fails as processing_failed.

+ *

Supplying request_id makes the upload idempotent on that token. A + * re-upload under the same token overwrites the same item rather than + * adding a second, so a client that lost an upload's response can safely + * re-send it. The response's deduplicated is true only when the + * re-uploaded payload matches the one already stored; a same-token upload + * with a changed payload overwrites in place and returns false.

+ *

Set a request_id on every upload: re-sending under the same token + * is the only way to repair a lost or incomplete upload, including the one + * a finalize 409 reports. Without one, a re-send adds a new item instead + * of replacing the missing one, and the job cannot be finalized.

+ *

Uploads are rejected once the job has been finalized (409), once it + * holds its 500-item limit (409), or when the item is too large (413 — + * see the raw-file limit in the API description).

+ */ + public UploadItemResponse uploadItem(String jobId, Optional file) { + return this.rawClient.uploadItem(jobId, file).body(); + } + + /** + * Stores one item of a job from a multipart upload. A batch's items arrive + * one per request. The item carries either a document extraction + * (whose input file rides as raw bytes in the file part) or a + * create extraction (JSON only, no file). + *

The upload enforces these rules:

+ *
    + *
  • Set exactly one of document or create. Setting both, or + * neither, is a 400.
  • + *
  • When document is set, file is required — it supplies the + * document's file content (PDF, image, RTF, or XML/C-CDA).
  • + *
  • When create is set, file is forbidden — a create item carries + * no file.
  • + *
  • document and create must each be a JSON object.
  • + *
+ *

Only the item's structure is checked here: the fields inside document + * or create are not validated at upload. A body that is well-formed JSON + * but not a valid request for its endpoint is still accepted with 202 + * and fails later during processing, recorded as an item error. A + * wrong-typed field the endpoint cannot decode fails as invalid_input; a + * body that decodes but the pipeline rejects (for example, a missing + * required field) fails as processing_failed.

+ *

Supplying request_id makes the upload idempotent on that token. A + * re-upload under the same token overwrites the same item rather than + * adding a second, so a client that lost an upload's response can safely + * re-send it. The response's deduplicated is true only when the + * re-uploaded payload matches the one already stored; a same-token upload + * with a changed payload overwrites in place and returns false.

+ *

Set a request_id on every upload: re-sending under the same token + * is the only way to repair a lost or incomplete upload, including the one + * a finalize 409 reports. Without one, a re-send adds a new item instead + * of replacing the missing one, and the job cannot be finalized.

+ *

Uploads are rejected once the job has been finalized (409), once it + * holds its 500-item limit (409), or when the item is too large (413 — + * see the raw-file limit in the API description).

+ */ + public UploadItemResponse uploadItem(String jobId, Optional file, RequestOptions requestOptions) { + return this.rawClient.uploadItem(jobId, file, requestOptions).body(); + } + + /** + * Stores one item of a job from a multipart upload. A batch's items arrive + * one per request. The item carries either a document extraction + * (whose input file rides as raw bytes in the file part) or a + * create extraction (JSON only, no file). + *

The upload enforces these rules:

+ *
    + *
  • Set exactly one of document or create. Setting both, or + * neither, is a 400.
  • + *
  • When document is set, file is required — it supplies the + * document's file content (PDF, image, RTF, or XML/C-CDA).
  • + *
  • When create is set, file is forbidden — a create item carries + * no file.
  • + *
  • document and create must each be a JSON object.
  • + *
+ *

Only the item's structure is checked here: the fields inside document + * or create are not validated at upload. A body that is well-formed JSON + * but not a valid request for its endpoint is still accepted with 202 + * and fails later during processing, recorded as an item error. A + * wrong-typed field the endpoint cannot decode fails as invalid_input; a + * body that decodes but the pipeline rejects (for example, a missing + * required field) fails as processing_failed.

+ *

Supplying request_id makes the upload idempotent on that token. A + * re-upload under the same token overwrites the same item rather than + * adding a second, so a client that lost an upload's response can safely + * re-send it. The response's deduplicated is true only when the + * re-uploaded payload matches the one already stored; a same-token upload + * with a changed payload overwrites in place and returns false.

+ *

Set a request_id on every upload: re-sending under the same token + * is the only way to repair a lost or incomplete upload, including the one + * a finalize 409 reports. Without one, a re-send adds a new item instead + * of replacing the missing one, and the job cannot be finalized.

+ *

Uploads are rejected once the job has been finalized (409), once it + * holds its 500-item limit (409), or when the item is too large (413 — + * see the raw-file limit in the API description).

+ */ + public UploadItemResponse uploadItem(String jobId, Optional file, UploadItemRequest request) { + return this.rawClient.uploadItem(jobId, file, request).body(); + } + + /** + * Stores one item of a job from a multipart upload. A batch's items arrive + * one per request. The item carries either a document extraction + * (whose input file rides as raw bytes in the file part) or a + * create extraction (JSON only, no file). + *

The upload enforces these rules:

+ *
    + *
  • Set exactly one of document or create. Setting both, or + * neither, is a 400.
  • + *
  • When document is set, file is required — it supplies the + * document's file content (PDF, image, RTF, or XML/C-CDA).
  • + *
  • When create is set, file is forbidden — a create item carries + * no file.
  • + *
  • document and create must each be a JSON object.
  • + *
+ *

Only the item's structure is checked here: the fields inside document + * or create are not validated at upload. A body that is well-formed JSON + * but not a valid request for its endpoint is still accepted with 202 + * and fails later during processing, recorded as an item error. A + * wrong-typed field the endpoint cannot decode fails as invalid_input; a + * body that decodes but the pipeline rejects (for example, a missing + * required field) fails as processing_failed.

+ *

Supplying request_id makes the upload idempotent on that token. A + * re-upload under the same token overwrites the same item rather than + * adding a second, so a client that lost an upload's response can safely + * re-send it. The response's deduplicated is true only when the + * re-uploaded payload matches the one already stored; a same-token upload + * with a changed payload overwrites in place and returns false.

+ *

Set a request_id on every upload: re-sending under the same token + * is the only way to repair a lost or incomplete upload, including the one + * a finalize 409 reports. Without one, a re-send adds a new item instead + * of replacing the missing one, and the job cannot be finalized.

+ *

Uploads are rejected once the job has been finalized (409), once it + * holds its 500-item limit (409), or when the item is too large (413 — + * see the raw-file limit in the API description).

+ */ + public UploadItemResponse uploadItem( + String jobId, Optional file, UploadItemRequest request, RequestOptions requestOptions) { + return this.rawClient.uploadItem(jobId, file, request, requestOptions).body(); + } + + public UploadItemResponse uploadItem(String jobId, Optional file, InputStream stream, String filename) { + return this.rawClient.uploadItem(jobId, file, stream, filename).body(); + } + + public UploadItemResponse uploadItem( + String jobId, Optional file, InputStream stream, String filename, MediaType mediaType) { + return this.rawClient + .uploadItem(jobId, file, stream, filename, mediaType) + .body(); + } + + public UploadItemResponse uploadItem( + String jobId, Optional file, InputStream stream, String filename, RequestOptions requestOptions) { + return this.rawClient + .uploadItem(jobId, file, stream, filename, requestOptions) + .body(); + } + + public UploadItemResponse uploadItem( + String jobId, + Optional file, + InputStream stream, + String filename, + MediaType mediaType, + RequestOptions requestOptions) { + return this.rawClient + .uploadItem(jobId, file, stream, filename, mediaType, requestOptions) + .body(); + } + + /** + * Seals the job's item set and starts processing. Takes no request body. + * Finalize is idempotent: a retried finalize succeeds again. + *

If a previous upload did not complete, finalize returns a 409; re-send + * the missing upload (with the same request_id), then finalize. + * Finalizing a job with no items is a 400.

+ */ + public BatchJob finalize(String jobId) { + return this.rawClient.finalize(jobId).body(); + } + + /** + * Seals the job's item set and starts processing. Takes no request body. + * Finalize is idempotent: a retried finalize succeeds again. + *

If a previous upload did not complete, finalize returns a 409; re-send + * the missing upload (with the same request_id), then finalize. + * Finalizing a job with no items is a 400.

+ */ + public BatchJob finalize(String jobId, RequestOptions requestOptions) { + return this.rawClient.finalize(jobId, requestOptions).body(); + } + + /** + * Drives a job to the terminal canceled state on request. Takes no + * request body. + *

Cancel does not delete the job: the job record and any results already + * produced are preserved for the normal retention window, the same as a + * completed or failed job. Items stop being processed and keep the state + * they held at cancellation, so a canceled job's counts may show + * unfinished items that never resolve.

+ *

Cancel is idempotent: canceling an already-canceled job returns 200 + * with the job. Canceling a job that has already completed or failed is + * a 409.

+ */ + public BatchJob cancel(String jobId) { + return this.rawClient.cancel(jobId).body(); + } + + /** + * Drives a job to the terminal canceled state on request. Takes no + * request body. + *

Cancel does not delete the job: the job record and any results already + * produced are preserved for the normal retention window, the same as a + * completed or failed job. Items stop being processed and keep the state + * they held at cancellation, so a canceled job's counts may show + * unfinished items that never resolve.

+ *

Cancel is idempotent: canceling an already-canceled job returns 200 + * with the job. Canceling a job that has already completed or failed is + * a 409.

+ */ + public BatchJob cancel(String jobId, RequestOptions requestOptions) { + return this.rawClient.cancel(jobId, requestOptions).body(); + } + + /** + * Returns a job's record, its per-status item counts, and one page of + * per-item statuses. + *

Items are listed in a stable order that is not upload order and is the + * same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public JobDetailResponse get(String jobId) { + return this.rawClient.get(jobId).body(); + } + + /** + * Returns a job's record, its per-status item counts, and one page of + * per-item statuses. + *

Items are listed in a stable order that is not upload order and is the + * same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public JobDetailResponse get(String jobId, RequestOptions requestOptions) { + return this.rawClient.get(jobId, requestOptions).body(); + } + + /** + * Returns a job's record, its per-status item counts, and one page of + * per-item statuses. + *

Items are listed in a stable order that is not upload order and is the + * same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public JobDetailResponse get(String jobId, GetRequest request) { + return this.rawClient.get(jobId, request).body(); + } + + /** + * Returns a job's record, its per-status item counts, and one page of + * per-item statuses. + *

Items are listed in a stable order that is not upload order and is the + * same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public JobDetailResponse get(String jobId, GetRequest request, RequestOptions requestOptions) { + return this.rawClient.get(jobId, request, requestOptions).body(); + } + + /** + * A lighter status page. Returns the same per-item status entries as + * GET /lang2fhir/batch/{job_id}, but without the job record or counts, + * and the entries carry result_size rather than any result content. Use + * each entry's item_id to fetch that item's result from + * GET /lang2fhir/batch/{job_id}/results/{item_id}. + *

Entries are listed in a stable order that is not upload order and is + * the same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public ResultsPageResponse getResults(String jobId) { + return this.rawClient.getResults(jobId).body(); + } + + /** + * A lighter status page. Returns the same per-item status entries as + * GET /lang2fhir/batch/{job_id}, but without the job record or counts, + * and the entries carry result_size rather than any result content. Use + * each entry's item_id to fetch that item's result from + * GET /lang2fhir/batch/{job_id}/results/{item_id}. + *

Entries are listed in a stable order that is not upload order and is + * the same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public ResultsPageResponse getResults(String jobId, RequestOptions requestOptions) { + return this.rawClient.getResults(jobId, requestOptions).body(); + } + + /** + * A lighter status page. Returns the same per-item status entries as + * GET /lang2fhir/batch/{job_id}, but without the job record or counts, + * and the entries carry result_size rather than any result content. Use + * each entry's item_id to fetch that item's result from + * GET /lang2fhir/batch/{job_id}/results/{item_id}. + *

Entries are listed in a stable order that is not upload order and is + * the same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public ResultsPageResponse getResults(String jobId, GetResultsRequest request) { + return this.rawClient.getResults(jobId, request).body(); + } + + /** + * A lighter status page. Returns the same per-item status entries as + * GET /lang2fhir/batch/{job_id}, but without the job record or counts, + * and the entries carry result_size rather than any result content. Use + * each entry's item_id to fetch that item's result from + * GET /lang2fhir/batch/{job_id}/results/{item_id}. + *

Entries are listed in a stable order that is not upload order and is + * the same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public ResultsPageResponse getResults(String jobId, GetResultsRequest request, RequestOptions requestOptions) { + return this.rawClient.getResults(jobId, request, requestOptions).body(); + } + + /** + * Streams one item's stored result bytes verbatim as application/json. + * The body is the response the item's synchronous multi endpoint would have + * returned — a DocumentMultiResponse for a document item or a + * CreateMultiResponse for a create item. + *

Only a succeeded item has a result: an item that has not succeeded + * (pending, processing, or failed) is a 409, and a result that has + * expired is a 404.

+ */ + public Map getResult(String jobId, String itemId) { + return this.rawClient.getResult(jobId, itemId).body(); + } + + /** + * Streams one item's stored result bytes verbatim as application/json. + * The body is the response the item's synchronous multi endpoint would have + * returned — a DocumentMultiResponse for a document item or a + * CreateMultiResponse for a create item. + *

Only a succeeded item has a result: an item that has not succeeded + * (pending, processing, or failed) is a 409, and a result that has + * expired is a 404.

+ */ + public Map getResult(String jobId, String itemId, RequestOptions requestOptions) { + return this.rawClient.getResult(jobId, itemId, requestOptions).body(); + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/RawLang2FhirBatchClient.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/RawLang2FhirBatchClient.java new file mode 100644 index 00000000..56b62f05 --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/RawLang2FhirBatchClient.java @@ -0,0 +1,1413 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.phenoml.api.core.ClientOptions; +import com.phenoml.api.core.FileStream; +import com.phenoml.api.core.MediaTypes; +import com.phenoml.api.core.ObjectMappers; +import com.phenoml.api.core.PhenomlClientApiException; +import com.phenoml.api.core.PhenomlClientException; +import com.phenoml.api.core.PhenomlClientHttpResponse; +import com.phenoml.api.core.QueryStringMapper; +import com.phenoml.api.core.RequestOptions; +import com.phenoml.api.core.RetryInterceptor; +import com.phenoml.api.resources.lang2fhirbatch.errors.BadRequestError; +import com.phenoml.api.resources.lang2fhirbatch.errors.ClientClosedRequestError; +import com.phenoml.api.resources.lang2fhirbatch.errors.ConflictError; +import com.phenoml.api.resources.lang2fhirbatch.errors.ContentTooLargeError; +import com.phenoml.api.resources.lang2fhirbatch.errors.GatewayTimeoutError; +import com.phenoml.api.resources.lang2fhirbatch.errors.InternalServerError; +import com.phenoml.api.resources.lang2fhirbatch.errors.NotFoundError; +import com.phenoml.api.resources.lang2fhirbatch.errors.UnauthorizedError; +import com.phenoml.api.resources.lang2fhirbatch.requests.CreateBatchRequest; +import com.phenoml.api.resources.lang2fhirbatch.requests.GetRequest; +import com.phenoml.api.resources.lang2fhirbatch.requests.GetResultsRequest; +import com.phenoml.api.resources.lang2fhirbatch.requests.ListRequest; +import com.phenoml.api.resources.lang2fhirbatch.requests.UploadItemRequest; +import com.phenoml.api.resources.lang2fhirbatch.types.BatchJob; +import com.phenoml.api.resources.lang2fhirbatch.types.JobDetailResponse; +import com.phenoml.api.resources.lang2fhirbatch.types.JobListResponse; +import com.phenoml.api.resources.lang2fhirbatch.types.ResultsPageResponse; +import com.phenoml.api.resources.lang2fhirbatch.types.UploadItemResponse; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.Map; +import java.util.Optional; +import okhttp3.Headers; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; + +public class RawLang2FhirBatchClient { + protected final ClientOptions clientOptions; + + public RawLang2FhirBatchClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + } + + /** + * Returns a page of the instance's batch jobs, newest first, without + * per-job counts. Jobs are shared across the instance's credentials, so + * this lists every batch job on the instance, not just the calling + * credential's. + */ + public PhenomlClientHttpResponse list() { + return list(ListRequest.builder().build()); + } + + /** + * Returns a page of the instance's batch jobs, newest first, without + * per-job counts. Jobs are shared across the instance's credentials, so + * this lists every batch job on the instance, not just the calling + * credential's. + */ + public PhenomlClientHttpResponse list(RequestOptions requestOptions) { + return list(ListRequest.builder().build(), requestOptions); + } + + /** + * Returns a page of the instance's batch jobs, newest first, without + * per-job counts. Jobs are shared across the instance's credentials, so + * this lists every batch job on the instance, not just the calling + * credential's. + */ + public PhenomlClientHttpResponse list(ListRequest request) { + return list(request, null); + } + + /** + * Returns a page of the instance's batch jobs, newest first, without + * per-job counts. Jobs are shared across the instance's credentials, so + * this lists every batch job on the instance, not just the calling + * credential's. + */ + public PhenomlClientHttpResponse list(ListRequest request, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch"); + if (request.getCursor().isPresent()) { + QueryStringMapper.addQueryParameter( + httpUrl, "cursor", request.getCursor().get(), false); + } + if (request.getLimit().isPresent()) { + QueryStringMapper.addQueryParameter( + httpUrl, "limit", request.getLimit().get(), false); + } + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request.Builder _requestBuilder = new Request.Builder() + .url(httpUrl.build()) + .method("GET", null) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json"); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, JobListResponse.class), response); + } + try { + switch (response.code()) { + case 400: + throw new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 401: + throw new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 499: + throw new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 500: + throw new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 504: + throw new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new PhenomlClientException("Network error executing HTTP request", e); + } + } + + /** + * Opens an empty batch job. Items arrive on later upload calls and the set + * is sealed at finalize. + *

Supplying request_id makes the create idempotent on that token: a + * retried submit whose response was lost returns the original job rather + * than opening a second one. This dedupe is scoped to the calling + * credential. A request_id whose job was canceled or failed before it + * finalized is released for a fresh replay; once a job is finalized, its + * request_id keeps resolving to it even after cancellation.

+ *

There is no limit on how many jobs an instance may hold at once; how many + * items run in parallel is a property of the instance, not of the job count.

+ */ + public PhenomlClientHttpResponse create() { + return create(CreateBatchRequest.builder().build()); + } + + /** + * Opens an empty batch job. Items arrive on later upload calls and the set + * is sealed at finalize. + *

Supplying request_id makes the create idempotent on that token: a + * retried submit whose response was lost returns the original job rather + * than opening a second one. This dedupe is scoped to the calling + * credential. A request_id whose job was canceled or failed before it + * finalized is released for a fresh replay; once a job is finalized, its + * request_id keeps resolving to it even after cancellation.

+ *

There is no limit on how many jobs an instance may hold at once; how many + * items run in parallel is a property of the instance, not of the job count.

+ */ + public PhenomlClientHttpResponse create(RequestOptions requestOptions) { + return create(CreateBatchRequest.builder().build(), requestOptions); + } + + /** + * Opens an empty batch job. Items arrive on later upload calls and the set + * is sealed at finalize. + *

Supplying request_id makes the create idempotent on that token: a + * retried submit whose response was lost returns the original job rather + * than opening a second one. This dedupe is scoped to the calling + * credential. A request_id whose job was canceled or failed before it + * finalized is released for a fresh replay; once a job is finalized, its + * request_id keeps resolving to it even after cancellation.

+ *

There is no limit on how many jobs an instance may hold at once; how many + * items run in parallel is a property of the instance, not of the job count.

+ */ + public PhenomlClientHttpResponse create(CreateBatchRequest request) { + return create(request, null); + } + + /** + * Opens an empty batch job. Items arrive on later upload calls and the set + * is sealed at finalize. + *

Supplying request_id makes the create idempotent on that token: a + * retried submit whose response was lost returns the original job rather + * than opening a second one. This dedupe is scoped to the calling + * credential. A request_id whose job was canceled or failed before it + * finalized is released for a fresh replay; once a job is finalized, its + * request_id keeps resolving to it even after cancellation.

+ *

There is no limit on how many jobs an instance may hold at once; how many + * items run in parallel is a property of the instance, not of the job count.

+ */ + public PhenomlClientHttpResponse create(CreateBatchRequest request, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, BatchJob.class), response); + } + try { + switch (response.code()) { + case 400: + throw new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 401: + throw new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 499: + throw new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 500: + throw new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 504: + throw new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new PhenomlClientException("Network error executing HTTP request", e); + } + } + + /** + * Stores one item of a job from a multipart upload. A batch's items arrive + * one per request. The item carries either a document extraction + * (whose input file rides as raw bytes in the file part) or a + * create extraction (JSON only, no file). + *

The upload enforces these rules:

+ *
    + *
  • Set exactly one of document or create. Setting both, or + * neither, is a 400.
  • + *
  • When document is set, file is required — it supplies the + * document's file content (PDF, image, RTF, or XML/C-CDA).
  • + *
  • When create is set, file is forbidden — a create item carries + * no file.
  • + *
  • document and create must each be a JSON object.
  • + *
+ *

Only the item's structure is checked here: the fields inside document + * or create are not validated at upload. A body that is well-formed JSON + * but not a valid request for its endpoint is still accepted with 202 + * and fails later during processing, recorded as an item error. A + * wrong-typed field the endpoint cannot decode fails as invalid_input; a + * body that decodes but the pipeline rejects (for example, a missing + * required field) fails as processing_failed.

+ *

Supplying request_id makes the upload idempotent on that token. A + * re-upload under the same token overwrites the same item rather than + * adding a second, so a client that lost an upload's response can safely + * re-send it. The response's deduplicated is true only when the + * re-uploaded payload matches the one already stored; a same-token upload + * with a changed payload overwrites in place and returns false.

+ *

Set a request_id on every upload: re-sending under the same token + * is the only way to repair a lost or incomplete upload, including the one + * a finalize 409 reports. Without one, a re-send adds a new item instead + * of replacing the missing one, and the job cannot be finalized.

+ *

Uploads are rejected once the job has been finalized (409), once it + * holds its 500-item limit (409), or when the item is too large (413 — + * see the raw-file limit in the API description).

+ */ + public PhenomlClientHttpResponse uploadItem(String jobId, Optional file) { + return uploadItem(jobId, file, UploadItemRequest.builder().build()); + } + + /** + * Stores one item of a job from a multipart upload. A batch's items arrive + * one per request. The item carries either a document extraction + * (whose input file rides as raw bytes in the file part) or a + * create extraction (JSON only, no file). + *

The upload enforces these rules:

+ *
    + *
  • Set exactly one of document or create. Setting both, or + * neither, is a 400.
  • + *
  • When document is set, file is required — it supplies the + * document's file content (PDF, image, RTF, or XML/C-CDA).
  • + *
  • When create is set, file is forbidden — a create item carries + * no file.
  • + *
  • document and create must each be a JSON object.
  • + *
+ *

Only the item's structure is checked here: the fields inside document + * or create are not validated at upload. A body that is well-formed JSON + * but not a valid request for its endpoint is still accepted with 202 + * and fails later during processing, recorded as an item error. A + * wrong-typed field the endpoint cannot decode fails as invalid_input; a + * body that decodes but the pipeline rejects (for example, a missing + * required field) fails as processing_failed.

+ *

Supplying request_id makes the upload idempotent on that token. A + * re-upload under the same token overwrites the same item rather than + * adding a second, so a client that lost an upload's response can safely + * re-send it. The response's deduplicated is true only when the + * re-uploaded payload matches the one already stored; a same-token upload + * with a changed payload overwrites in place and returns false.

+ *

Set a request_id on every upload: re-sending under the same token + * is the only way to repair a lost or incomplete upload, including the one + * a finalize 409 reports. Without one, a re-send adds a new item instead + * of replacing the missing one, and the job cannot be finalized.

+ *

Uploads are rejected once the job has been finalized (409), once it + * holds its 500-item limit (409), or when the item is too large (413 — + * see the raw-file limit in the API description).

+ */ + public PhenomlClientHttpResponse uploadItem( + String jobId, Optional file, RequestOptions requestOptions) { + return uploadItem(jobId, file, UploadItemRequest.builder().build(), requestOptions); + } + + /** + * Stores one item of a job from a multipart upload. A batch's items arrive + * one per request. The item carries either a document extraction + * (whose input file rides as raw bytes in the file part) or a + * create extraction (JSON only, no file). + *

The upload enforces these rules:

+ *
    + *
  • Set exactly one of document or create. Setting both, or + * neither, is a 400.
  • + *
  • When document is set, file is required — it supplies the + * document's file content (PDF, image, RTF, or XML/C-CDA).
  • + *
  • When create is set, file is forbidden — a create item carries + * no file.
  • + *
  • document and create must each be a JSON object.
  • + *
+ *

Only the item's structure is checked here: the fields inside document + * or create are not validated at upload. A body that is well-formed JSON + * but not a valid request for its endpoint is still accepted with 202 + * and fails later during processing, recorded as an item error. A + * wrong-typed field the endpoint cannot decode fails as invalid_input; a + * body that decodes but the pipeline rejects (for example, a missing + * required field) fails as processing_failed.

+ *

Supplying request_id makes the upload idempotent on that token. A + * re-upload under the same token overwrites the same item rather than + * adding a second, so a client that lost an upload's response can safely + * re-send it. The response's deduplicated is true only when the + * re-uploaded payload matches the one already stored; a same-token upload + * with a changed payload overwrites in place and returns false.

+ *

Set a request_id on every upload: re-sending under the same token + * is the only way to repair a lost or incomplete upload, including the one + * a finalize 409 reports. Without one, a re-send adds a new item instead + * of replacing the missing one, and the job cannot be finalized.

+ *

Uploads are rejected once the job has been finalized (409), once it + * holds its 500-item limit (409), or when the item is too large (413 — + * see the raw-file limit in the API description).

+ */ + public PhenomlClientHttpResponse uploadItem( + String jobId, Optional file, UploadItemRequest request) { + return uploadItem(jobId, file, request, null); + } + + /** + * Stores one item of a job from a multipart upload. A batch's items arrive + * one per request. The item carries either a document extraction + * (whose input file rides as raw bytes in the file part) or a + * create extraction (JSON only, no file). + *

The upload enforces these rules:

+ *
    + *
  • Set exactly one of document or create. Setting both, or + * neither, is a 400.
  • + *
  • When document is set, file is required — it supplies the + * document's file content (PDF, image, RTF, or XML/C-CDA).
  • + *
  • When create is set, file is forbidden — a create item carries + * no file.
  • + *
  • document and create must each be a JSON object.
  • + *
+ *

Only the item's structure is checked here: the fields inside document + * or create are not validated at upload. A body that is well-formed JSON + * but not a valid request for its endpoint is still accepted with 202 + * and fails later during processing, recorded as an item error. A + * wrong-typed field the endpoint cannot decode fails as invalid_input; a + * body that decodes but the pipeline rejects (for example, a missing + * required field) fails as processing_failed.

+ *

Supplying request_id makes the upload idempotent on that token. A + * re-upload under the same token overwrites the same item rather than + * adding a second, so a client that lost an upload's response can safely + * re-send it. The response's deduplicated is true only when the + * re-uploaded payload matches the one already stored; a same-token upload + * with a changed payload overwrites in place and returns false.

+ *

Set a request_id on every upload: re-sending under the same token + * is the only way to repair a lost or incomplete upload, including the one + * a finalize 409 reports. Without one, a re-send adds a new item instead + * of replacing the missing one, and the job cannot be finalized.

+ *

Uploads are rejected once the job has been finalized (409), once it + * holds its 500-item limit (409), or when the item is too large (413 — + * see the raw-file limit in the API description).

+ */ + public PhenomlClientHttpResponse uploadItem( + String jobId, Optional file, UploadItemRequest request, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch") + .addPathSegment(jobId) + .addPathSegments("items"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + try { + if (request.getDocument().isPresent()) { + multipartBodyBuilder.addFormDataPart( + "document", + ObjectMappers.JSON_MAPPER.writeValueAsString( + request.getDocument().get())); + } + if (request.getCreate().isPresent()) { + multipartBodyBuilder.addFormDataPart( + "create", + ObjectMappers.JSON_MAPPER.writeValueAsString( + request.getCreate().get())); + } + if (file.isPresent()) { + String fileMimeType = Files.probeContentType(file.get().toPath()); + MediaType fileMimeTypeMediaType = fileMimeType != null ? MediaType.parse(fileMimeType) : null; + multipartBodyBuilder.addFormDataPart( + "file", file.get().getName(), RequestBody.create(file.get(), fileMimeTypeMediaType)); + } + if (request.getRequestId().isPresent()) { + multipartBodyBuilder.addFormDataPart( + "request_id", request.getRequestId().get()); + } + if (request.getId().isPresent()) { + multipartBodyBuilder.addFormDataPart("id", request.getId().get()); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + Request.Builder _requestBuilder = new Request.Builder() + .url(httpUrl.build()) + .method("POST", multipartBodyBuilder.build()) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json"); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, UploadItemResponse.class), response); + } + try { + switch (response.code()) { + case 400: + throw new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 401: + throw new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 404: + throw new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 409: + throw new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 413: + throw new ContentTooLargeError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 499: + throw new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 500: + throw new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 504: + throw new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new PhenomlClientException("Network error executing HTTP request", e); + } + } + + public PhenomlClientHttpResponse uploadItem( + String jobId, Optional file, InputStream stream, String filename) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch") + .addPathSegment(jobId) + .addPathSegments("items"); + FileStream fs = new FileStream(stream, filename, null); + MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + multipartBodyBuilder.addFormDataPart("file", filename, fs.toRequestBody()); + RequestBody body = multipartBodyBuilder.build(); + Request.Builder _requestBuilder = new Request.Builder(); + _requestBuilder.url(httpUrl.build()); + _requestBuilder.method("POST", body); + _requestBuilder.headers(Headers.of(this.clientOptions.headers((RequestOptions) null))); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, UploadItemResponse.class), response); + } + try { + switch (response.code()) { + case 400: + throw new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 401: + throw new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 404: + throw new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 409: + throw new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 413: + throw new ContentTooLargeError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 499: + throw new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 500: + throw new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 504: + throw new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new PhenomlClientException("Network error executing HTTP request", e); + } + } + + public PhenomlClientHttpResponse uploadItem( + String jobId, Optional file, InputStream stream, String filename, MediaType mediaType) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch") + .addPathSegment(jobId) + .addPathSegments("items"); + FileStream fs = new FileStream(stream, filename, mediaType); + MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + multipartBodyBuilder.addFormDataPart("file", filename, fs.toRequestBody()); + RequestBody body = multipartBodyBuilder.build(); + Request.Builder _requestBuilder = new Request.Builder(); + _requestBuilder.url(httpUrl.build()); + _requestBuilder.method("POST", body); + _requestBuilder.headers(Headers.of(this.clientOptions.headers((RequestOptions) null))); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, UploadItemResponse.class), response); + } + try { + switch (response.code()) { + case 400: + throw new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 401: + throw new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 404: + throw new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 409: + throw new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 413: + throw new ContentTooLargeError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 499: + throw new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 500: + throw new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 504: + throw new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new PhenomlClientException("Network error executing HTTP request", e); + } + } + + public PhenomlClientHttpResponse uploadItem( + String jobId, Optional file, InputStream stream, String filename, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch") + .addPathSegment(jobId) + .addPathSegments("items"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + FileStream fs = new FileStream(stream, filename, null); + MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + multipartBodyBuilder.addFormDataPart("file", filename, fs.toRequestBody()); + RequestBody body = multipartBodyBuilder.build(); + Request.Builder _requestBuilder = new Request.Builder(); + _requestBuilder.url(httpUrl.build()); + _requestBuilder.method("POST", body); + _requestBuilder.headers(Headers.of(this.clientOptions.headers(requestOptions))); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, UploadItemResponse.class), response); + } + try { + switch (response.code()) { + case 400: + throw new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 401: + throw new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 404: + throw new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 409: + throw new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 413: + throw new ContentTooLargeError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 499: + throw new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 500: + throw new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 504: + throw new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new PhenomlClientException("Network error executing HTTP request", e); + } + } + + public PhenomlClientHttpResponse uploadItem( + String jobId, + Optional file, + InputStream stream, + String filename, + MediaType mediaType, + RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch") + .addPathSegment(jobId) + .addPathSegments("items"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + FileStream fs = new FileStream(stream, filename, mediaType); + MultipartBody.Builder multipartBodyBuilder = new MultipartBody.Builder().setType(MultipartBody.FORM); + multipartBodyBuilder.addFormDataPart("file", filename, fs.toRequestBody()); + RequestBody body = multipartBodyBuilder.build(); + Request.Builder _requestBuilder = new Request.Builder(); + _requestBuilder.url(httpUrl.build()); + _requestBuilder.method("POST", body); + _requestBuilder.headers(Headers.of(this.clientOptions.headers(requestOptions))); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, UploadItemResponse.class), response); + } + try { + switch (response.code()) { + case 400: + throw new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 401: + throw new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 404: + throw new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 409: + throw new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 413: + throw new ContentTooLargeError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 499: + throw new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 500: + throw new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 504: + throw new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new PhenomlClientException("Network error executing HTTP request", e); + } + } + + /** + * Seals the job's item set and starts processing. Takes no request body. + * Finalize is idempotent: a retried finalize succeeds again. + *

If a previous upload did not complete, finalize returns a 409; re-send + * the missing upload (with the same request_id), then finalize. + * Finalizing a job with no items is a 400.

+ */ + public PhenomlClientHttpResponse finalize(String jobId) { + return finalize(jobId, null); + } + + /** + * Seals the job's item set and starts processing. Takes no request body. + * Finalize is idempotent: a retried finalize succeeds again. + *

If a previous upload did not complete, finalize returns a 409; re-send + * the missing upload (with the same request_id), then finalize. + * Finalizing a job with no items is a 400.

+ */ + public PhenomlClientHttpResponse finalize(String jobId, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch") + .addPathSegment(jobId) + .addPathSegments("finalize"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("POST", RequestBody.create("", null)) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, BatchJob.class), response); + } + try { + switch (response.code()) { + case 400: + throw new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 401: + throw new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 404: + throw new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 409: + throw new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 499: + throw new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 500: + throw new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 504: + throw new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new PhenomlClientException("Network error executing HTTP request", e); + } + } + + /** + * Drives a job to the terminal canceled state on request. Takes no + * request body. + *

Cancel does not delete the job: the job record and any results already + * produced are preserved for the normal retention window, the same as a + * completed or failed job. Items stop being processed and keep the state + * they held at cancellation, so a canceled job's counts may show + * unfinished items that never resolve.

+ *

Cancel is idempotent: canceling an already-canceled job returns 200 + * with the job. Canceling a job that has already completed or failed is + * a 409.

+ */ + public PhenomlClientHttpResponse cancel(String jobId) { + return cancel(jobId, null); + } + + /** + * Drives a job to the terminal canceled state on request. Takes no + * request body. + *

Cancel does not delete the job: the job record and any results already + * produced are preserved for the normal retention window, the same as a + * completed or failed job. Items stop being processed and keep the state + * they held at cancellation, so a canceled job's counts may show + * unfinished items that never resolve.

+ *

Cancel is idempotent: canceling an already-canceled job returns 200 + * with the job. Canceling a job that has already completed or failed is + * a 409.

+ */ + public PhenomlClientHttpResponse cancel(String jobId, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch") + .addPathSegment(jobId) + .addPathSegments("cancel"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("POST", RequestBody.create("", null)) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, BatchJob.class), response); + } + try { + switch (response.code()) { + case 401: + throw new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 404: + throw new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 409: + throw new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 499: + throw new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 500: + throw new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 504: + throw new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new PhenomlClientException("Network error executing HTTP request", e); + } + } + + /** + * Returns a job's record, its per-status item counts, and one page of + * per-item statuses. + *

Items are listed in a stable order that is not upload order and is the + * same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public PhenomlClientHttpResponse get(String jobId) { + return get(jobId, GetRequest.builder().build()); + } + + /** + * Returns a job's record, its per-status item counts, and one page of + * per-item statuses. + *

Items are listed in a stable order that is not upload order and is the + * same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public PhenomlClientHttpResponse get(String jobId, RequestOptions requestOptions) { + return get(jobId, GetRequest.builder().build(), requestOptions); + } + + /** + * Returns a job's record, its per-status item counts, and one page of + * per-item statuses. + *

Items are listed in a stable order that is not upload order and is the + * same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public PhenomlClientHttpResponse get(String jobId, GetRequest request) { + return get(jobId, request, null); + } + + /** + * Returns a job's record, its per-status item counts, and one page of + * per-item statuses. + *

Items are listed in a stable order that is not upload order and is the + * same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public PhenomlClientHttpResponse get( + String jobId, GetRequest request, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch") + .addPathSegment(jobId); + if (request.getCursor().isPresent()) { + QueryStringMapper.addQueryParameter( + httpUrl, "cursor", request.getCursor().get(), false); + } + if (request.getLimit().isPresent()) { + QueryStringMapper.addQueryParameter( + httpUrl, "limit", request.getLimit().get(), false); + } + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request.Builder _requestBuilder = new Request.Builder() + .url(httpUrl.build()) + .method("GET", null) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json"); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, JobDetailResponse.class), response); + } + try { + switch (response.code()) { + case 400: + throw new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 401: + throw new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 404: + throw new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 499: + throw new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 500: + throw new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 504: + throw new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new PhenomlClientException("Network error executing HTTP request", e); + } + } + + /** + * A lighter status page. Returns the same per-item status entries as + * GET /lang2fhir/batch/{job_id}, but without the job record or counts, + * and the entries carry result_size rather than any result content. Use + * each entry's item_id to fetch that item's result from + * GET /lang2fhir/batch/{job_id}/results/{item_id}. + *

Entries are listed in a stable order that is not upload order and is + * the same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public PhenomlClientHttpResponse getResults(String jobId) { + return getResults(jobId, GetResultsRequest.builder().build()); + } + + /** + * A lighter status page. Returns the same per-item status entries as + * GET /lang2fhir/batch/{job_id}, but without the job record or counts, + * and the entries carry result_size rather than any result content. Use + * each entry's item_id to fetch that item's result from + * GET /lang2fhir/batch/{job_id}/results/{item_id}. + *

Entries are listed in a stable order that is not upload order and is + * the same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public PhenomlClientHttpResponse getResults(String jobId, RequestOptions requestOptions) { + return getResults(jobId, GetResultsRequest.builder().build(), requestOptions); + } + + /** + * A lighter status page. Returns the same per-item status entries as + * GET /lang2fhir/batch/{job_id}, but without the job record or counts, + * and the entries carry result_size rather than any result content. Use + * each entry's item_id to fetch that item's result from + * GET /lang2fhir/batch/{job_id}/results/{item_id}. + *

Entries are listed in a stable order that is not upload order and is + * the same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public PhenomlClientHttpResponse getResults(String jobId, GetResultsRequest request) { + return getResults(jobId, request, null); + } + + /** + * A lighter status page. Returns the same per-item status entries as + * GET /lang2fhir/batch/{job_id}, but without the job record or counts, + * and the entries carry result_size rather than any result content. Use + * each entry's item_id to fetch that item's result from + * GET /lang2fhir/batch/{job_id}/results/{item_id}. + *

Entries are listed in a stable order that is not upload order and is + * the same across pages. Match each entry to your own records by its id + * (your correlation label) or item_id (from the upload response), + * never by position.

+ */ + public PhenomlClientHttpResponse getResults( + String jobId, GetResultsRequest request, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch") + .addPathSegment(jobId) + .addPathSegments("results"); + if (request.getCursor().isPresent()) { + QueryStringMapper.addQueryParameter( + httpUrl, "cursor", request.getCursor().get(), false); + } + if (request.getLimit().isPresent()) { + QueryStringMapper.addQueryParameter( + httpUrl, "limit", request.getLimit().get(), false); + } + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request.Builder _requestBuilder = new Request.Builder() + .url(httpUrl.build()) + .method("GET", null) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json"); + Request okhttpRequest = _requestBuilder.build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, ResultsPageResponse.class), response); + } + try { + switch (response.code()) { + case 400: + throw new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 401: + throw new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 404: + throw new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 499: + throw new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 500: + throw new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 504: + throw new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new PhenomlClientException("Network error executing HTTP request", e); + } + } + + /** + * Streams one item's stored result bytes verbatim as application/json. + * The body is the response the item's synchronous multi endpoint would have + * returned — a DocumentMultiResponse for a document item or a + * CreateMultiResponse for a create item. + *

Only a succeeded item has a result: an item that has not succeeded + * (pending, processing, or failed) is a 409, and a result that has + * expired is a 404.

+ */ + public PhenomlClientHttpResponse> getResult(String jobId, String itemId) { + return getResult(jobId, itemId, null); + } + + /** + * Streams one item's stored result bytes verbatim as application/json. + * The body is the response the item's synchronous multi endpoint would have + * returned — a DocumentMultiResponse for a document item or a + * CreateMultiResponse for a create item. + *

Only a succeeded item has a result: an item that has not succeeded + * (pending, processing, or failed) is a 409, and a result that has + * expired is a 404.

+ */ + public PhenomlClientHttpResponse> getResult( + String jobId, String itemId, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("lang2fhir/batch") + .addPathSegment(jobId) + .addPathSegments("results") + .addPathSegment(itemId); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("GET", null) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue( + responseBodyString, new TypeReference>() {}), + response); + } + try { + switch (response.code()) { + case 400: + throw new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 401: + throw new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 404: + throw new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 409: + throw new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 499: + throw new ClientClosedRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 500: + throw new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 504: + throw new GatewayTimeoutError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new PhenomlClientException("Network error executing HTTP request", e); + } + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/BadRequestError.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/BadRequestError.java new file mode 100644 index 00000000..546fdf27 --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/BadRequestError.java @@ -0,0 +1,32 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.errors; + +import com.phenoml.api.core.PhenomlClientApiException; +import okhttp3.Response; + +public final class BadRequestError extends PhenomlClientApiException { + /** + * The body of the response that triggered the exception. + */ + private final Object body; + + public BadRequestError(Object body) { + super("BadRequestError", 400, body); + this.body = body; + } + + public BadRequestError(Object body, Response rawResponse) { + super("BadRequestError", 400, body, rawResponse); + this.body = body; + } + + /** + * @return the body + */ + @java.lang.Override + public Object body() { + return this.body; + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/ClientClosedRequestError.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/ClientClosedRequestError.java new file mode 100644 index 00000000..b0aa1c4d --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/ClientClosedRequestError.java @@ -0,0 +1,32 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.errors; + +import com.phenoml.api.core.PhenomlClientApiException; +import okhttp3.Response; + +public final class ClientClosedRequestError extends PhenomlClientApiException { + /** + * The body of the response that triggered the exception. + */ + private final Object body; + + public ClientClosedRequestError(Object body) { + super("ClientClosedRequestError", 499, body); + this.body = body; + } + + public ClientClosedRequestError(Object body, Response rawResponse) { + super("ClientClosedRequestError", 499, body, rawResponse); + this.body = body; + } + + /** + * @return the body + */ + @java.lang.Override + public Object body() { + return this.body; + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/ConflictError.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/ConflictError.java new file mode 100644 index 00000000..f2fbb948 --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/ConflictError.java @@ -0,0 +1,32 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.errors; + +import com.phenoml.api.core.PhenomlClientApiException; +import okhttp3.Response; + +public final class ConflictError extends PhenomlClientApiException { + /** + * The body of the response that triggered the exception. + */ + private final Object body; + + public ConflictError(Object body) { + super("ConflictError", 409, body); + this.body = body; + } + + public ConflictError(Object body, Response rawResponse) { + super("ConflictError", 409, body, rawResponse); + this.body = body; + } + + /** + * @return the body + */ + @java.lang.Override + public Object body() { + return this.body; + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/ContentTooLargeError.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/ContentTooLargeError.java new file mode 100644 index 00000000..71290236 --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/ContentTooLargeError.java @@ -0,0 +1,32 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.errors; + +import com.phenoml.api.core.PhenomlClientApiException; +import okhttp3.Response; + +public final class ContentTooLargeError extends PhenomlClientApiException { + /** + * The body of the response that triggered the exception. + */ + private final Object body; + + public ContentTooLargeError(Object body) { + super("ContentTooLargeError", 413, body); + this.body = body; + } + + public ContentTooLargeError(Object body, Response rawResponse) { + super("ContentTooLargeError", 413, body, rawResponse); + this.body = body; + } + + /** + * @return the body + */ + @java.lang.Override + public Object body() { + return this.body; + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/GatewayTimeoutError.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/GatewayTimeoutError.java new file mode 100644 index 00000000..817bb014 --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/GatewayTimeoutError.java @@ -0,0 +1,32 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.errors; + +import com.phenoml.api.core.PhenomlClientApiException; +import okhttp3.Response; + +public final class GatewayTimeoutError extends PhenomlClientApiException { + /** + * The body of the response that triggered the exception. + */ + private final Object body; + + public GatewayTimeoutError(Object body) { + super("GatewayTimeoutError", 504, body); + this.body = body; + } + + public GatewayTimeoutError(Object body, Response rawResponse) { + super("GatewayTimeoutError", 504, body, rawResponse); + this.body = body; + } + + /** + * @return the body + */ + @java.lang.Override + public Object body() { + return this.body; + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/InternalServerError.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/InternalServerError.java new file mode 100644 index 00000000..d2caec54 --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/InternalServerError.java @@ -0,0 +1,32 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.errors; + +import com.phenoml.api.core.PhenomlClientApiException; +import okhttp3.Response; + +public final class InternalServerError extends PhenomlClientApiException { + /** + * The body of the response that triggered the exception. + */ + private final Object body; + + public InternalServerError(Object body) { + super("InternalServerError", 500, body); + this.body = body; + } + + public InternalServerError(Object body, Response rawResponse) { + super("InternalServerError", 500, body, rawResponse); + this.body = body; + } + + /** + * @return the body + */ + @java.lang.Override + public Object body() { + return this.body; + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/NotFoundError.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/NotFoundError.java new file mode 100644 index 00000000..2153ca5e --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/NotFoundError.java @@ -0,0 +1,32 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.errors; + +import com.phenoml.api.core.PhenomlClientApiException; +import okhttp3.Response; + +public final class NotFoundError extends PhenomlClientApiException { + /** + * The body of the response that triggered the exception. + */ + private final Object body; + + public NotFoundError(Object body) { + super("NotFoundError", 404, body); + this.body = body; + } + + public NotFoundError(Object body, Response rawResponse) { + super("NotFoundError", 404, body, rawResponse); + this.body = body; + } + + /** + * @return the body + */ + @java.lang.Override + public Object body() { + return this.body; + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/UnauthorizedError.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/UnauthorizedError.java new file mode 100644 index 00000000..c25bcdee --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/errors/UnauthorizedError.java @@ -0,0 +1,32 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.errors; + +import com.phenoml.api.core.PhenomlClientApiException; +import okhttp3.Response; + +public final class UnauthorizedError extends PhenomlClientApiException { + /** + * The body of the response that triggered the exception. + */ + private final Object body; + + public UnauthorizedError(Object body) { + super("UnauthorizedError", 401, body); + this.body = body; + } + + public UnauthorizedError(Object body, Response rawResponse) { + super("UnauthorizedError", 401, body, rawResponse); + this.body = body; + } + + /** + * @return the body + */ + @java.lang.Override + public Object body() { + return this.body; + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/requests/CreateBatchRequest.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/requests/CreateBatchRequest.java new file mode 100644 index 00000000..db47ac7f --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/requests/CreateBatchRequest.java @@ -0,0 +1,115 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.requests; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.phenoml.api.core.ObjectMappers; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = CreateBatchRequest.Builder.class) +public final class CreateBatchRequest { + private final Optional requestId; + + private final Map additionalProperties; + + private CreateBatchRequest(Optional requestId, Map additionalProperties) { + this.requestId = requestId; + this.additionalProperties = additionalProperties; + } + + /** + * @return Optional client idempotency token (at most 256 UTF-8 bytes). A + * retried create with the same token returns the original job instead + * of opening a second one. + */ + @JsonProperty("request_id") + public Optional getRequestId() { + return requestId; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof CreateBatchRequest && equalTo((CreateBatchRequest) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(CreateBatchRequest other) { + return requestId.equals(other.requestId); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.requestId); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional requestId = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(CreateBatchRequest other) { + requestId(other.getRequestId()); + return this; + } + + /** + *

Optional client idempotency token (at most 256 UTF-8 bytes). A + * retried create with the same token returns the original job instead + * of opening a second one.

+ */ + @JsonSetter(value = "request_id", nulls = Nulls.SKIP) + public Builder requestId(Optional requestId) { + this.requestId = requestId; + return this; + } + + public Builder requestId(String requestId) { + this.requestId = Optional.ofNullable(requestId); + return this; + } + + public CreateBatchRequest build() { + return new CreateBatchRequest(requestId, additionalProperties); + } + + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/requests/GetRequest.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/requests/GetRequest.java new file mode 100644 index 00000000..77ddc1b7 --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/requests/GetRequest.java @@ -0,0 +1,139 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.requests; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.phenoml.api.core.ObjectMappers; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = GetRequest.Builder.class) +public final class GetRequest { + private final Optional cursor; + + private final Optional limit; + + private final Map additionalProperties; + + private GetRequest(Optional cursor, Optional limit, Map additionalProperties) { + this.cursor = cursor; + this.limit = limit; + this.additionalProperties = additionalProperties; + } + + /** + * @return Opaque pagination cursor from a previous page's next_cursor. + */ + @JsonProperty("cursor") + public Optional getCursor() { + return cursor; + } + + /** + * @return Page size for the item-status page. Defaults to 20; values above 100 are clamped to 100. + */ + @JsonProperty("limit") + public Optional getLimit() { + return limit; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof GetRequest && equalTo((GetRequest) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(GetRequest other) { + return cursor.equals(other.cursor) && limit.equals(other.limit); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.cursor, this.limit); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional cursor = Optional.empty(); + + private Optional limit = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(GetRequest other) { + cursor(other.getCursor()); + limit(other.getLimit()); + return this; + } + + /** + *

Opaque pagination cursor from a previous page's next_cursor.

+ */ + @JsonSetter(value = "cursor", nulls = Nulls.SKIP) + public Builder cursor(Optional cursor) { + this.cursor = cursor; + return this; + } + + public Builder cursor(String cursor) { + this.cursor = Optional.ofNullable(cursor); + return this; + } + + /** + *

Page size for the item-status page. Defaults to 20; values above 100 are clamped to 100.

+ */ + @JsonSetter(value = "limit", nulls = Nulls.SKIP) + public Builder limit(Optional limit) { + this.limit = limit; + return this; + } + + public Builder limit(Integer limit) { + this.limit = Optional.ofNullable(limit); + return this; + } + + public GetRequest build() { + return new GetRequest(cursor, limit, additionalProperties); + } + + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/requests/GetResultsRequest.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/requests/GetResultsRequest.java new file mode 100644 index 00000000..fa55df76 --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/requests/GetResultsRequest.java @@ -0,0 +1,140 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.requests; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.phenoml.api.core.ObjectMappers; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = GetResultsRequest.Builder.class) +public final class GetResultsRequest { + private final Optional cursor; + + private final Optional limit; + + private final Map additionalProperties; + + private GetResultsRequest( + Optional cursor, Optional limit, Map additionalProperties) { + this.cursor = cursor; + this.limit = limit; + this.additionalProperties = additionalProperties; + } + + /** + * @return Opaque pagination cursor from a previous page's next_cursor. + */ + @JsonProperty("cursor") + public Optional getCursor() { + return cursor; + } + + /** + * @return Page size. Defaults to 20; values above 100 are clamped to 100. + */ + @JsonProperty("limit") + public Optional getLimit() { + return limit; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof GetResultsRequest && equalTo((GetResultsRequest) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(GetResultsRequest other) { + return cursor.equals(other.cursor) && limit.equals(other.limit); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.cursor, this.limit); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional cursor = Optional.empty(); + + private Optional limit = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(GetResultsRequest other) { + cursor(other.getCursor()); + limit(other.getLimit()); + return this; + } + + /** + *

Opaque pagination cursor from a previous page's next_cursor.

+ */ + @JsonSetter(value = "cursor", nulls = Nulls.SKIP) + public Builder cursor(Optional cursor) { + this.cursor = cursor; + return this; + } + + public Builder cursor(String cursor) { + this.cursor = Optional.ofNullable(cursor); + return this; + } + + /** + *

Page size. Defaults to 20; values above 100 are clamped to 100.

+ */ + @JsonSetter(value = "limit", nulls = Nulls.SKIP) + public Builder limit(Optional limit) { + this.limit = limit; + return this; + } + + public Builder limit(Integer limit) { + this.limit = Optional.ofNullable(limit); + return this; + } + + public GetResultsRequest build() { + return new GetResultsRequest(cursor, limit, additionalProperties); + } + + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/requests/ListRequest.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/requests/ListRequest.java new file mode 100644 index 00000000..49e89c21 --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/requests/ListRequest.java @@ -0,0 +1,139 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.requests; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.phenoml.api.core.ObjectMappers; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = ListRequest.Builder.class) +public final class ListRequest { + private final Optional cursor; + + private final Optional limit; + + private final Map additionalProperties; + + private ListRequest(Optional cursor, Optional limit, Map additionalProperties) { + this.cursor = cursor; + this.limit = limit; + this.additionalProperties = additionalProperties; + } + + /** + * @return Opaque pagination cursor from a previous page's next_cursor. + */ + @JsonProperty("cursor") + public Optional getCursor() { + return cursor; + } + + /** + * @return Page size. Defaults to 20; values above 100 are clamped to 100. + */ + @JsonProperty("limit") + public Optional getLimit() { + return limit; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof ListRequest && equalTo((ListRequest) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(ListRequest other) { + return cursor.equals(other.cursor) && limit.equals(other.limit); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.cursor, this.limit); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional cursor = Optional.empty(); + + private Optional limit = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(ListRequest other) { + cursor(other.getCursor()); + limit(other.getLimit()); + return this; + } + + /** + *

Opaque pagination cursor from a previous page's next_cursor.

+ */ + @JsonSetter(value = "cursor", nulls = Nulls.SKIP) + public Builder cursor(Optional cursor) { + this.cursor = cursor; + return this; + } + + public Builder cursor(String cursor) { + this.cursor = Optional.ofNullable(cursor); + return this; + } + + /** + *

Page size. Defaults to 20; values above 100 are clamped to 100.

+ */ + @JsonSetter(value = "limit", nulls = Nulls.SKIP) + public Builder limit(Optional limit) { + this.limit = limit; + return this; + } + + public Builder limit(Integer limit) { + this.limit = Optional.ofNullable(limit); + return this; + } + + public ListRequest build() { + return new ListRequest(cursor, limit, additionalProperties); + } + + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/requests/UploadItemRequest.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/requests/UploadItemRequest.java new file mode 100644 index 00000000..e18358eb --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/requests/UploadItemRequest.java @@ -0,0 +1,247 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.requests; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.phenoml.api.core.ObjectMappers; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = UploadItemRequest.Builder.class) +public final class UploadItemRequest { + private final Optional> document; + + private final Optional> create; + + private final Optional requestId; + + private final Optional id; + + private final Map additionalProperties; + + private UploadItemRequest( + Optional> document, + Optional> create, + Optional requestId, + Optional id, + Map additionalProperties) { + this.document = document; + this.create = create; + this.requestId = requestId; + this.id = id; + this.additionalProperties = additionalProperties; + } + + /** + * @return The JSON body of POST /lang2fhir/document/multi, without + * its base64 content field — the uploaded file supplies the + * content. Accepts that endpoint's fields (version, provider, + * primary_patient, patient_reference (deprecated), implementation_guide, detection_effort, + * validation_method, config). This is the multi-resource + * body: it has no single-resource field, and the item's result + * is a DocumentMultiResponse (a Bundle of resources). Mutually + * exclusive with create; requires file. Do not combine + * primary_patient with patient_reference. + */ + @JsonProperty("document") + public Optional> getDocument() { + return document; + } + + /** + * @return The JSON body of POST /lang2fhir/create/multi. Accepts that + * endpoint's fields (text, version, provider, + * primary_patient, patient_reference (deprecated), implementation_guide, detection_effort, + * validation_method, resource_review). This is the + * multi-resource body: it has no single-resource field, and + * the item's result is a CreateMultiResponse (a Bundle of + * resources). Mutually exclusive with document; must not be + * accompanied by a file. Do not combine primary_patient with + * patient_reference. + */ + @JsonProperty("create") + public Optional> getCreate() { + return create; + } + + /** + * @return Optional idempotency token (at most 256 UTF-8 bytes). + * Re-uploading under the same token overwrites the same item + * instead of adding a new one. The token is scoped to this job; + * the same token in another job is independent and creates a + * separate item. + */ + @JsonProperty("request_id") + public Optional getRequestId() { + return requestId; + } + + /** + * @return Optional caller-supplied correlation label (at most 512 UTF-8 + * bytes), echoed back on status and result listings so you can + * match the server's item_id to your own record. + */ + @JsonProperty("id") + public Optional getId() { + return id; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof UploadItemRequest && equalTo((UploadItemRequest) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(UploadItemRequest other) { + return document.equals(other.document) + && create.equals(other.create) + && requestId.equals(other.requestId) + && id.equals(other.id); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.document, this.create, this.requestId, this.id); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private Optional> document = Optional.empty(); + + private Optional> create = Optional.empty(); + + private Optional requestId = Optional.empty(); + + private Optional id = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(UploadItemRequest other) { + document(other.getDocument()); + create(other.getCreate()); + requestId(other.getRequestId()); + id(other.getId()); + return this; + } + + /** + *

The JSON body of POST /lang2fhir/document/multi, without + * its base64 content field — the uploaded file supplies the + * content. Accepts that endpoint's fields (version, provider, + * primary_patient, patient_reference (deprecated), implementation_guide, detection_effort, + * validation_method, config). This is the multi-resource + * body: it has no single-resource field, and the item's result + * is a DocumentMultiResponse (a Bundle of resources). Mutually + * exclusive with create; requires file. Do not combine + * primary_patient with patient_reference.

+ */ + @JsonSetter(value = "document", nulls = Nulls.SKIP) + public Builder document(Optional> document) { + this.document = document; + return this; + } + + public Builder document(Map document) { + this.document = Optional.ofNullable(document); + return this; + } + + /** + *

The JSON body of POST /lang2fhir/create/multi. Accepts that + * endpoint's fields (text, version, provider, + * primary_patient, patient_reference (deprecated), implementation_guide, detection_effort, + * validation_method, resource_review). This is the + * multi-resource body: it has no single-resource field, and + * the item's result is a CreateMultiResponse (a Bundle of + * resources). Mutually exclusive with document; must not be + * accompanied by a file. Do not combine primary_patient with + * patient_reference.

+ */ + @JsonSetter(value = "create", nulls = Nulls.SKIP) + public Builder create(Optional> create) { + this.create = create; + return this; + } + + public Builder create(Map create) { + this.create = Optional.ofNullable(create); + return this; + } + + /** + *

Optional idempotency token (at most 256 UTF-8 bytes). + * Re-uploading under the same token overwrites the same item + * instead of adding a new one. The token is scoped to this job; + * the same token in another job is independent and creates a + * separate item.

+ */ + @JsonSetter(value = "request_id", nulls = Nulls.SKIP) + public Builder requestId(Optional requestId) { + this.requestId = requestId; + return this; + } + + public Builder requestId(String requestId) { + this.requestId = Optional.ofNullable(requestId); + return this; + } + + /** + *

Optional caller-supplied correlation label (at most 512 UTF-8 + * bytes), echoed back on status and result listings so you can + * match the server's item_id to your own record.

+ */ + @JsonSetter(value = "id", nulls = Nulls.SKIP) + public Builder id(Optional id) { + this.id = id; + return this; + } + + public Builder id(String id) { + this.id = Optional.ofNullable(id); + return this; + } + + public UploadItemRequest build() { + return new UploadItemRequest(document, create, requestId, id, additionalProperties); + } + + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/BatchCounts.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/BatchCounts.java new file mode 100644 index 00000000..c376f56c --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/BatchCounts.java @@ -0,0 +1,216 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.phenoml.api.core.ObjectMappers; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = BatchCounts.Builder.class) +public final class BatchCounts { + private final int total; + + private final int pending; + + private final int processing; + + private final int succeeded; + + private final int failed; + + private final Map additionalProperties; + + private BatchCounts( + int total, + int pending, + int processing, + int succeeded, + int failed, + Map additionalProperties) { + this.total = total; + this.pending = pending; + this.processing = processing; + this.succeeded = succeeded; + this.failed = failed; + this.additionalProperties = additionalProperties; + } + + @JsonProperty("total") + public int getTotal() { + return total; + } + + @JsonProperty("pending") + public int getPending() { + return pending; + } + + @JsonProperty("processing") + public int getProcessing() { + return processing; + } + + @JsonProperty("succeeded") + public int getSucceeded() { + return succeeded; + } + + @JsonProperty("failed") + public int getFailed() { + return failed; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof BatchCounts && equalTo((BatchCounts) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(BatchCounts other) { + return total == other.total + && pending == other.pending + && processing == other.processing + && succeeded == other.succeeded + && failed == other.failed; + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.total, this.pending, this.processing, this.succeeded, this.failed); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static TotalStage builder() { + return new Builder(); + } + + public interface TotalStage { + PendingStage total(int total); + + Builder from(BatchCounts other); + } + + public interface PendingStage { + ProcessingStage pending(int pending); + } + + public interface ProcessingStage { + SucceededStage processing(int processing); + } + + public interface SucceededStage { + FailedStage succeeded(int succeeded); + } + + public interface FailedStage { + _FinalStage failed(int failed); + } + + public interface _FinalStage { + BatchCounts build(); + + _FinalStage additionalProperty(String key, Object value); + + _FinalStage additionalProperties(Map additionalProperties); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder + implements TotalStage, PendingStage, ProcessingStage, SucceededStage, FailedStage, _FinalStage { + private int total; + + private int pending; + + private int processing; + + private int succeeded; + + private int failed; + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + @java.lang.Override + public Builder from(BatchCounts other) { + total(other.getTotal()); + pending(other.getPending()); + processing(other.getProcessing()); + succeeded(other.getSucceeded()); + failed(other.getFailed()); + return this; + } + + @java.lang.Override + @JsonSetter("total") + public PendingStage total(int total) { + this.total = total; + return this; + } + + @java.lang.Override + @JsonSetter("pending") + public ProcessingStage pending(int pending) { + this.pending = pending; + return this; + } + + @java.lang.Override + @JsonSetter("processing") + public SucceededStage processing(int processing) { + this.processing = processing; + return this; + } + + @java.lang.Override + @JsonSetter("succeeded") + public FailedStage succeeded(int succeeded) { + this.succeeded = succeeded; + return this; + } + + @java.lang.Override + @JsonSetter("failed") + public _FinalStage failed(int failed) { + this.failed = failed; + return this; + } + + @java.lang.Override + public BatchCounts build() { + return new BatchCounts(total, pending, processing, succeeded, failed, additionalProperties); + } + + @java.lang.Override + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + @java.lang.Override + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/BatchError.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/BatchError.java new file mode 100644 index 00000000..0a5e70aa --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/BatchError.java @@ -0,0 +1,181 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.phenoml.api.core.ObjectMappers; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = BatchError.Builder.class) +public final class BatchError { + private final String kind; + + private final String message; + + private final Map additionalProperties; + + private BatchError(String kind, String message, Map additionalProperties) { + this.kind = kind; + this.message = message; + this.additionalProperties = additionalProperties; + } + + /** + * @return Short stable token to branch on. Item-level kinds: invalid_input + * (the stored body was not a valid create/document request), + * processing_failed (the conversion failed), result_too_large (the + * result exceeded the storage cap), input_unavailable (the input + * could not be read), and retries_exhausted / attempts_exhausted + * (the item could not complete after repeated interruptions). + * Job-level kinds: timeout (the job did not finish within 36 hours + * of creation). + */ + @JsonProperty("kind") + public String getKind() { + return kind; + } + + /** + * @return Human-readable description of the failure. + */ + @JsonProperty("message") + public String getMessage() { + return message; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof BatchError && equalTo((BatchError) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(BatchError other) { + return kind.equals(other.kind) && message.equals(other.message); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.kind, this.message); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static KindStage builder() { + return new Builder(); + } + + public interface KindStage { + /** + *

Short stable token to branch on. Item-level kinds: invalid_input + * (the stored body was not a valid create/document request), + * processing_failed (the conversion failed), result_too_large (the + * result exceeded the storage cap), input_unavailable (the input + * could not be read), and retries_exhausted / attempts_exhausted + * (the item could not complete after repeated interruptions). + * Job-level kinds: timeout (the job did not finish within 36 hours + * of creation).

+ */ + MessageStage kind(@NotNull String kind); + + Builder from(BatchError other); + } + + public interface MessageStage { + /** + *

Human-readable description of the failure.

+ */ + _FinalStage message(@NotNull String message); + } + + public interface _FinalStage { + BatchError build(); + + _FinalStage additionalProperty(String key, Object value); + + _FinalStage additionalProperties(Map additionalProperties); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder implements KindStage, MessageStage, _FinalStage { + private String kind; + + private String message; + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + @java.lang.Override + public Builder from(BatchError other) { + kind(other.getKind()); + message(other.getMessage()); + return this; + } + + /** + *

Short stable token to branch on. Item-level kinds: invalid_input + * (the stored body was not a valid create/document request), + * processing_failed (the conversion failed), result_too_large (the + * result exceeded the storage cap), input_unavailable (the input + * could not be read), and retries_exhausted / attempts_exhausted + * (the item could not complete after repeated interruptions). + * Job-level kinds: timeout (the job did not finish within 36 hours + * of creation).

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("kind") + public MessageStage kind(@NotNull String kind) { + this.kind = Objects.requireNonNull(kind, "kind must not be null"); + return this; + } + + /** + *

Human-readable description of the failure.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("message") + public _FinalStage message(@NotNull String message) { + this.message = Objects.requireNonNull(message, "message must not be null"); + return this; + } + + @java.lang.Override + public BatchError build() { + return new BatchError(kind, message, additionalProperties); + } + + @java.lang.Override + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + @java.lang.Override + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/BatchItemStatus.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/BatchItemStatus.java new file mode 100644 index 00000000..74a2a75a --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/BatchItemStatus.java @@ -0,0 +1,423 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.phenoml.api.core.ObjectMappers; +import java.time.OffsetDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import org.jetbrains.annotations.NotNull; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = BatchItemStatus.Builder.class) +public final class BatchItemStatus { + private final String itemId; + + private final Optional id; + + private final BatchItemStatusStatus status; + + private final int attempts; + + private final Optional detectRetries; + + private final Optional resultSize; + + private final Optional error; + + private final Optional completedAt; + + private final Map additionalProperties; + + private BatchItemStatus( + String itemId, + Optional id, + BatchItemStatusStatus status, + int attempts, + Optional detectRetries, + Optional resultSize, + Optional error, + Optional completedAt, + Map additionalProperties) { + this.itemId = itemId; + this.id = id; + this.status = status; + this.attempts = attempts; + this.detectRetries = detectRetries; + this.resultSize = resultSize; + this.error = error; + this.completedAt = completedAt; + this.additionalProperties = additionalProperties; + } + + /** + * @return Server-assigned opaque item ID, used in result URLs. + */ + @JsonProperty("item_id") + public String getItemId() { + return itemId; + } + + /** + * @return The caller's correlation label, echoed back; omitted when none was supplied. + */ + @JsonProperty("id") + public Optional getId() { + return id; + } + + /** + * @return Item status. + */ + @JsonProperty("status") + public BatchItemStatusStatus getStatus() { + return status; + } + + /** + * @return Number of processing attempts started so far. The service may retry + * interrupted work; a conversion error fails the item without retry. + */ + @JsonProperty("attempts") + public int getAttempts() { + return attempts; + } + + /** + * @return How many times concept detection was re-run for the item because a + * pass returned no concepts. Present only on a succeeded item whose + * detection re-ran at least once; omitted otherwise. + */ + @JsonProperty("detect_retries") + public Optional getDetectRetries() { + return detectRetries; + } + + /** + * @return Size in bytes of the item's stored result. Present once the item has succeeded. + */ + @JsonProperty("result_size") + public Optional getResultSize() { + return resultSize; + } + + @JsonProperty("error") + public Optional getError() { + return error; + } + + /** + * @return When the item finished. Absent until then. + */ + @JsonProperty("completed_at") + public Optional getCompletedAt() { + return completedAt; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof BatchItemStatus && equalTo((BatchItemStatus) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(BatchItemStatus other) { + return itemId.equals(other.itemId) + && id.equals(other.id) + && status.equals(other.status) + && attempts == other.attempts + && detectRetries.equals(other.detectRetries) + && resultSize.equals(other.resultSize) + && error.equals(other.error) + && completedAt.equals(other.completedAt); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash( + this.itemId, + this.id, + this.status, + this.attempts, + this.detectRetries, + this.resultSize, + this.error, + this.completedAt); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static ItemIdStage builder() { + return new Builder(); + } + + public interface ItemIdStage { + /** + *

Server-assigned opaque item ID, used in result URLs.

+ */ + StatusStage itemId(@NotNull String itemId); + + Builder from(BatchItemStatus other); + } + + public interface StatusStage { + /** + *

Item status.

+ */ + AttemptsStage status(@NotNull BatchItemStatusStatus status); + } + + public interface AttemptsStage { + /** + *

Number of processing attempts started so far. The service may retry + * interrupted work; a conversion error fails the item without retry.

+ */ + _FinalStage attempts(int attempts); + } + + public interface _FinalStage { + BatchItemStatus build(); + + _FinalStage additionalProperty(String key, Object value); + + _FinalStage additionalProperties(Map additionalProperties); + + /** + *

The caller's correlation label, echoed back; omitted when none was supplied.

+ */ + _FinalStage id(Optional id); + + _FinalStage id(String id); + + /** + *

How many times concept detection was re-run for the item because a + * pass returned no concepts. Present only on a succeeded item whose + * detection re-ran at least once; omitted otherwise.

+ */ + _FinalStage detectRetries(Optional detectRetries); + + _FinalStage detectRetries(Long detectRetries); + + /** + *

Size in bytes of the item's stored result. Present once the item has succeeded.

+ */ + _FinalStage resultSize(Optional resultSize); + + _FinalStage resultSize(Long resultSize); + + _FinalStage error(Optional error); + + _FinalStage error(BatchError error); + + /** + *

When the item finished. Absent until then.

+ */ + _FinalStage completedAt(Optional completedAt); + + _FinalStage completedAt(OffsetDateTime completedAt); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder implements ItemIdStage, StatusStage, AttemptsStage, _FinalStage { + private String itemId; + + private BatchItemStatusStatus status; + + private int attempts; + + private Optional completedAt = Optional.empty(); + + private Optional error = Optional.empty(); + + private Optional resultSize = Optional.empty(); + + private Optional detectRetries = Optional.empty(); + + private Optional id = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + @java.lang.Override + public Builder from(BatchItemStatus other) { + itemId(other.getItemId()); + id(other.getId()); + status(other.getStatus()); + attempts(other.getAttempts()); + detectRetries(other.getDetectRetries()); + resultSize(other.getResultSize()); + error(other.getError()); + completedAt(other.getCompletedAt()); + return this; + } + + /** + *

Server-assigned opaque item ID, used in result URLs.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("item_id") + public StatusStage itemId(@NotNull String itemId) { + this.itemId = Objects.requireNonNull(itemId, "itemId must not be null"); + return this; + } + + /** + *

Item status.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("status") + public AttemptsStage status(@NotNull BatchItemStatusStatus status) { + this.status = Objects.requireNonNull(status, "status must not be null"); + return this; + } + + /** + *

Number of processing attempts started so far. The service may retry + * interrupted work; a conversion error fails the item without retry.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("attempts") + public _FinalStage attempts(int attempts) { + this.attempts = attempts; + return this; + } + + /** + *

When the item finished. Absent until then.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage completedAt(OffsetDateTime completedAt) { + this.completedAt = Optional.ofNullable(completedAt); + return this; + } + + /** + *

When the item finished. Absent until then.

+ */ + @java.lang.Override + @JsonSetter(value = "completed_at", nulls = Nulls.SKIP) + public _FinalStage completedAt(Optional completedAt) { + this.completedAt = completedAt; + return this; + } + + @java.lang.Override + public _FinalStage error(BatchError error) { + this.error = Optional.ofNullable(error); + return this; + } + + @java.lang.Override + @JsonSetter(value = "error", nulls = Nulls.SKIP) + public _FinalStage error(Optional error) { + this.error = error; + return this; + } + + /** + *

Size in bytes of the item's stored result. Present once the item has succeeded.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage resultSize(Long resultSize) { + this.resultSize = Optional.ofNullable(resultSize); + return this; + } + + /** + *

Size in bytes of the item's stored result. Present once the item has succeeded.

+ */ + @java.lang.Override + @JsonSetter(value = "result_size", nulls = Nulls.SKIP) + public _FinalStage resultSize(Optional resultSize) { + this.resultSize = resultSize; + return this; + } + + /** + *

How many times concept detection was re-run for the item because a + * pass returned no concepts. Present only on a succeeded item whose + * detection re-ran at least once; omitted otherwise.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage detectRetries(Long detectRetries) { + this.detectRetries = Optional.ofNullable(detectRetries); + return this; + } + + /** + *

How many times concept detection was re-run for the item because a + * pass returned no concepts. Present only on a succeeded item whose + * detection re-ran at least once; omitted otherwise.

+ */ + @java.lang.Override + @JsonSetter(value = "detect_retries", nulls = Nulls.SKIP) + public _FinalStage detectRetries(Optional detectRetries) { + this.detectRetries = detectRetries; + return this; + } + + /** + *

The caller's correlation label, echoed back; omitted when none was supplied.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage id(String id) { + this.id = Optional.ofNullable(id); + return this; + } + + /** + *

The caller's correlation label, echoed back; omitted when none was supplied.

+ */ + @java.lang.Override + @JsonSetter(value = "id", nulls = Nulls.SKIP) + public _FinalStage id(Optional id) { + this.id = id; + return this; + } + + @java.lang.Override + public BatchItemStatus build() { + return new BatchItemStatus( + itemId, id, status, attempts, detectRetries, resultSize, error, completedAt, additionalProperties); + } + + @java.lang.Override + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + @java.lang.Override + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/BatchItemStatusStatus.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/BatchItemStatusStatus.java new file mode 100644 index 00000000..9a9aa691 --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/BatchItemStatusStatus.java @@ -0,0 +1,104 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.types; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public final class BatchItemStatusStatus { + public static final BatchItemStatusStatus FAILED = new BatchItemStatusStatus(Value.FAILED, "failed"); + + public static final BatchItemStatusStatus SUCCEEDED = new BatchItemStatusStatus(Value.SUCCEEDED, "succeeded"); + + public static final BatchItemStatusStatus PENDING = new BatchItemStatusStatus(Value.PENDING, "pending"); + + public static final BatchItemStatusStatus PROCESSING = new BatchItemStatusStatus(Value.PROCESSING, "processing"); + + private final Value value; + + private final String string; + + BatchItemStatusStatus(Value value, String string) { + this.value = value; + this.string = string; + } + + public Value getEnumValue() { + return value; + } + + @java.lang.Override + @JsonValue + public String toString() { + return this.string; + } + + @java.lang.Override + public boolean equals(Object other) { + return (this == other) + || (other instanceof BatchItemStatusStatus + && this.string.equals(((BatchItemStatusStatus) other).string)); + } + + @java.lang.Override + public int hashCode() { + return this.string.hashCode(); + } + + public T visit(Visitor visitor) { + switch (value) { + case FAILED: + return visitor.visitFailed(); + case SUCCEEDED: + return visitor.visitSucceeded(); + case PENDING: + return visitor.visitPending(); + case PROCESSING: + return visitor.visitProcessing(); + case UNKNOWN: + default: + return visitor.visitUnknown(string); + } + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static BatchItemStatusStatus valueOf(String value) { + switch (value) { + case "failed": + return FAILED; + case "succeeded": + return SUCCEEDED; + case "pending": + return PENDING; + case "processing": + return PROCESSING; + default: + return new BatchItemStatusStatus(Value.UNKNOWN, value); + } + } + + public enum Value { + PENDING, + + PROCESSING, + + SUCCEEDED, + + FAILED, + + UNKNOWN + } + + public interface Visitor { + T visitPending(); + + T visitProcessing(); + + T visitSucceeded(); + + T visitFailed(); + + T visitUnknown(String unknownType); + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/BatchJob.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/BatchJob.java new file mode 100644 index 00000000..a8d85810 --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/BatchJob.java @@ -0,0 +1,514 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.phenoml.api.core.ObjectMappers; +import java.time.OffsetDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import org.jetbrains.annotations.NotNull; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = BatchJob.Builder.class) +public final class BatchJob implements IBatchJob { + private final String jobId; + + private final Optional requestId; + + private final BatchJobStatus status; + + private final boolean finalized; + + private final int totalItems; + + private final Optional error; + + private final OffsetDateTime createdAt; + + private final OffsetDateTime updatedAt; + + private final Optional completedAt; + + private final OffsetDateTime expiresAt; + + private final Map additionalProperties; + + private BatchJob( + String jobId, + Optional requestId, + BatchJobStatus status, + boolean finalized, + int totalItems, + Optional error, + OffsetDateTime createdAt, + OffsetDateTime updatedAt, + Optional completedAt, + OffsetDateTime expiresAt, + Map additionalProperties) { + this.jobId = jobId; + this.requestId = requestId; + this.status = status; + this.finalized = finalized; + this.totalItems = totalItems; + this.error = error; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + this.completedAt = completedAt; + this.expiresAt = expiresAt; + this.additionalProperties = additionalProperties; + } + + /** + * @return Server-assigned job identifier. + */ + @JsonProperty("job_id") + @java.lang.Override + public String getJobId() { + return jobId; + } + + /** + * @return The idempotency token supplied at create, if any. + */ + @JsonProperty("request_id") + @java.lang.Override + public Optional getRequestId() { + return requestId; + } + + /** + * @return Job status. completed means every item has finished — some may have + * failed, so check counts for the split. failed is a whole-job + * failure (the job could not run at all), distinct from individual item + * failures, which never fail the job. canceled is a caller-requested + * cancellation via POST /lang2fhir/batch/{job_id}/cancel; like the + * other terminal states it keeps any results already produced readable + * for the retention window. + */ + @JsonProperty("status") + public BatchJobStatus getStatus() { + return status; + } + + /** + * @return Whether the job's item set has been sealed. + */ + @JsonProperty("finalized") + @java.lang.Override + public boolean getFinalized() { + return finalized; + } + + /** + * @return The sealed item count. It is 0 until the job is finalized, so an + * upload response always reports 0; poll the job after finalize for the + * real count. + */ + @JsonProperty("total_items") + @java.lang.Override + public int getTotalItems() { + return totalItems; + } + + /** + * @return A whole-job failure. Present only on a failed job. + */ + @JsonProperty("error") + @java.lang.Override + public Optional getError() { + return error; + } + + @JsonProperty("created_at") + @java.lang.Override + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + @JsonProperty("updated_at") + @java.lang.Override + public OffsetDateTime getUpdatedAt() { + return updatedAt; + } + + /** + * @return When the job finished. Absent until then. + */ + @JsonProperty("completed_at") + @java.lang.Override + public Optional getCompletedAt() { + return completedAt; + } + + /** + * @return When the job and its stored inputs and results are deleted. Set 7 + * days out, with the clock restarting when the job reaches completed, + * failed, or canceled. At expiry the job's request_id is freed for reuse. + */ + @JsonProperty("expires_at") + @java.lang.Override + public OffsetDateTime getExpiresAt() { + return expiresAt; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof BatchJob && equalTo((BatchJob) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(BatchJob other) { + return jobId.equals(other.jobId) + && requestId.equals(other.requestId) + && status.equals(other.status) + && finalized == other.finalized + && totalItems == other.totalItems + && error.equals(other.error) + && createdAt.equals(other.createdAt) + && updatedAt.equals(other.updatedAt) + && completedAt.equals(other.completedAt) + && expiresAt.equals(other.expiresAt); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash( + this.jobId, + this.requestId, + this.status, + this.finalized, + this.totalItems, + this.error, + this.createdAt, + this.updatedAt, + this.completedAt, + this.expiresAt); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static JobIdStage builder() { + return new Builder(); + } + + public interface JobIdStage { + /** + *

Server-assigned job identifier.

+ */ + StatusStage jobId(@NotNull String jobId); + + Builder from(BatchJob other); + } + + public interface StatusStage { + /** + *

Job status. completed means every item has finished — some may have + * failed, so check counts for the split. failed is a whole-job + * failure (the job could not run at all), distinct from individual item + * failures, which never fail the job. canceled is a caller-requested + * cancellation via POST /lang2fhir/batch/{job_id}/cancel; like the + * other terminal states it keeps any results already produced readable + * for the retention window.

+ */ + FinalizedStage status(@NotNull BatchJobStatus status); + } + + public interface FinalizedStage { + /** + *

Whether the job's item set has been sealed.

+ */ + TotalItemsStage finalized(boolean finalized); + } + + public interface TotalItemsStage { + /** + *

The sealed item count. It is 0 until the job is finalized, so an + * upload response always reports 0; poll the job after finalize for the + * real count.

+ */ + CreatedAtStage totalItems(int totalItems); + } + + public interface CreatedAtStage { + UpdatedAtStage createdAt(@NotNull OffsetDateTime createdAt); + } + + public interface UpdatedAtStage { + ExpiresAtStage updatedAt(@NotNull OffsetDateTime updatedAt); + } + + public interface ExpiresAtStage { + /** + *

When the job and its stored inputs and results are deleted. Set 7 + * days out, with the clock restarting when the job reaches completed, + * failed, or canceled. At expiry the job's request_id is freed for reuse.

+ */ + _FinalStage expiresAt(@NotNull OffsetDateTime expiresAt); + } + + public interface _FinalStage { + BatchJob build(); + + _FinalStage additionalProperty(String key, Object value); + + _FinalStage additionalProperties(Map additionalProperties); + + /** + *

The idempotency token supplied at create, if any.

+ */ + _FinalStage requestId(Optional requestId); + + _FinalStage requestId(String requestId); + + /** + *

A whole-job failure. Present only on a failed job.

+ */ + _FinalStage error(Optional error); + + _FinalStage error(BatchError error); + + /** + *

When the job finished. Absent until then.

+ */ + _FinalStage completedAt(Optional completedAt); + + _FinalStage completedAt(OffsetDateTime completedAt); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder + implements JobIdStage, + StatusStage, + FinalizedStage, + TotalItemsStage, + CreatedAtStage, + UpdatedAtStage, + ExpiresAtStage, + _FinalStage { + private String jobId; + + private BatchJobStatus status; + + private boolean finalized; + + private int totalItems; + + private OffsetDateTime createdAt; + + private OffsetDateTime updatedAt; + + private OffsetDateTime expiresAt; + + private Optional completedAt = Optional.empty(); + + private Optional error = Optional.empty(); + + private Optional requestId = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + @java.lang.Override + public Builder from(BatchJob other) { + jobId(other.getJobId()); + requestId(other.getRequestId()); + status(other.getStatus()); + finalized(other.getFinalized()); + totalItems(other.getTotalItems()); + error(other.getError()); + createdAt(other.getCreatedAt()); + updatedAt(other.getUpdatedAt()); + completedAt(other.getCompletedAt()); + expiresAt(other.getExpiresAt()); + return this; + } + + /** + *

Server-assigned job identifier.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("job_id") + public StatusStage jobId(@NotNull String jobId) { + this.jobId = Objects.requireNonNull(jobId, "jobId must not be null"); + return this; + } + + /** + *

Job status. completed means every item has finished — some may have + * failed, so check counts for the split. failed is a whole-job + * failure (the job could not run at all), distinct from individual item + * failures, which never fail the job. canceled is a caller-requested + * cancellation via POST /lang2fhir/batch/{job_id}/cancel; like the + * other terminal states it keeps any results already produced readable + * for the retention window.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("status") + public FinalizedStage status(@NotNull BatchJobStatus status) { + this.status = Objects.requireNonNull(status, "status must not be null"); + return this; + } + + /** + *

Whether the job's item set has been sealed.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("finalized") + public TotalItemsStage finalized(boolean finalized) { + this.finalized = finalized; + return this; + } + + /** + *

The sealed item count. It is 0 until the job is finalized, so an + * upload response always reports 0; poll the job after finalize for the + * real count.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("total_items") + public CreatedAtStage totalItems(int totalItems) { + this.totalItems = totalItems; + return this; + } + + @java.lang.Override + @JsonSetter("created_at") + public UpdatedAtStage createdAt(@NotNull OffsetDateTime createdAt) { + this.createdAt = Objects.requireNonNull(createdAt, "createdAt must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("updated_at") + public ExpiresAtStage updatedAt(@NotNull OffsetDateTime updatedAt) { + this.updatedAt = Objects.requireNonNull(updatedAt, "updatedAt must not be null"); + return this; + } + + /** + *

When the job and its stored inputs and results are deleted. Set 7 + * days out, with the clock restarting when the job reaches completed, + * failed, or canceled. At expiry the job's request_id is freed for reuse.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("expires_at") + public _FinalStage expiresAt(@NotNull OffsetDateTime expiresAt) { + this.expiresAt = Objects.requireNonNull(expiresAt, "expiresAt must not be null"); + return this; + } + + /** + *

When the job finished. Absent until then.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage completedAt(OffsetDateTime completedAt) { + this.completedAt = Optional.ofNullable(completedAt); + return this; + } + + /** + *

When the job finished. Absent until then.

+ */ + @java.lang.Override + @JsonSetter(value = "completed_at", nulls = Nulls.SKIP) + public _FinalStage completedAt(Optional completedAt) { + this.completedAt = completedAt; + return this; + } + + /** + *

A whole-job failure. Present only on a failed job.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage error(BatchError error) { + this.error = Optional.ofNullable(error); + return this; + } + + /** + *

A whole-job failure. Present only on a failed job.

+ */ + @java.lang.Override + @JsonSetter(value = "error", nulls = Nulls.SKIP) + public _FinalStage error(Optional error) { + this.error = error; + return this; + } + + /** + *

The idempotency token supplied at create, if any.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage requestId(String requestId) { + this.requestId = Optional.ofNullable(requestId); + return this; + } + + /** + *

The idempotency token supplied at create, if any.

+ */ + @java.lang.Override + @JsonSetter(value = "request_id", nulls = Nulls.SKIP) + public _FinalStage requestId(Optional requestId) { + this.requestId = requestId; + return this; + } + + @java.lang.Override + public BatchJob build() { + return new BatchJob( + jobId, + requestId, + status, + finalized, + totalItems, + error, + createdAt, + updatedAt, + completedAt, + expiresAt, + additionalProperties); + } + + @java.lang.Override + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + @java.lang.Override + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/BatchJobStatus.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/BatchJobStatus.java new file mode 100644 index 00000000..78170b83 --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/BatchJobStatus.java @@ -0,0 +1,113 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.types; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public final class BatchJobStatus { + public static final BatchJobStatus FAILED = new BatchJobStatus(Value.FAILED, "failed"); + + public static final BatchJobStatus PENDING = new BatchJobStatus(Value.PENDING, "pending"); + + public static final BatchJobStatus PROCESSING = new BatchJobStatus(Value.PROCESSING, "processing"); + + public static final BatchJobStatus CANCELED = new BatchJobStatus(Value.CANCELED, "canceled"); + + public static final BatchJobStatus COMPLETED = new BatchJobStatus(Value.COMPLETED, "completed"); + + private final Value value; + + private final String string; + + BatchJobStatus(Value value, String string) { + this.value = value; + this.string = string; + } + + public Value getEnumValue() { + return value; + } + + @java.lang.Override + @JsonValue + public String toString() { + return this.string; + } + + @java.lang.Override + public boolean equals(Object other) { + return (this == other) + || (other instanceof BatchJobStatus && this.string.equals(((BatchJobStatus) other).string)); + } + + @java.lang.Override + public int hashCode() { + return this.string.hashCode(); + } + + public T visit(Visitor visitor) { + switch (value) { + case FAILED: + return visitor.visitFailed(); + case PENDING: + return visitor.visitPending(); + case PROCESSING: + return visitor.visitProcessing(); + case CANCELED: + return visitor.visitCanceled(); + case COMPLETED: + return visitor.visitCompleted(); + case UNKNOWN: + default: + return visitor.visitUnknown(string); + } + } + + @JsonCreator(mode = JsonCreator.Mode.DELEGATING) + public static BatchJobStatus valueOf(String value) { + switch (value) { + case "failed": + return FAILED; + case "pending": + return PENDING; + case "processing": + return PROCESSING; + case "canceled": + return CANCELED; + case "completed": + return COMPLETED; + default: + return new BatchJobStatus(Value.UNKNOWN, value); + } + } + + public enum Value { + PENDING, + + PROCESSING, + + COMPLETED, + + FAILED, + + CANCELED, + + UNKNOWN + } + + public interface Visitor { + T visitPending(); + + T visitProcessing(); + + T visitCompleted(); + + T visitFailed(); + + T visitCanceled(); + + T visitUnknown(String unknownType); + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/IBatchJob.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/IBatchJob.java new file mode 100644 index 00000000..5c02257d --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/IBatchJob.java @@ -0,0 +1,29 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.types; + +import java.time.OffsetDateTime; +import java.util.Optional; + +public interface IBatchJob { + String getJobId(); + + Optional getRequestId(); + + BatchJobStatus getStatus(); + + boolean getFinalized(); + + int getTotalItems(); + + Optional getError(); + + OffsetDateTime getCreatedAt(); + + OffsetDateTime getUpdatedAt(); + + Optional getCompletedAt(); + + OffsetDateTime getExpiresAt(); +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/JobDetailResponse.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/JobDetailResponse.java new file mode 100644 index 00000000..b8d65064 --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/JobDetailResponse.java @@ -0,0 +1,670 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.phenoml.api.core.ObjectMappers; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import org.jetbrains.annotations.NotNull; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = JobDetailResponse.Builder.class) +public final class JobDetailResponse implements IBatchJob { + private final String jobId; + + private final Optional requestId; + + private final BatchJobStatus status; + + private final boolean finalized; + + private final int totalItems; + + private final Optional error; + + private final OffsetDateTime createdAt; + + private final OffsetDateTime updatedAt; + + private final Optional completedAt; + + private final OffsetDateTime expiresAt; + + private final BatchCounts counts; + + private final List items; + + private final Optional nextCursor; + + private final boolean hasMore; + + private final Map additionalProperties; + + private JobDetailResponse( + String jobId, + Optional requestId, + BatchJobStatus status, + boolean finalized, + int totalItems, + Optional error, + OffsetDateTime createdAt, + OffsetDateTime updatedAt, + Optional completedAt, + OffsetDateTime expiresAt, + BatchCounts counts, + List items, + Optional nextCursor, + boolean hasMore, + Map additionalProperties) { + this.jobId = jobId; + this.requestId = requestId; + this.status = status; + this.finalized = finalized; + this.totalItems = totalItems; + this.error = error; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + this.completedAt = completedAt; + this.expiresAt = expiresAt; + this.counts = counts; + this.items = items; + this.nextCursor = nextCursor; + this.hasMore = hasMore; + this.additionalProperties = additionalProperties; + } + + /** + * @return Server-assigned job identifier. + */ + @JsonProperty("job_id") + @java.lang.Override + public String getJobId() { + return jobId; + } + + /** + * @return The idempotency token supplied at create, if any. + */ + @JsonProperty("request_id") + @java.lang.Override + public Optional getRequestId() { + return requestId; + } + + /** + * @return Job status. completed means every item has finished — some may have + * failed, so check counts for the split. failed is a whole-job + * failure (the job could not run at all), distinct from individual item + * failures, which never fail the job. canceled is a caller-requested + * cancellation via POST /lang2fhir/batch/{job_id}/cancel; like the + * other terminal states it keeps any results already produced readable + * for the retention window. + */ + @JsonProperty("status") + public BatchJobStatus getStatus() { + return status; + } + + /** + * @return Whether the job's item set has been sealed. + */ + @JsonProperty("finalized") + @java.lang.Override + public boolean getFinalized() { + return finalized; + } + + /** + * @return The sealed item count. It is 0 until the job is finalized, so an + * upload response always reports 0; poll the job after finalize for the + * real count. + */ + @JsonProperty("total_items") + @java.lang.Override + public int getTotalItems() { + return totalItems; + } + + /** + * @return A whole-job failure. Present only on a failed job. + */ + @JsonProperty("error") + @java.lang.Override + public Optional getError() { + return error; + } + + @JsonProperty("created_at") + @java.lang.Override + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + @JsonProperty("updated_at") + @java.lang.Override + public OffsetDateTime getUpdatedAt() { + return updatedAt; + } + + /** + * @return When the job finished. Absent until then. + */ + @JsonProperty("completed_at") + @java.lang.Override + public Optional getCompletedAt() { + return completedAt; + } + + /** + * @return When the job and its stored inputs and results are deleted. Set 7 + * days out, with the clock restarting when the job reaches completed, + * failed, or canceled. At expiry the job's request_id is freed for reuse. + */ + @JsonProperty("expires_at") + @java.lang.Override + public OffsetDateTime getExpiresAt() { + return expiresAt; + } + + @JsonProperty("counts") + public BatchCounts getCounts() { + return counts; + } + + @JsonProperty("items") + public List getItems() { + return items; + } + + /** + * @return Cursor for the next page, when has_more is true. + */ + @JsonProperty("next_cursor") + public Optional getNextCursor() { + return nextCursor; + } + + /** + * @return Whether more item statuses remain beyond this page. + */ + @JsonProperty("has_more") + public boolean getHasMore() { + return hasMore; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof JobDetailResponse && equalTo((JobDetailResponse) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(JobDetailResponse other) { + return jobId.equals(other.jobId) + && requestId.equals(other.requestId) + && status.equals(other.status) + && finalized == other.finalized + && totalItems == other.totalItems + && error.equals(other.error) + && createdAt.equals(other.createdAt) + && updatedAt.equals(other.updatedAt) + && completedAt.equals(other.completedAt) + && expiresAt.equals(other.expiresAt) + && counts.equals(other.counts) + && items.equals(other.items) + && nextCursor.equals(other.nextCursor) + && hasMore == other.hasMore; + } + + @java.lang.Override + public int hashCode() { + return Objects.hash( + this.jobId, + this.requestId, + this.status, + this.finalized, + this.totalItems, + this.error, + this.createdAt, + this.updatedAt, + this.completedAt, + this.expiresAt, + this.counts, + this.items, + this.nextCursor, + this.hasMore); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static JobIdStage builder() { + return new Builder(); + } + + public interface JobIdStage { + /** + *

Server-assigned job identifier.

+ */ + StatusStage jobId(@NotNull String jobId); + + Builder from(JobDetailResponse other); + } + + public interface StatusStage { + /** + *

Job status. completed means every item has finished — some may have + * failed, so check counts for the split. failed is a whole-job + * failure (the job could not run at all), distinct from individual item + * failures, which never fail the job. canceled is a caller-requested + * cancellation via POST /lang2fhir/batch/{job_id}/cancel; like the + * other terminal states it keeps any results already produced readable + * for the retention window.

+ */ + FinalizedStage status(@NotNull BatchJobStatus status); + } + + public interface FinalizedStage { + /** + *

Whether the job's item set has been sealed.

+ */ + TotalItemsStage finalized(boolean finalized); + } + + public interface TotalItemsStage { + /** + *

The sealed item count. It is 0 until the job is finalized, so an + * upload response always reports 0; poll the job after finalize for the + * real count.

+ */ + CreatedAtStage totalItems(int totalItems); + } + + public interface CreatedAtStage { + UpdatedAtStage createdAt(@NotNull OffsetDateTime createdAt); + } + + public interface UpdatedAtStage { + ExpiresAtStage updatedAt(@NotNull OffsetDateTime updatedAt); + } + + public interface ExpiresAtStage { + /** + *

When the job and its stored inputs and results are deleted. Set 7 + * days out, with the clock restarting when the job reaches completed, + * failed, or canceled. At expiry the job's request_id is freed for reuse.

+ */ + CountsStage expiresAt(@NotNull OffsetDateTime expiresAt); + } + + public interface CountsStage { + HasMoreStage counts(@NotNull BatchCounts counts); + } + + public interface HasMoreStage { + /** + *

Whether more item statuses remain beyond this page.

+ */ + _FinalStage hasMore(boolean hasMore); + } + + public interface _FinalStage { + JobDetailResponse build(); + + _FinalStage additionalProperty(String key, Object value); + + _FinalStage additionalProperties(Map additionalProperties); + + /** + *

The idempotency token supplied at create, if any.

+ */ + _FinalStage requestId(Optional requestId); + + _FinalStage requestId(String requestId); + + /** + *

A whole-job failure. Present only on a failed job.

+ */ + _FinalStage error(Optional error); + + _FinalStage error(BatchError error); + + /** + *

When the job finished. Absent until then.

+ */ + _FinalStage completedAt(Optional completedAt); + + _FinalStage completedAt(OffsetDateTime completedAt); + + _FinalStage items(List items); + + _FinalStage addItems(BatchItemStatus items); + + _FinalStage addAllItems(List items); + + /** + *

Cursor for the next page, when has_more is true.

+ */ + _FinalStage nextCursor(Optional nextCursor); + + _FinalStage nextCursor(String nextCursor); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder + implements JobIdStage, + StatusStage, + FinalizedStage, + TotalItemsStage, + CreatedAtStage, + UpdatedAtStage, + ExpiresAtStage, + CountsStage, + HasMoreStage, + _FinalStage { + private String jobId; + + private BatchJobStatus status; + + private boolean finalized; + + private int totalItems; + + private OffsetDateTime createdAt; + + private OffsetDateTime updatedAt; + + private OffsetDateTime expiresAt; + + private BatchCounts counts; + + private boolean hasMore; + + private Optional nextCursor = Optional.empty(); + + private List items = new ArrayList<>(); + + private Optional completedAt = Optional.empty(); + + private Optional error = Optional.empty(); + + private Optional requestId = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + @java.lang.Override + public Builder from(JobDetailResponse other) { + jobId(other.getJobId()); + requestId(other.getRequestId()); + status(other.getStatus()); + finalized(other.getFinalized()); + totalItems(other.getTotalItems()); + error(other.getError()); + createdAt(other.getCreatedAt()); + updatedAt(other.getUpdatedAt()); + completedAt(other.getCompletedAt()); + expiresAt(other.getExpiresAt()); + counts(other.getCounts()); + items(other.getItems()); + nextCursor(other.getNextCursor()); + hasMore(other.getHasMore()); + return this; + } + + /** + *

Server-assigned job identifier.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("job_id") + public StatusStage jobId(@NotNull String jobId) { + this.jobId = Objects.requireNonNull(jobId, "jobId must not be null"); + return this; + } + + /** + *

Job status. completed means every item has finished — some may have + * failed, so check counts for the split. failed is a whole-job + * failure (the job could not run at all), distinct from individual item + * failures, which never fail the job. canceled is a caller-requested + * cancellation via POST /lang2fhir/batch/{job_id}/cancel; like the + * other terminal states it keeps any results already produced readable + * for the retention window.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("status") + public FinalizedStage status(@NotNull BatchJobStatus status) { + this.status = Objects.requireNonNull(status, "status must not be null"); + return this; + } + + /** + *

Whether the job's item set has been sealed.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("finalized") + public TotalItemsStage finalized(boolean finalized) { + this.finalized = finalized; + return this; + } + + /** + *

The sealed item count. It is 0 until the job is finalized, so an + * upload response always reports 0; poll the job after finalize for the + * real count.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("total_items") + public CreatedAtStage totalItems(int totalItems) { + this.totalItems = totalItems; + return this; + } + + @java.lang.Override + @JsonSetter("created_at") + public UpdatedAtStage createdAt(@NotNull OffsetDateTime createdAt) { + this.createdAt = Objects.requireNonNull(createdAt, "createdAt must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("updated_at") + public ExpiresAtStage updatedAt(@NotNull OffsetDateTime updatedAt) { + this.updatedAt = Objects.requireNonNull(updatedAt, "updatedAt must not be null"); + return this; + } + + /** + *

When the job and its stored inputs and results are deleted. Set 7 + * days out, with the clock restarting when the job reaches completed, + * failed, or canceled. At expiry the job's request_id is freed for reuse.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("expires_at") + public CountsStage expiresAt(@NotNull OffsetDateTime expiresAt) { + this.expiresAt = Objects.requireNonNull(expiresAt, "expiresAt must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("counts") + public HasMoreStage counts(@NotNull BatchCounts counts) { + this.counts = Objects.requireNonNull(counts, "counts must not be null"); + return this; + } + + /** + *

Whether more item statuses remain beyond this page.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("has_more") + public _FinalStage hasMore(boolean hasMore) { + this.hasMore = hasMore; + return this; + } + + /** + *

Cursor for the next page, when has_more is true.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage nextCursor(String nextCursor) { + this.nextCursor = Optional.ofNullable(nextCursor); + return this; + } + + /** + *

Cursor for the next page, when has_more is true.

+ */ + @java.lang.Override + @JsonSetter(value = "next_cursor", nulls = Nulls.SKIP) + public _FinalStage nextCursor(Optional nextCursor) { + this.nextCursor = nextCursor; + return this; + } + + @java.lang.Override + public _FinalStage addAllItems(List items) { + if (items != null) { + this.items.addAll(items); + } + return this; + } + + @java.lang.Override + public _FinalStage addItems(BatchItemStatus items) { + this.items.add(items); + return this; + } + + @java.lang.Override + @JsonSetter(value = "items", nulls = Nulls.SKIP) + public _FinalStage items(List items) { + this.items.clear(); + if (items != null) { + this.items.addAll(items); + } + return this; + } + + /** + *

When the job finished. Absent until then.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage completedAt(OffsetDateTime completedAt) { + this.completedAt = Optional.ofNullable(completedAt); + return this; + } + + /** + *

When the job finished. Absent until then.

+ */ + @java.lang.Override + @JsonSetter(value = "completed_at", nulls = Nulls.SKIP) + public _FinalStage completedAt(Optional completedAt) { + this.completedAt = completedAt; + return this; + } + + /** + *

A whole-job failure. Present only on a failed job.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage error(BatchError error) { + this.error = Optional.ofNullable(error); + return this; + } + + /** + *

A whole-job failure. Present only on a failed job.

+ */ + @java.lang.Override + @JsonSetter(value = "error", nulls = Nulls.SKIP) + public _FinalStage error(Optional error) { + this.error = error; + return this; + } + + /** + *

The idempotency token supplied at create, if any.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage requestId(String requestId) { + this.requestId = Optional.ofNullable(requestId); + return this; + } + + /** + *

The idempotency token supplied at create, if any.

+ */ + @java.lang.Override + @JsonSetter(value = "request_id", nulls = Nulls.SKIP) + public _FinalStage requestId(Optional requestId) { + this.requestId = requestId; + return this; + } + + @java.lang.Override + public JobDetailResponse build() { + return new JobDetailResponse( + jobId, + requestId, + status, + finalized, + totalItems, + error, + createdAt, + updatedAt, + completedAt, + expiresAt, + counts, + items, + nextCursor, + hasMore, + additionalProperties); + } + + @java.lang.Override + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + @java.lang.Override + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/JobListResponse.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/JobListResponse.java new file mode 100644 index 00000000..6ee0915d --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/JobListResponse.java @@ -0,0 +1,217 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.phenoml.api.core.ObjectMappers; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = JobListResponse.Builder.class) +public final class JobListResponse { + private final List jobs; + + private final Optional nextCursor; + + private final boolean hasMore; + + private final Map additionalProperties; + + private JobListResponse( + List jobs, + Optional nextCursor, + boolean hasMore, + Map additionalProperties) { + this.jobs = jobs; + this.nextCursor = nextCursor; + this.hasMore = hasMore; + this.additionalProperties = additionalProperties; + } + + @JsonProperty("jobs") + public List getJobs() { + return jobs; + } + + /** + * @return Cursor for the next page, when has_more is true. + */ + @JsonProperty("next_cursor") + public Optional getNextCursor() { + return nextCursor; + } + + /** + * @return Whether more jobs remain beyond this page. + */ + @JsonProperty("has_more") + public boolean getHasMore() { + return hasMore; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof JobListResponse && equalTo((JobListResponse) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(JobListResponse other) { + return jobs.equals(other.jobs) && nextCursor.equals(other.nextCursor) && hasMore == other.hasMore; + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.jobs, this.nextCursor, this.hasMore); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static HasMoreStage builder() { + return new Builder(); + } + + public interface HasMoreStage { + /** + *

Whether more jobs remain beyond this page.

+ */ + _FinalStage hasMore(boolean hasMore); + + Builder from(JobListResponse other); + } + + public interface _FinalStage { + JobListResponse build(); + + _FinalStage additionalProperty(String key, Object value); + + _FinalStage additionalProperties(Map additionalProperties); + + _FinalStage jobs(List jobs); + + _FinalStage addJobs(BatchJob jobs); + + _FinalStage addAllJobs(List jobs); + + /** + *

Cursor for the next page, when has_more is true.

+ */ + _FinalStage nextCursor(Optional nextCursor); + + _FinalStage nextCursor(String nextCursor); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder implements HasMoreStage, _FinalStage { + private boolean hasMore; + + private Optional nextCursor = Optional.empty(); + + private List jobs = new ArrayList<>(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + @java.lang.Override + public Builder from(JobListResponse other) { + jobs(other.getJobs()); + nextCursor(other.getNextCursor()); + hasMore(other.getHasMore()); + return this; + } + + /** + *

Whether more jobs remain beyond this page.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("has_more") + public _FinalStage hasMore(boolean hasMore) { + this.hasMore = hasMore; + return this; + } + + /** + *

Cursor for the next page, when has_more is true.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage nextCursor(String nextCursor) { + this.nextCursor = Optional.ofNullable(nextCursor); + return this; + } + + /** + *

Cursor for the next page, when has_more is true.

+ */ + @java.lang.Override + @JsonSetter(value = "next_cursor", nulls = Nulls.SKIP) + public _FinalStage nextCursor(Optional nextCursor) { + this.nextCursor = nextCursor; + return this; + } + + @java.lang.Override + public _FinalStage addAllJobs(List jobs) { + if (jobs != null) { + this.jobs.addAll(jobs); + } + return this; + } + + @java.lang.Override + public _FinalStage addJobs(BatchJob jobs) { + this.jobs.add(jobs); + return this; + } + + @java.lang.Override + @JsonSetter(value = "jobs", nulls = Nulls.SKIP) + public _FinalStage jobs(List jobs) { + this.jobs.clear(); + if (jobs != null) { + this.jobs.addAll(jobs); + } + return this; + } + + @java.lang.Override + public JobListResponse build() { + return new JobListResponse(jobs, nextCursor, hasMore, additionalProperties); + } + + @java.lang.Override + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + @java.lang.Override + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/ResultsPageResponse.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/ResultsPageResponse.java new file mode 100644 index 00000000..00a13736 --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/ResultsPageResponse.java @@ -0,0 +1,217 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.phenoml.api.core.ObjectMappers; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = ResultsPageResponse.Builder.class) +public final class ResultsPageResponse { + private final List results; + + private final Optional nextCursor; + + private final boolean hasMore; + + private final Map additionalProperties; + + private ResultsPageResponse( + List results, + Optional nextCursor, + boolean hasMore, + Map additionalProperties) { + this.results = results; + this.nextCursor = nextCursor; + this.hasMore = hasMore; + this.additionalProperties = additionalProperties; + } + + @JsonProperty("results") + public List getResults() { + return results; + } + + /** + * @return Cursor for the next page, when has_more is true. + */ + @JsonProperty("next_cursor") + public Optional getNextCursor() { + return nextCursor; + } + + /** + * @return Whether more results remain beyond this page. + */ + @JsonProperty("has_more") + public boolean getHasMore() { + return hasMore; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof ResultsPageResponse && equalTo((ResultsPageResponse) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(ResultsPageResponse other) { + return results.equals(other.results) && nextCursor.equals(other.nextCursor) && hasMore == other.hasMore; + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.results, this.nextCursor, this.hasMore); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static HasMoreStage builder() { + return new Builder(); + } + + public interface HasMoreStage { + /** + *

Whether more results remain beyond this page.

+ */ + _FinalStage hasMore(boolean hasMore); + + Builder from(ResultsPageResponse other); + } + + public interface _FinalStage { + ResultsPageResponse build(); + + _FinalStage additionalProperty(String key, Object value); + + _FinalStage additionalProperties(Map additionalProperties); + + _FinalStage results(List results); + + _FinalStage addResults(BatchItemStatus results); + + _FinalStage addAllResults(List results); + + /** + *

Cursor for the next page, when has_more is true.

+ */ + _FinalStage nextCursor(Optional nextCursor); + + _FinalStage nextCursor(String nextCursor); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder implements HasMoreStage, _FinalStage { + private boolean hasMore; + + private Optional nextCursor = Optional.empty(); + + private List results = new ArrayList<>(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + @java.lang.Override + public Builder from(ResultsPageResponse other) { + results(other.getResults()); + nextCursor(other.getNextCursor()); + hasMore(other.getHasMore()); + return this; + } + + /** + *

Whether more results remain beyond this page.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("has_more") + public _FinalStage hasMore(boolean hasMore) { + this.hasMore = hasMore; + return this; + } + + /** + *

Cursor for the next page, when has_more is true.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage nextCursor(String nextCursor) { + this.nextCursor = Optional.ofNullable(nextCursor); + return this; + } + + /** + *

Cursor for the next page, when has_more is true.

+ */ + @java.lang.Override + @JsonSetter(value = "next_cursor", nulls = Nulls.SKIP) + public _FinalStage nextCursor(Optional nextCursor) { + this.nextCursor = nextCursor; + return this; + } + + @java.lang.Override + public _FinalStage addAllResults(List results) { + if (results != null) { + this.results.addAll(results); + } + return this; + } + + @java.lang.Override + public _FinalStage addResults(BatchItemStatus results) { + this.results.add(results); + return this; + } + + @java.lang.Override + @JsonSetter(value = "results", nulls = Nulls.SKIP) + public _FinalStage results(List results) { + this.results.clear(); + if (results != null) { + this.results.addAll(results); + } + return this; + } + + @java.lang.Override + public ResultsPageResponse build() { + return new ResultsPageResponse(results, nextCursor, hasMore, additionalProperties); + } + + @java.lang.Override + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + @java.lang.Override + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/UploadItemResponse.java b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/UploadItemResponse.java new file mode 100644 index 00000000..f8cb88ed --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/lang2fhirbatch/types/UploadItemResponse.java @@ -0,0 +1,639 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.lang2fhirbatch.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.phenoml.api.core.ObjectMappers; +import java.time.OffsetDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import org.jetbrains.annotations.NotNull; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = UploadItemResponse.Builder.class) +public final class UploadItemResponse implements IBatchJob { + private final String jobId; + + private final Optional requestId; + + private final BatchJobStatus status; + + private final boolean finalized; + + private final int totalItems; + + private final Optional error; + + private final OffsetDateTime createdAt; + + private final OffsetDateTime updatedAt; + + private final Optional completedAt; + + private final OffsetDateTime expiresAt; + + private final String itemId; + + private final Optional id; + + private final boolean deduplicated; + + private final Map additionalProperties; + + private UploadItemResponse( + String jobId, + Optional requestId, + BatchJobStatus status, + boolean finalized, + int totalItems, + Optional error, + OffsetDateTime createdAt, + OffsetDateTime updatedAt, + Optional completedAt, + OffsetDateTime expiresAt, + String itemId, + Optional id, + boolean deduplicated, + Map additionalProperties) { + this.jobId = jobId; + this.requestId = requestId; + this.status = status; + this.finalized = finalized; + this.totalItems = totalItems; + this.error = error; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + this.completedAt = completedAt; + this.expiresAt = expiresAt; + this.itemId = itemId; + this.id = id; + this.deduplicated = deduplicated; + this.additionalProperties = additionalProperties; + } + + /** + * @return Server-assigned job identifier. + */ + @JsonProperty("job_id") + @java.lang.Override + public String getJobId() { + return jobId; + } + + /** + * @return The idempotency token supplied at create, if any. + */ + @JsonProperty("request_id") + @java.lang.Override + public Optional getRequestId() { + return requestId; + } + + /** + * @return Job status. completed means every item has finished — some may have + * failed, so check counts for the split. failed is a whole-job + * failure (the job could not run at all), distinct from individual item + * failures, which never fail the job. canceled is a caller-requested + * cancellation via POST /lang2fhir/batch/{job_id}/cancel; like the + * other terminal states it keeps any results already produced readable + * for the retention window. + */ + @JsonProperty("status") + public BatchJobStatus getStatus() { + return status; + } + + /** + * @return Whether the job's item set has been sealed. + */ + @JsonProperty("finalized") + @java.lang.Override + public boolean getFinalized() { + return finalized; + } + + /** + * @return The sealed item count. It is 0 until the job is finalized, so an + * upload response always reports 0; poll the job after finalize for the + * real count. + */ + @JsonProperty("total_items") + @java.lang.Override + public int getTotalItems() { + return totalItems; + } + + /** + * @return A whole-job failure. Present only on a failed job. + */ + @JsonProperty("error") + @java.lang.Override + public Optional getError() { + return error; + } + + @JsonProperty("created_at") + @java.lang.Override + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + @JsonProperty("updated_at") + @java.lang.Override + public OffsetDateTime getUpdatedAt() { + return updatedAt; + } + + /** + * @return When the job finished. Absent until then. + */ + @JsonProperty("completed_at") + @java.lang.Override + public Optional getCompletedAt() { + return completedAt; + } + + /** + * @return When the job and its stored inputs and results are deleted. Set 7 + * days out, with the clock restarting when the job reaches completed, + * failed, or canceled. At expiry the job's request_id is freed for reuse. + */ + @JsonProperty("expires_at") + @java.lang.Override + public OffsetDateTime getExpiresAt() { + return expiresAt; + } + + /** + * @return The server-assigned ID of the uploaded item; use it to fetch the result. + */ + @JsonProperty("item_id") + public String getItemId() { + return itemId; + } + + /** + * @return The caller's correlation label, echoed back; omitted when none was supplied. + */ + @JsonProperty("id") + public Optional getId() { + return id; + } + + /** + * @return True only when a repeated request_id resolved to an item whose + * payload matched the one already stored. A same-token upload with a + * changed payload overwrites in place and returns false. + */ + @JsonProperty("deduplicated") + public boolean getDeduplicated() { + return deduplicated; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof UploadItemResponse && equalTo((UploadItemResponse) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(UploadItemResponse other) { + return jobId.equals(other.jobId) + && requestId.equals(other.requestId) + && status.equals(other.status) + && finalized == other.finalized + && totalItems == other.totalItems + && error.equals(other.error) + && createdAt.equals(other.createdAt) + && updatedAt.equals(other.updatedAt) + && completedAt.equals(other.completedAt) + && expiresAt.equals(other.expiresAt) + && itemId.equals(other.itemId) + && id.equals(other.id) + && deduplicated == other.deduplicated; + } + + @java.lang.Override + public int hashCode() { + return Objects.hash( + this.jobId, + this.requestId, + this.status, + this.finalized, + this.totalItems, + this.error, + this.createdAt, + this.updatedAt, + this.completedAt, + this.expiresAt, + this.itemId, + this.id, + this.deduplicated); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static JobIdStage builder() { + return new Builder(); + } + + public interface JobIdStage { + /** + *

Server-assigned job identifier.

+ */ + StatusStage jobId(@NotNull String jobId); + + Builder from(UploadItemResponse other); + } + + public interface StatusStage { + /** + *

Job status. completed means every item has finished — some may have + * failed, so check counts for the split. failed is a whole-job + * failure (the job could not run at all), distinct from individual item + * failures, which never fail the job. canceled is a caller-requested + * cancellation via POST /lang2fhir/batch/{job_id}/cancel; like the + * other terminal states it keeps any results already produced readable + * for the retention window.

+ */ + FinalizedStage status(@NotNull BatchJobStatus status); + } + + public interface FinalizedStage { + /** + *

Whether the job's item set has been sealed.

+ */ + TotalItemsStage finalized(boolean finalized); + } + + public interface TotalItemsStage { + /** + *

The sealed item count. It is 0 until the job is finalized, so an + * upload response always reports 0; poll the job after finalize for the + * real count.

+ */ + CreatedAtStage totalItems(int totalItems); + } + + public interface CreatedAtStage { + UpdatedAtStage createdAt(@NotNull OffsetDateTime createdAt); + } + + public interface UpdatedAtStage { + ExpiresAtStage updatedAt(@NotNull OffsetDateTime updatedAt); + } + + public interface ExpiresAtStage { + /** + *

When the job and its stored inputs and results are deleted. Set 7 + * days out, with the clock restarting when the job reaches completed, + * failed, or canceled. At expiry the job's request_id is freed for reuse.

+ */ + ItemIdStage expiresAt(@NotNull OffsetDateTime expiresAt); + } + + public interface ItemIdStage { + /** + *

The server-assigned ID of the uploaded item; use it to fetch the result.

+ */ + DeduplicatedStage itemId(@NotNull String itemId); + } + + public interface DeduplicatedStage { + /** + *

True only when a repeated request_id resolved to an item whose + * payload matched the one already stored. A same-token upload with a + * changed payload overwrites in place and returns false.

+ */ + _FinalStage deduplicated(boolean deduplicated); + } + + public interface _FinalStage { + UploadItemResponse build(); + + _FinalStage additionalProperty(String key, Object value); + + _FinalStage additionalProperties(Map additionalProperties); + + /** + *

The idempotency token supplied at create, if any.

+ */ + _FinalStage requestId(Optional requestId); + + _FinalStage requestId(String requestId); + + /** + *

A whole-job failure. Present only on a failed job.

+ */ + _FinalStage error(Optional error); + + _FinalStage error(BatchError error); + + /** + *

When the job finished. Absent until then.

+ */ + _FinalStage completedAt(Optional completedAt); + + _FinalStage completedAt(OffsetDateTime completedAt); + + /** + *

The caller's correlation label, echoed back; omitted when none was supplied.

+ */ + _FinalStage id(Optional id); + + _FinalStage id(String id); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder + implements JobIdStage, + StatusStage, + FinalizedStage, + TotalItemsStage, + CreatedAtStage, + UpdatedAtStage, + ExpiresAtStage, + ItemIdStage, + DeduplicatedStage, + _FinalStage { + private String jobId; + + private BatchJobStatus status; + + private boolean finalized; + + private int totalItems; + + private OffsetDateTime createdAt; + + private OffsetDateTime updatedAt; + + private OffsetDateTime expiresAt; + + private String itemId; + + private boolean deduplicated; + + private Optional id = Optional.empty(); + + private Optional completedAt = Optional.empty(); + + private Optional error = Optional.empty(); + + private Optional requestId = Optional.empty(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + @java.lang.Override + public Builder from(UploadItemResponse other) { + jobId(other.getJobId()); + requestId(other.getRequestId()); + status(other.getStatus()); + finalized(other.getFinalized()); + totalItems(other.getTotalItems()); + error(other.getError()); + createdAt(other.getCreatedAt()); + updatedAt(other.getUpdatedAt()); + completedAt(other.getCompletedAt()); + expiresAt(other.getExpiresAt()); + itemId(other.getItemId()); + id(other.getId()); + deduplicated(other.getDeduplicated()); + return this; + } + + /** + *

Server-assigned job identifier.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("job_id") + public StatusStage jobId(@NotNull String jobId) { + this.jobId = Objects.requireNonNull(jobId, "jobId must not be null"); + return this; + } + + /** + *

Job status. completed means every item has finished — some may have + * failed, so check counts for the split. failed is a whole-job + * failure (the job could not run at all), distinct from individual item + * failures, which never fail the job. canceled is a caller-requested + * cancellation via POST /lang2fhir/batch/{job_id}/cancel; like the + * other terminal states it keeps any results already produced readable + * for the retention window.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("status") + public FinalizedStage status(@NotNull BatchJobStatus status) { + this.status = Objects.requireNonNull(status, "status must not be null"); + return this; + } + + /** + *

Whether the job's item set has been sealed.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("finalized") + public TotalItemsStage finalized(boolean finalized) { + this.finalized = finalized; + return this; + } + + /** + *

The sealed item count. It is 0 until the job is finalized, so an + * upload response always reports 0; poll the job after finalize for the + * real count.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("total_items") + public CreatedAtStage totalItems(int totalItems) { + this.totalItems = totalItems; + return this; + } + + @java.lang.Override + @JsonSetter("created_at") + public UpdatedAtStage createdAt(@NotNull OffsetDateTime createdAt) { + this.createdAt = Objects.requireNonNull(createdAt, "createdAt must not be null"); + return this; + } + + @java.lang.Override + @JsonSetter("updated_at") + public ExpiresAtStage updatedAt(@NotNull OffsetDateTime updatedAt) { + this.updatedAt = Objects.requireNonNull(updatedAt, "updatedAt must not be null"); + return this; + } + + /** + *

When the job and its stored inputs and results are deleted. Set 7 + * days out, with the clock restarting when the job reaches completed, + * failed, or canceled. At expiry the job's request_id is freed for reuse.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("expires_at") + public ItemIdStage expiresAt(@NotNull OffsetDateTime expiresAt) { + this.expiresAt = Objects.requireNonNull(expiresAt, "expiresAt must not be null"); + return this; + } + + /** + *

The server-assigned ID of the uploaded item; use it to fetch the result.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("item_id") + public DeduplicatedStage itemId(@NotNull String itemId) { + this.itemId = Objects.requireNonNull(itemId, "itemId must not be null"); + return this; + } + + /** + *

True only when a repeated request_id resolved to an item whose + * payload matched the one already stored. A same-token upload with a + * changed payload overwrites in place and returns false.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("deduplicated") + public _FinalStage deduplicated(boolean deduplicated) { + this.deduplicated = deduplicated; + return this; + } + + /** + *

The caller's correlation label, echoed back; omitted when none was supplied.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage id(String id) { + this.id = Optional.ofNullable(id); + return this; + } + + /** + *

The caller's correlation label, echoed back; omitted when none was supplied.

+ */ + @java.lang.Override + @JsonSetter(value = "id", nulls = Nulls.SKIP) + public _FinalStage id(Optional id) { + this.id = id; + return this; + } + + /** + *

When the job finished. Absent until then.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage completedAt(OffsetDateTime completedAt) { + this.completedAt = Optional.ofNullable(completedAt); + return this; + } + + /** + *

When the job finished. Absent until then.

+ */ + @java.lang.Override + @JsonSetter(value = "completed_at", nulls = Nulls.SKIP) + public _FinalStage completedAt(Optional completedAt) { + this.completedAt = completedAt; + return this; + } + + /** + *

A whole-job failure. Present only on a failed job.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage error(BatchError error) { + this.error = Optional.ofNullable(error); + return this; + } + + /** + *

A whole-job failure. Present only on a failed job.

+ */ + @java.lang.Override + @JsonSetter(value = "error", nulls = Nulls.SKIP) + public _FinalStage error(Optional error) { + this.error = error; + return this; + } + + /** + *

The idempotency token supplied at create, if any.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage requestId(String requestId) { + this.requestId = Optional.ofNullable(requestId); + return this; + } + + /** + *

The idempotency token supplied at create, if any.

+ */ + @java.lang.Override + @JsonSetter(value = "request_id", nulls = Nulls.SKIP) + public _FinalStage requestId(Optional requestId) { + this.requestId = requestId; + return this; + } + + @java.lang.Override + public UploadItemResponse build() { + return new UploadItemResponse( + jobId, + requestId, + status, + finalized, + totalItems, + error, + createdAt, + updatedAt, + completedAt, + expiresAt, + itemId, + id, + deduplicated, + additionalProperties); + } + + @java.lang.Override + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + @java.lang.Override + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/src/main/java/com/phenoml/api/resources/profiles/AsyncProfilesClient.java b/src/main/java/com/phenoml/api/resources/profiles/AsyncProfilesClient.java index ec2f77cc..07282ab2 100644 --- a/src/main/java/com/phenoml/api/resources/profiles/AsyncProfilesClient.java +++ b/src/main/java/com/phenoml/api/resources/profiles/AsyncProfilesClient.java @@ -5,6 +5,7 @@ import com.phenoml.api.core.ClientOptions; import com.phenoml.api.core.Suppliers; +import com.phenoml.api.resources.profiles.versions.AsyncVersionsClient; import java.util.function.Supplier; public class AsyncProfilesClient { @@ -12,13 +13,20 @@ public class AsyncProfilesClient { protected final Supplier profilesClient; + protected final Supplier versionsClient; + public AsyncProfilesClient(ClientOptions clientOptions) { this.clientOptions = clientOptions; this.profilesClient = Suppliers.memoize( () -> new com.phenoml.api.resources.profiles.profiles.AsyncProfilesClient(clientOptions)); + this.versionsClient = Suppliers.memoize(() -> new AsyncVersionsClient(clientOptions)); } public com.phenoml.api.resources.profiles.profiles.AsyncProfilesClient profiles() { return this.profilesClient.get(); } + + public AsyncVersionsClient versions() { + return this.versionsClient.get(); + } } diff --git a/src/main/java/com/phenoml/api/resources/profiles/ProfilesClient.java b/src/main/java/com/phenoml/api/resources/profiles/ProfilesClient.java index 23b6a0c6..b6f60dab 100644 --- a/src/main/java/com/phenoml/api/resources/profiles/ProfilesClient.java +++ b/src/main/java/com/phenoml/api/resources/profiles/ProfilesClient.java @@ -5,6 +5,7 @@ import com.phenoml.api.core.ClientOptions; import com.phenoml.api.core.Suppliers; +import com.phenoml.api.resources.profiles.versions.VersionsClient; import java.util.function.Supplier; public class ProfilesClient { @@ -12,13 +13,20 @@ public class ProfilesClient { protected final Supplier profilesClient; + protected final Supplier versionsClient; + public ProfilesClient(ClientOptions clientOptions) { this.clientOptions = clientOptions; this.profilesClient = Suppliers.memoize(() -> new com.phenoml.api.resources.profiles.profiles.ProfilesClient(clientOptions)); + this.versionsClient = Suppliers.memoize(() -> new VersionsClient(clientOptions)); } public com.phenoml.api.resources.profiles.profiles.ProfilesClient profiles() { return this.profilesClient.get(); } + + public VersionsClient versions() { + return this.versionsClient.get(); + } } diff --git a/src/main/java/com/phenoml/api/resources/profiles/errors/ConflictError.java b/src/main/java/com/phenoml/api/resources/profiles/errors/ConflictError.java new file mode 100644 index 00000000..b74ca10e --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/profiles/errors/ConflictError.java @@ -0,0 +1,32 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.profiles.errors; + +import com.phenoml.api.core.PhenomlClientApiException; +import okhttp3.Response; + +public final class ConflictError extends PhenomlClientApiException { + /** + * The body of the response that triggered the exception. + */ + private final Object body; + + public ConflictError(Object body) { + super("ConflictError", 409, body); + this.body = body; + } + + public ConflictError(Object body, Response rawResponse) { + super("ConflictError", 409, body, rawResponse); + this.body = body; + } + + /** + * @return the body + */ + @java.lang.Override + public Object body() { + return this.body; + } +} diff --git a/src/main/java/com/phenoml/api/resources/profiles/profiles/AsyncProfilesClient.java b/src/main/java/com/phenoml/api/resources/profiles/profiles/AsyncProfilesClient.java index 34d2d24b..503fe9bd 100644 --- a/src/main/java/com/phenoml/api/resources/profiles/profiles/AsyncProfilesClient.java +++ b/src/main/java/com/phenoml/api/resources/profiles/profiles/AsyncProfilesClient.java @@ -36,7 +36,12 @@ public AsyncRawProfilesClient withRawResponse() { *

The url query parameter filters by canonical URL. The canonical URL is the * stable key other platform features use to reference a profile (FHIR's * meta.profile, baseDefinition), since StructureDefinition ids are only - * unique within a package. A non-matching filter returns an empty list, not a 404.

+ * unique within a package. An unpinned url filter returns metadata for + * the profile's current StructureDefinition. Pinned url|version filters + * resolve a retained version when present; otherwise they can fall back to + * the profile's current StructureDefinition, whose content can change + * through the profile update endpoint. A non-matching filter returns an + * empty list, not a 404.

*/ public CompletableFuture list() { return this.rawClient.list().thenApply(response -> response.body()); @@ -49,7 +54,12 @@ public CompletableFuture list() { *

The url query parameter filters by canonical URL. The canonical URL is the * stable key other platform features use to reference a profile (FHIR's * meta.profile, baseDefinition), since StructureDefinition ids are only - * unique within a package. A non-matching filter returns an empty list, not a 404.

+ * unique within a package. An unpinned url filter returns metadata for + * the profile's current StructureDefinition. Pinned url|version filters + * resolve a retained version when present; otherwise they can fall back to + * the profile's current StructureDefinition, whose content can change + * through the profile update endpoint. A non-matching filter returns an + * empty list, not a 404.

*/ public CompletableFuture list(RequestOptions requestOptions) { return this.rawClient.list(requestOptions).thenApply(response -> response.body()); @@ -62,7 +72,12 @@ public CompletableFuture list(RequestOptions requestOptions *

The url query parameter filters by canonical URL. The canonical URL is the * stable key other platform features use to reference a profile (FHIR's * meta.profile, baseDefinition), since StructureDefinition ids are only - * unique within a package. A non-matching filter returns an empty list, not a 404.

+ * unique within a package. An unpinned url filter returns metadata for + * the profile's current StructureDefinition. Pinned url|version filters + * resolve a retained version when present; otherwise they can fall back to + * the profile's current StructureDefinition, whose content can change + * through the profile update endpoint. A non-matching filter returns an + * empty list, not a 404.

*/ public CompletableFuture list(ListRequest request) { return this.rawClient.list(request).thenApply(response -> response.body()); @@ -75,7 +90,12 @@ public CompletableFuture list(ListRequest request) { *

The url query parameter filters by canonical URL. The canonical URL is the * stable key other platform features use to reference a profile (FHIR's * meta.profile, baseDefinition), since StructureDefinition ids are only - * unique within a package. A non-matching filter returns an empty list, not a 404.

+ * unique within a package. An unpinned url filter returns metadata for + * the profile's current StructureDefinition. Pinned url|version filters + * resolve a retained version when present; otherwise they can fall back to + * the profile's current StructureDefinition, whose content can change + * through the profile update endpoint. A non-matching filter returns an + * empty list, not a 404.

*/ public CompletableFuture list(ListRequest request, RequestOptions requestOptions) { return this.rawClient.list(request, requestOptions).thenApply(response -> response.body()); @@ -85,9 +105,8 @@ public CompletableFuture list(ListRequest request, RequestO * Creates a custom profile from a FHIR StructureDefinition supplied as a JSON * object. Metadata such as version, resource type, and url is read from the * StructureDefinition; the lowercase StructureDefinition id becomes the - * profile's lookup key. When id is omitted, a random UUID is assigned. Code - * system configuration is auto-extracted from the snapshot. Optionally group - * the profile under a named implementation guide. + * profile's lookup key. When id is omitted, a random UUID is assigned. + * Optionally group the profile under a named implementation guide. */ public CompletableFuture create(ProfileUploadRequest request) { return this.rawClient.create(request).thenApply(response -> response.body()); @@ -97,23 +116,24 @@ public CompletableFuture create(ProfileUploadRequest request) { * Creates a custom profile from a FHIR StructureDefinition supplied as a JSON * object. Metadata such as version, resource type, and url is read from the * StructureDefinition; the lowercase StructureDefinition id becomes the - * profile's lookup key. When id is omitted, a random UUID is assigned. Code - * system configuration is auto-extracted from the snapshot. Optionally group - * the profile under a named implementation guide. + * profile's lookup key. When id is omitted, a random UUID is assigned. + * Optionally group the profile under a named implementation guide. */ public CompletableFuture create(ProfileUploadRequest request, RequestOptions requestOptions) { return this.rawClient.create(request, requestOptions).thenApply(response -> response.body()); } /** - * Returns a single custom profile by id, including its full StructureDefinition JSON. + * Returns a single custom profile by id, including its full StructureDefinition + * JSON. */ public CompletableFuture get(String id) { return this.rawClient.get(id).thenApply(response -> response.body()); } /** - * Returns a single custom profile by id, including its full StructureDefinition JSON. + * Returns a single custom profile by id, including its full StructureDefinition + * JSON. */ public CompletableFuture get(String id, RequestOptions requestOptions) { return this.rawClient.get(id, requestOptions).thenApply(response -> response.body()); @@ -124,10 +144,12 @@ public CompletableFuture get(String id, RequestOptions reque * id path parameter is authoritative: if the StructureDefinition includes * an id it must match the path parameter, and if it omits one the path * parameter is used. The FHIR resource type of the profile cannot change. - * Code system configuration is - * re-derived from the new StructureDefinition. When implementation_guide is - * omitted, the profile keeps its existing implementation guide. The instance - * stores a single version per canonical URL, so this replaces it in place. + * When implementation_guide is omitted, the profile keeps its existing + * implementation guide. A retained version string is allowed only when + * re-submitting the profile's current version with an unchanged + * StructureDefinition; otherwise it returns a conflict. While the profile + * has retained versions, its + * canonical URL cannot be changed. */ public CompletableFuture update(String id, ProfileUploadRequest request) { return this.rawClient.update(id, request).thenApply(response -> response.body()); @@ -138,10 +160,12 @@ public CompletableFuture update(String id, ProfileUploadRequest * id path parameter is authoritative: if the StructureDefinition includes * an id it must match the path parameter, and if it omits one the path * parameter is used. The FHIR resource type of the profile cannot change. - * Code system configuration is - * re-derived from the new StructureDefinition. When implementation_guide is - * omitted, the profile keeps its existing implementation guide. The instance - * stores a single version per canonical URL, so this replaces it in place. + * When implementation_guide is omitted, the profile keeps its existing + * implementation guide. A retained version string is allowed only when + * re-submitting the profile's current version with an unchanged + * StructureDefinition; otherwise it returns a conflict. While the profile + * has retained versions, its + * canonical URL cannot be changed. */ public CompletableFuture update( String id, ProfileUploadRequest request, RequestOptions requestOptions) { @@ -149,14 +173,18 @@ public CompletableFuture update( } /** - * Permanently deletes a custom profile by id. + * Permanently deletes a custom profile by id. This also deletes all retained + * versions for that profile so the canonical URL can be reused by a later + * upload. */ public CompletableFuture delete(String id) { return this.rawClient.delete(id).thenApply(response -> response.body()); } /** - * Permanently deletes a custom profile by id. + * Permanently deletes a custom profile by id. This also deletes all retained + * versions for that profile so the canonical URL can be reused by a later + * upload. */ public CompletableFuture delete(String id, RequestOptions requestOptions) { return this.rawClient.delete(id, requestOptions).thenApply(response -> response.body()); diff --git a/src/main/java/com/phenoml/api/resources/profiles/profiles/AsyncRawProfilesClient.java b/src/main/java/com/phenoml/api/resources/profiles/profiles/AsyncRawProfilesClient.java index 7f7dd525..18e98e37 100644 --- a/src/main/java/com/phenoml/api/resources/profiles/profiles/AsyncRawProfilesClient.java +++ b/src/main/java/com/phenoml/api/resources/profiles/profiles/AsyncRawProfilesClient.java @@ -14,6 +14,7 @@ import com.phenoml.api.core.RequestOptions; import com.phenoml.api.core.RetryInterceptor; import com.phenoml.api.resources.profiles.errors.BadRequestError; +import com.phenoml.api.resources.profiles.errors.ConflictError; import com.phenoml.api.resources.profiles.errors.ForbiddenError; import com.phenoml.api.resources.profiles.errors.InternalServerError; import com.phenoml.api.resources.profiles.errors.NotFoundError; @@ -50,7 +51,12 @@ public AsyncRawProfilesClient(ClientOptions clientOptions) { *

The url query parameter filters by canonical URL. The canonical URL is the * stable key other platform features use to reference a profile (FHIR's * meta.profile, baseDefinition), since StructureDefinition ids are only - * unique within a package. A non-matching filter returns an empty list, not a 404.

+ * unique within a package. An unpinned url filter returns metadata for + * the profile's current StructureDefinition. Pinned url|version filters + * resolve a retained version when present; otherwise they can fall back to + * the profile's current StructureDefinition, whose content can change + * through the profile update endpoint. A non-matching filter returns an + * empty list, not a 404.

*/ public CompletableFuture> list() { return list(ListRequest.builder().build()); @@ -63,7 +69,12 @@ public CompletableFuture> list() *

The url query parameter filters by canonical URL. The canonical URL is the * stable key other platform features use to reference a profile (FHIR's * meta.profile, baseDefinition), since StructureDefinition ids are only - * unique within a package. A non-matching filter returns an empty list, not a 404.

+ * unique within a package. An unpinned url filter returns metadata for + * the profile's current StructureDefinition. Pinned url|version filters + * resolve a retained version when present; otherwise they can fall back to + * the profile's current StructureDefinition, whose content can change + * through the profile update endpoint. A non-matching filter returns an + * empty list, not a 404.

*/ public CompletableFuture> list(RequestOptions requestOptions) { return list(ListRequest.builder().build(), requestOptions); @@ -76,7 +87,12 @@ public CompletableFuture> list(Re *

The url query parameter filters by canonical URL. The canonical URL is the * stable key other platform features use to reference a profile (FHIR's * meta.profile, baseDefinition), since StructureDefinition ids are only - * unique within a package. A non-matching filter returns an empty list, not a 404.

+ * unique within a package. An unpinned url filter returns metadata for + * the profile's current StructureDefinition. Pinned url|version filters + * resolve a retained version when present; otherwise they can fall back to + * the profile's current StructureDefinition, whose content can change + * through the profile update endpoint. A non-matching filter returns an + * empty list, not a 404.

*/ public CompletableFuture> list(ListRequest request) { return list(request, null); @@ -89,7 +105,12 @@ public CompletableFuture> list(Li *

The url query parameter filters by canonical URL. The canonical URL is the * stable key other platform features use to reference a profile (FHIR's * meta.profile, baseDefinition), since StructureDefinition ids are only - * unique within a package. A non-matching filter returns an empty list, not a 404.

+ * unique within a package. An unpinned url filter returns metadata for + * the profile's current StructureDefinition. Pinned url|version filters + * resolve a retained version when present; otherwise they can fall back to + * the profile's current StructureDefinition, whose content can change + * through the profile update endpoint. A non-matching filter returns an + * empty list, not a 404.

*/ public CompletableFuture> list( ListRequest request, RequestOptions requestOptions) { @@ -180,9 +201,8 @@ public void onFailure(@NotNull Call call, @NotNull IOException e) { * Creates a custom profile from a FHIR StructureDefinition supplied as a JSON * object. Metadata such as version, resource type, and url is read from the * StructureDefinition; the lowercase StructureDefinition id becomes the - * profile's lookup key. When id is omitted, a random UUID is assigned. Code - * system configuration is auto-extracted from the snapshot. Optionally group - * the profile under a named implementation guide. + * profile's lookup key. When id is omitted, a random UUID is assigned. + * Optionally group the profile under a named implementation guide. */ public CompletableFuture> create(ProfileUploadRequest request) { return create(request, null); @@ -192,9 +212,8 @@ public CompletableFuture> create(Profi * Creates a custom profile from a FHIR StructureDefinition supplied as a JSON * object. Metadata such as version, resource type, and url is read from the * StructureDefinition; the lowercase StructureDefinition id becomes the - * profile's lookup key. When id is omitted, a random UUID is assigned. Code - * system configuration is auto-extracted from the snapshot. Optionally group - * the profile under a named implementation guide. + * profile's lookup key. When id is omitted, a random UUID is assigned. + * Optionally group the profile under a named implementation guide. */ public CompletableFuture> create( ProfileUploadRequest request, RequestOptions requestOptions) { @@ -292,14 +311,16 @@ public void onFailure(@NotNull Call call, @NotNull IOException e) { } /** - * Returns a single custom profile by id, including its full StructureDefinition JSON. + * Returns a single custom profile by id, including its full StructureDefinition + * JSON. */ public CompletableFuture> get(String id) { return get(id, null); } /** - * Returns a single custom profile by id, including its full StructureDefinition JSON. + * Returns a single custom profile by id, including its full StructureDefinition + * JSON. */ public CompletableFuture> get( String id, RequestOptions requestOptions) { @@ -399,10 +420,12 @@ public void onFailure(@NotNull Call call, @NotNull IOException e) { * id path parameter is authoritative: if the StructureDefinition includes * an id it must match the path parameter, and if it omits one the path * parameter is used. The FHIR resource type of the profile cannot change. - * Code system configuration is - * re-derived from the new StructureDefinition. When implementation_guide is - * omitted, the profile keeps its existing implementation guide. The instance - * stores a single version per canonical URL, so this replaces it in place. + * When implementation_guide is omitted, the profile keeps its existing + * implementation guide. A retained version string is allowed only when + * re-submitting the profile's current version with an unchanged + * StructureDefinition; otherwise it returns a conflict. While the profile + * has retained versions, its + * canonical URL cannot be changed. */ public CompletableFuture> update( String id, ProfileUploadRequest request) { @@ -414,10 +437,12 @@ public CompletableFuture> update( * id path parameter is authoritative: if the StructureDefinition includes * an id it must match the path parameter, and if it omits one the path * parameter is used. The FHIR resource type of the profile cannot change. - * Code system configuration is - * re-derived from the new StructureDefinition. When implementation_guide is - * omitted, the profile keeps its existing implementation guide. The instance - * stores a single version per canonical URL, so this replaces it in place. + * When implementation_guide is omitted, the profile keeps its existing + * implementation guide. A retained version string is allowed only when + * re-submitting the profile's current version with an unchanged + * StructureDefinition; otherwise it returns a conflict. While the profile + * has retained versions, its + * canonical URL cannot be changed. */ public CompletableFuture> update( String id, ProfileUploadRequest request, RequestOptions requestOptions) { @@ -491,6 +516,11 @@ public void onResponse(@NotNull Call call, @NotNull Response response) throws IO ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response)); return; + case 409: + future.completeExceptionally(new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; case 500: future.completeExceptionally(new InternalServerError( ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), @@ -521,14 +551,18 @@ public void onFailure(@NotNull Call call, @NotNull IOException e) { } /** - * Permanently deletes a custom profile by id. + * Permanently deletes a custom profile by id. This also deletes all retained + * versions for that profile so the canonical URL can be reused by a later + * upload. */ public CompletableFuture> delete(String id) { return delete(id, null); } /** - * Permanently deletes a custom profile by id. + * Permanently deletes a custom profile by id. This also deletes all retained + * versions for that profile so the canonical URL can be reused by a later + * upload. */ public CompletableFuture> delete(String id, RequestOptions requestOptions) { HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) @@ -591,6 +625,11 @@ public void onResponse(@NotNull Call call, @NotNull Response response) throws IO ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response)); return; + case 409: + future.completeExceptionally(new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; case 500: future.completeExceptionally(new InternalServerError( ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), diff --git a/src/main/java/com/phenoml/api/resources/profiles/profiles/ProfilesClient.java b/src/main/java/com/phenoml/api/resources/profiles/profiles/ProfilesClient.java index e8315886..7f6aeeef 100644 --- a/src/main/java/com/phenoml/api/resources/profiles/profiles/ProfilesClient.java +++ b/src/main/java/com/phenoml/api/resources/profiles/profiles/ProfilesClient.java @@ -35,7 +35,12 @@ public RawProfilesClient withRawResponse() { *

The url query parameter filters by canonical URL. The canonical URL is the * stable key other platform features use to reference a profile (FHIR's * meta.profile, baseDefinition), since StructureDefinition ids are only - * unique within a package. A non-matching filter returns an empty list, not a 404.

+ * unique within a package. An unpinned url filter returns metadata for + * the profile's current StructureDefinition. Pinned url|version filters + * resolve a retained version when present; otherwise they can fall back to + * the profile's current StructureDefinition, whose content can change + * through the profile update endpoint. A non-matching filter returns an + * empty list, not a 404.

*/ public ProfileListResponse list() { return this.rawClient.list().body(); @@ -48,7 +53,12 @@ public ProfileListResponse list() { *

The url query parameter filters by canonical URL. The canonical URL is the * stable key other platform features use to reference a profile (FHIR's * meta.profile, baseDefinition), since StructureDefinition ids are only - * unique within a package. A non-matching filter returns an empty list, not a 404.

+ * unique within a package. An unpinned url filter returns metadata for + * the profile's current StructureDefinition. Pinned url|version filters + * resolve a retained version when present; otherwise they can fall back to + * the profile's current StructureDefinition, whose content can change + * through the profile update endpoint. A non-matching filter returns an + * empty list, not a 404.

*/ public ProfileListResponse list(RequestOptions requestOptions) { return this.rawClient.list(requestOptions).body(); @@ -61,7 +71,12 @@ public ProfileListResponse list(RequestOptions requestOptions) { *

The url query parameter filters by canonical URL. The canonical URL is the * stable key other platform features use to reference a profile (FHIR's * meta.profile, baseDefinition), since StructureDefinition ids are only - * unique within a package. A non-matching filter returns an empty list, not a 404.

+ * unique within a package. An unpinned url filter returns metadata for + * the profile's current StructureDefinition. Pinned url|version filters + * resolve a retained version when present; otherwise they can fall back to + * the profile's current StructureDefinition, whose content can change + * through the profile update endpoint. A non-matching filter returns an + * empty list, not a 404.

*/ public ProfileListResponse list(ListRequest request) { return this.rawClient.list(request).body(); @@ -74,7 +89,12 @@ public ProfileListResponse list(ListRequest request) { *

The url query parameter filters by canonical URL. The canonical URL is the * stable key other platform features use to reference a profile (FHIR's * meta.profile, baseDefinition), since StructureDefinition ids are only - * unique within a package. A non-matching filter returns an empty list, not a 404.

+ * unique within a package. An unpinned url filter returns metadata for + * the profile's current StructureDefinition. Pinned url|version filters + * resolve a retained version when present; otherwise they can fall back to + * the profile's current StructureDefinition, whose content can change + * through the profile update endpoint. A non-matching filter returns an + * empty list, not a 404.

*/ public ProfileListResponse list(ListRequest request, RequestOptions requestOptions) { return this.rawClient.list(request, requestOptions).body(); @@ -84,9 +104,8 @@ public ProfileListResponse list(ListRequest request, RequestOptions requestOptio * Creates a custom profile from a FHIR StructureDefinition supplied as a JSON * object. Metadata such as version, resource type, and url is read from the * StructureDefinition; the lowercase StructureDefinition id becomes the - * profile's lookup key. When id is omitted, a random UUID is assigned. Code - * system configuration is auto-extracted from the snapshot. Optionally group - * the profile under a named implementation guide. + * profile's lookup key. When id is omitted, a random UUID is assigned. + * Optionally group the profile under a named implementation guide. */ public ProfileSummary create(ProfileUploadRequest request) { return this.rawClient.create(request).body(); @@ -96,23 +115,24 @@ public ProfileSummary create(ProfileUploadRequest request) { * Creates a custom profile from a FHIR StructureDefinition supplied as a JSON * object. Metadata such as version, resource type, and url is read from the * StructureDefinition; the lowercase StructureDefinition id becomes the - * profile's lookup key. When id is omitted, a random UUID is assigned. Code - * system configuration is auto-extracted from the snapshot. Optionally group - * the profile under a named implementation guide. + * profile's lookup key. When id is omitted, a random UUID is assigned. + * Optionally group the profile under a named implementation guide. */ public ProfileSummary create(ProfileUploadRequest request, RequestOptions requestOptions) { return this.rawClient.create(request, requestOptions).body(); } /** - * Returns a single custom profile by id, including its full StructureDefinition JSON. + * Returns a single custom profile by id, including its full StructureDefinition + * JSON. */ public ProfileGetResponse get(String id) { return this.rawClient.get(id).body(); } /** - * Returns a single custom profile by id, including its full StructureDefinition JSON. + * Returns a single custom profile by id, including its full StructureDefinition + * JSON. */ public ProfileGetResponse get(String id, RequestOptions requestOptions) { return this.rawClient.get(id, requestOptions).body(); @@ -123,10 +143,12 @@ public ProfileGetResponse get(String id, RequestOptions requestOptions) { * id path parameter is authoritative: if the StructureDefinition includes * an id it must match the path parameter, and if it omits one the path * parameter is used. The FHIR resource type of the profile cannot change. - * Code system configuration is - * re-derived from the new StructureDefinition. When implementation_guide is - * omitted, the profile keeps its existing implementation guide. The instance - * stores a single version per canonical URL, so this replaces it in place. + * When implementation_guide is omitted, the profile keeps its existing + * implementation guide. A retained version string is allowed only when + * re-submitting the profile's current version with an unchanged + * StructureDefinition; otherwise it returns a conflict. While the profile + * has retained versions, its + * canonical URL cannot be changed. */ public ProfileSummary update(String id, ProfileUploadRequest request) { return this.rawClient.update(id, request).body(); @@ -137,24 +159,30 @@ public ProfileSummary update(String id, ProfileUploadRequest request) { * id path parameter is authoritative: if the StructureDefinition includes * an id it must match the path parameter, and if it omits one the path * parameter is used. The FHIR resource type of the profile cannot change. - * Code system configuration is - * re-derived from the new StructureDefinition. When implementation_guide is - * omitted, the profile keeps its existing implementation guide. The instance - * stores a single version per canonical URL, so this replaces it in place. + * When implementation_guide is omitted, the profile keeps its existing + * implementation guide. A retained version string is allowed only when + * re-submitting the profile's current version with an unchanged + * StructureDefinition; otherwise it returns a conflict. While the profile + * has retained versions, its + * canonical URL cannot be changed. */ public ProfileSummary update(String id, ProfileUploadRequest request, RequestOptions requestOptions) { return this.rawClient.update(id, request, requestOptions).body(); } /** - * Permanently deletes a custom profile by id. + * Permanently deletes a custom profile by id. This also deletes all retained + * versions for that profile so the canonical URL can be reused by a later + * upload. */ public void delete(String id) { this.rawClient.delete(id).body(); } /** - * Permanently deletes a custom profile by id. + * Permanently deletes a custom profile by id. This also deletes all retained + * versions for that profile so the canonical URL can be reused by a later + * upload. */ public void delete(String id, RequestOptions requestOptions) { this.rawClient.delete(id, requestOptions).body(); diff --git a/src/main/java/com/phenoml/api/resources/profiles/profiles/RawProfilesClient.java b/src/main/java/com/phenoml/api/resources/profiles/profiles/RawProfilesClient.java index d8f9ae7f..182b9ac3 100644 --- a/src/main/java/com/phenoml/api/resources/profiles/profiles/RawProfilesClient.java +++ b/src/main/java/com/phenoml/api/resources/profiles/profiles/RawProfilesClient.java @@ -14,6 +14,7 @@ import com.phenoml.api.core.RequestOptions; import com.phenoml.api.core.RetryInterceptor; import com.phenoml.api.resources.profiles.errors.BadRequestError; +import com.phenoml.api.resources.profiles.errors.ConflictError; import com.phenoml.api.resources.profiles.errors.ForbiddenError; import com.phenoml.api.resources.profiles.errors.InternalServerError; import com.phenoml.api.resources.profiles.errors.NotFoundError; @@ -46,7 +47,12 @@ public RawProfilesClient(ClientOptions clientOptions) { *

The url query parameter filters by canonical URL. The canonical URL is the * stable key other platform features use to reference a profile (FHIR's * meta.profile, baseDefinition), since StructureDefinition ids are only - * unique within a package. A non-matching filter returns an empty list, not a 404.

+ * unique within a package. An unpinned url filter returns metadata for + * the profile's current StructureDefinition. Pinned url|version filters + * resolve a retained version when present; otherwise they can fall back to + * the profile's current StructureDefinition, whose content can change + * through the profile update endpoint. A non-matching filter returns an + * empty list, not a 404.

*/ public PhenomlClientHttpResponse list() { return list(ListRequest.builder().build()); @@ -59,7 +65,12 @@ public PhenomlClientHttpResponse list() { *

The url query parameter filters by canonical URL. The canonical URL is the * stable key other platform features use to reference a profile (FHIR's * meta.profile, baseDefinition), since StructureDefinition ids are only - * unique within a package. A non-matching filter returns an empty list, not a 404.

+ * unique within a package. An unpinned url filter returns metadata for + * the profile's current StructureDefinition. Pinned url|version filters + * resolve a retained version when present; otherwise they can fall back to + * the profile's current StructureDefinition, whose content can change + * through the profile update endpoint. A non-matching filter returns an + * empty list, not a 404.

*/ public PhenomlClientHttpResponse list(RequestOptions requestOptions) { return list(ListRequest.builder().build(), requestOptions); @@ -72,7 +83,12 @@ public PhenomlClientHttpResponse list(RequestOptions reques *

The url query parameter filters by canonical URL. The canonical URL is the * stable key other platform features use to reference a profile (FHIR's * meta.profile, baseDefinition), since StructureDefinition ids are only - * unique within a package. A non-matching filter returns an empty list, not a 404.

+ * unique within a package. An unpinned url filter returns metadata for + * the profile's current StructureDefinition. Pinned url|version filters + * resolve a retained version when present; otherwise they can fall back to + * the profile's current StructureDefinition, whose content can change + * through the profile update endpoint. A non-matching filter returns an + * empty list, not a 404.

*/ public PhenomlClientHttpResponse list(ListRequest request) { return list(request, null); @@ -85,7 +101,12 @@ public PhenomlClientHttpResponse list(ListRequest request) *

The url query parameter filters by canonical URL. The canonical URL is the * stable key other platform features use to reference a profile (FHIR's * meta.profile, baseDefinition), since StructureDefinition ids are only - * unique within a package. A non-matching filter returns an empty list, not a 404.

+ * unique within a package. An unpinned url filter returns metadata for + * the profile's current StructureDefinition. Pinned url|version filters + * resolve a retained version when present; otherwise they can fall back to + * the profile's current StructureDefinition, whose content can change + * through the profile update endpoint. A non-matching filter returns an + * empty list, not a 404.

*/ public PhenomlClientHttpResponse list(ListRequest request, RequestOptions requestOptions) { HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) @@ -154,9 +175,8 @@ public PhenomlClientHttpResponse list(ListRequest request, * Creates a custom profile from a FHIR StructureDefinition supplied as a JSON * object. Metadata such as version, resource type, and url is read from the * StructureDefinition; the lowercase StructureDefinition id becomes the - * profile's lookup key. When id is omitted, a random UUID is assigned. Code - * system configuration is auto-extracted from the snapshot. Optionally group - * the profile under a named implementation guide. + * profile's lookup key. When id is omitted, a random UUID is assigned. + * Optionally group the profile under a named implementation guide. */ public PhenomlClientHttpResponse create(ProfileUploadRequest request) { return create(request, null); @@ -166,9 +186,8 @@ public PhenomlClientHttpResponse create(ProfileUploadRequest req * Creates a custom profile from a FHIR StructureDefinition supplied as a JSON * object. Metadata such as version, resource type, and url is read from the * StructureDefinition; the lowercase StructureDefinition id becomes the - * profile's lookup key. When id is omitted, a random UUID is assigned. Code - * system configuration is auto-extracted from the snapshot. Optionally group - * the profile under a named implementation guide. + * profile's lookup key. When id is omitted, a random UUID is assigned. + * Optionally group the profile under a named implementation guide. */ public PhenomlClientHttpResponse create( ProfileUploadRequest request, RequestOptions requestOptions) { @@ -243,14 +262,16 @@ public PhenomlClientHttpResponse create( } /** - * Returns a single custom profile by id, including its full StructureDefinition JSON. + * Returns a single custom profile by id, including its full StructureDefinition + * JSON. */ public PhenomlClientHttpResponse get(String id) { return get(id, null); } /** - * Returns a single custom profile by id, including its full StructureDefinition JSON. + * Returns a single custom profile by id, including its full StructureDefinition + * JSON. */ public PhenomlClientHttpResponse get(String id, RequestOptions requestOptions) { HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) @@ -324,10 +345,12 @@ public PhenomlClientHttpResponse get(String id, RequestOptio * id path parameter is authoritative: if the StructureDefinition includes * an id it must match the path parameter, and if it omits one the path * parameter is used. The FHIR resource type of the profile cannot change. - * Code system configuration is - * re-derived from the new StructureDefinition. When implementation_guide is - * omitted, the profile keeps its existing implementation guide. The instance - * stores a single version per canonical URL, so this replaces it in place. + * When implementation_guide is omitted, the profile keeps its existing + * implementation guide. A retained version string is allowed only when + * re-submitting the profile's current version with an unchanged + * StructureDefinition; otherwise it returns a conflict. While the profile + * has retained versions, its + * canonical URL cannot be changed. */ public PhenomlClientHttpResponse update(String id, ProfileUploadRequest request) { return update(id, request, null); @@ -338,10 +361,12 @@ public PhenomlClientHttpResponse update(String id, ProfileUpload * id path parameter is authoritative: if the StructureDefinition includes * an id it must match the path parameter, and if it omits one the path * parameter is used. The FHIR resource type of the profile cannot change. - * Code system configuration is - * re-derived from the new StructureDefinition. When implementation_guide is - * omitted, the profile keeps its existing implementation guide. The instance - * stores a single version per canonical URL, so this replaces it in place. + * When implementation_guide is omitted, the profile keeps its existing + * implementation guide. A retained version string is allowed only when + * re-submitting the profile's current version with an unchanged + * StructureDefinition; otherwise it returns a conflict. While the profile + * has retained versions, its + * canonical URL cannot be changed. */ public PhenomlClientHttpResponse update( String id, ProfileUploadRequest request, RequestOptions requestOptions) { @@ -402,6 +427,9 @@ public PhenomlClientHttpResponse update( case 404: throw new NotFoundError( ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 409: + throw new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); case 500: throw new InternalServerError( ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); @@ -420,14 +448,18 @@ public PhenomlClientHttpResponse update( } /** - * Permanently deletes a custom profile by id. + * Permanently deletes a custom profile by id. This also deletes all retained + * versions for that profile so the canonical URL can be reused by a later + * upload. */ public PhenomlClientHttpResponse delete(String id) { return delete(id, null); } /** - * Permanently deletes a custom profile by id. + * Permanently deletes a custom profile by id. This also deletes all retained + * versions for that profile so the canonical URL can be reused by a later + * upload. */ public PhenomlClientHttpResponse delete(String id, RequestOptions requestOptions) { HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) @@ -478,6 +510,9 @@ public PhenomlClientHttpResponse delete(String id, RequestOptions requestO case 404: throw new NotFoundError( ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 409: + throw new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); case 500: throw new InternalServerError( ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); diff --git a/src/main/java/com/phenoml/api/resources/profiles/profiles/requests/ListRequest.java b/src/main/java/com/phenoml/api/resources/profiles/profiles/requests/ListRequest.java index bef547c8..3ef9e578 100644 --- a/src/main/java/com/phenoml/api/resources/profiles/profiles/requests/ListRequest.java +++ b/src/main/java/com/phenoml/api/resources/profiles/profiles/requests/ListRequest.java @@ -30,7 +30,7 @@ private ListRequest(Optional url, Map additionalProperti } /** - * @return Filter by canonical URL. Accepts the FHIR pinned form url|version (split on the last |); the bare form matches the current version. + * @return Filter by canonical URL. Accepts the FHIR pinned form url|version; without a version pin, returns the profile's current StructureDefinition metadata. */ @JsonProperty("url") public Optional getUrl() { @@ -81,7 +81,7 @@ public Builder from(ListRequest other) { } /** - *

Filter by canonical URL. Accepts the FHIR pinned form url|version (split on the last |); the bare form matches the current version.

+ *

Filter by canonical URL. Accepts the FHIR pinned form url|version; without a version pin, returns the profile's current StructureDefinition metadata.

*/ @JsonSetter(value = "url", nulls = Nulls.SKIP) public Builder url(Optional url) { diff --git a/src/main/java/com/phenoml/api/resources/profiles/types/IProfileSummary.java b/src/main/java/com/phenoml/api/resources/profiles/types/IProfileSummary.java index af2f81da..910fb8a3 100644 --- a/src/main/java/com/phenoml/api/resources/profiles/types/IProfileSummary.java +++ b/src/main/java/com/phenoml/api/resources/profiles/types/IProfileSummary.java @@ -7,21 +7,27 @@ import java.util.Optional; public interface IProfileSummary { - Optional getId(); + String getId(); - Optional getSource(); + ProfileSummarySource getSource(); - Optional getResourceType(); + String getResourceType(); - Optional getUrl(); + String getUrl(); - Optional getVersion(); + String getVersion(); - Optional getFhirVersion(); + Optional getStatus(); - Optional getImplementationGuide(); + Optional getDate(); - Optional getCreatedAt(); + String getCanonical(); - Optional getUpdatedAt(); + String getFhirVersion(); + + String getImplementationGuide(); + + OffsetDateTime getCreatedAt(); + + OffsetDateTime getUpdatedAt(); } diff --git a/src/main/java/com/phenoml/api/resources/profiles/types/ProfileGetResponse.java b/src/main/java/com/phenoml/api/resources/profiles/types/ProfileGetResponse.java index 27d89a62..0354df20 100644 --- a/src/main/java/com/phenoml/api/resources/profiles/types/ProfileGetResponse.java +++ b/src/main/java/com/phenoml/api/resources/profiles/types/ProfileGetResponse.java @@ -14,52 +14,66 @@ import com.phenoml.api.core.ObjectMappers; import java.time.OffsetDateTime; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; +import org.jetbrains.annotations.NotNull; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = ProfileGetResponse.Builder.class) public final class ProfileGetResponse implements IProfileSummary { - private final Optional id; + private final String id; - private final Optional source; + private final ProfileSummarySource source; - private final Optional resourceType; + private final String resourceType; - private final Optional url; + private final String url; - private final Optional version; + private final String version; - private final Optional fhirVersion; + private final Optional status; - private final Optional implementationGuide; + private final Optional date; - private final Optional createdAt; + private final String canonical; - private final Optional updatedAt; + private final String fhirVersion; - private final Optional> structureDefinition; + private final String implementationGuide; + + private final OffsetDateTime createdAt; + + private final OffsetDateTime updatedAt; + + private final Map structureDefinition; private final Map additionalProperties; private ProfileGetResponse( - Optional id, - Optional source, - Optional resourceType, - Optional url, - Optional version, - Optional fhirVersion, - Optional implementationGuide, - Optional createdAt, - Optional updatedAt, - Optional> structureDefinition, + String id, + ProfileSummarySource source, + String resourceType, + String url, + String version, + Optional status, + Optional date, + String canonical, + String fhirVersion, + String implementationGuide, + OffsetDateTime createdAt, + OffsetDateTime updatedAt, + Map structureDefinition, Map additionalProperties) { this.id = id; this.source = source; this.resourceType = resourceType; this.url = url; this.version = version; + this.status = status; + this.date = date; + this.canonical = canonical; this.fhirVersion = fhirVersion; this.implementationGuide = implementationGuide; this.createdAt = createdAt; @@ -73,15 +87,15 @@ private ProfileGetResponse( */ @JsonProperty("id") @java.lang.Override - public Optional getId() { + public String getId() { return id; } /** - * @return The profile's origin. Listings currently return only custom (uploaded) profiles, so this is always "custom" today; built-in (US Core / R4 base) profiles would be surfaced via an opt-in parameter in a future release, not by changing the default behavior. + * @return The profile's origin. Profile management responses currently return custom (uploaded) profiles, so this is always "custom" today. */ @JsonProperty("source") - public Optional getSource() { + public ProfileSummarySource getSource() { return source; } @@ -90,7 +104,7 @@ public Optional getSource() { */ @JsonProperty("resource_type") @java.lang.Override - public Optional getResourceType() { + public String getResourceType() { return resourceType; } @@ -99,7 +113,7 @@ public Optional getResourceType() { */ @JsonProperty("url") @java.lang.Override - public Optional getUrl() { + public String getUrl() { return url; } @@ -108,16 +122,43 @@ public Optional getUrl() { */ @JsonProperty("version") @java.lang.Override - public Optional getVersion() { + public String getVersion() { return version; } + /** + * @return The publication status from StructureDefinition.status. Expected FHIR values include draft, active, retired, and unknown; the server preserves authored strings. + */ + @JsonProperty("status") + @java.lang.Override + public Optional getStatus() { + return status; + } + + /** + * @return The authored publication date from StructureDefinition.date, when present. This is a FHIR dateTime string and may be less precise than a full timestamp. + */ + @JsonProperty("date") + @java.lang.Override + public Optional getDate() { + return date; + } + + /** + * @return The canonical profile reference, including the version pin when present. + */ + @JsonProperty("canonical") + @java.lang.Override + public String getCanonical() { + return canonical; + } + /** * @return The base FHIR version the StructureDefinition targets. */ @JsonProperty("fhir_version") @java.lang.Override - public Optional getFhirVersion() { + public String getFhirVersion() { return fhirVersion; } @@ -126,19 +167,22 @@ public Optional getFhirVersion() { */ @JsonProperty("implementation_guide") @java.lang.Override - public Optional getImplementationGuide() { + public String getImplementationGuide() { return implementationGuide; } @JsonProperty("created_at") @java.lang.Override - public Optional getCreatedAt() { + public OffsetDateTime getCreatedAt() { return createdAt; } + /** + * @return Last update timestamp for the profile's current StructureDefinition. For retained versions, this equals created_at. + */ @JsonProperty("updated_at") @java.lang.Override - public Optional getUpdatedAt() { + public OffsetDateTime getUpdatedAt() { return updatedAt; } @@ -146,7 +190,7 @@ public Optional getUpdatedAt() { * @return The full FHIR StructureDefinition JSON. */ @JsonProperty("structure_definition") - public Optional> getStructureDefinition() { + public Map getStructureDefinition() { return structureDefinition; } @@ -167,6 +211,9 @@ private boolean equalTo(ProfileGetResponse other) { && resourceType.equals(other.resourceType) && url.equals(other.url) && version.equals(other.version) + && status.equals(other.status) + && date.equals(other.date) + && canonical.equals(other.canonical) && fhirVersion.equals(other.fhirVersion) && implementationGuide.equals(other.implementationGuide) && createdAt.equals(other.createdAt) @@ -182,6 +229,9 @@ public int hashCode() { this.resourceType, this.url, this.version, + this.status, + this.date, + this.canonical, this.fhirVersion, this.implementationGuide, this.createdAt, @@ -194,43 +244,164 @@ public String toString() { return ObjectMappers.stringify(this); } - public static Builder builder() { + public static IdStage builder() { return new Builder(); } + public interface IdStage { + /** + *

The lowercase StructureDefinition id, used as the profile's lookup key.

+ */ + SourceStage id(@NotNull String id); + + Builder from(ProfileGetResponse other); + } + + public interface SourceStage { + /** + *

The profile's origin. Profile management responses currently return custom (uploaded) profiles, so this is always "custom" today.

+ */ + ResourceTypeStage source(@NotNull ProfileSummarySource source); + } + + public interface ResourceTypeStage { + /** + *

The FHIR resource type from the StructureDefinition.

+ */ + UrlStage resourceType(@NotNull String resourceType); + } + + public interface UrlStage { + /** + *

The canonical URL from the StructureDefinition.

+ */ + VersionStage url(@NotNull String url); + } + + public interface VersionStage { + /** + *

The version from the StructureDefinition.version field.

+ */ + CanonicalStage version(@NotNull String version); + } + + public interface CanonicalStage { + /** + *

The canonical profile reference, including the version pin when present.

+ */ + FhirVersionStage canonical(@NotNull String canonical); + } + + public interface FhirVersionStage { + /** + *

The base FHIR version the StructureDefinition targets.

+ */ + ImplementationGuideStage fhirVersion(@NotNull String fhirVersion); + } + + public interface ImplementationGuideStage { + /** + *

The implementation guide the profile belongs to.

+ */ + CreatedAtStage implementationGuide(@NotNull String implementationGuide); + } + + public interface CreatedAtStage { + UpdatedAtStage createdAt(@NotNull OffsetDateTime createdAt); + } + + public interface UpdatedAtStage { + /** + *

Last update timestamp for the profile's current StructureDefinition. For retained versions, this equals created_at.

+ */ + _FinalStage updatedAt(@NotNull OffsetDateTime updatedAt); + } + + public interface _FinalStage { + ProfileGetResponse build(); + + _FinalStage additionalProperty(String key, Object value); + + _FinalStage additionalProperties(Map additionalProperties); + + /** + *

The publication status from StructureDefinition.status. Expected FHIR values include draft, active, retired, and unknown; the server preserves authored strings.

+ */ + _FinalStage status(Optional status); + + _FinalStage status(String status); + + /** + *

The authored publication date from StructureDefinition.date, when present. This is a FHIR dateTime string and may be less precise than a full timestamp.

+ */ + _FinalStage date(Optional date); + + _FinalStage date(String date); + + /** + *

The full FHIR StructureDefinition JSON.

+ */ + _FinalStage structureDefinition(Map structureDefinition); + + _FinalStage putAllStructureDefinition(Map structureDefinition); + + _FinalStage structureDefinition(String key, Object value); + } + @JsonIgnoreProperties(ignoreUnknown = true) - public static final class Builder { - private Optional id = Optional.empty(); + public static final class Builder + implements IdStage, + SourceStage, + ResourceTypeStage, + UrlStage, + VersionStage, + CanonicalStage, + FhirVersionStage, + ImplementationGuideStage, + CreatedAtStage, + UpdatedAtStage, + _FinalStage { + private String id; + + private ProfileSummarySource source; + + private String resourceType; + + private String url; - private Optional source = Optional.empty(); + private String version; - private Optional resourceType = Optional.empty(); + private String canonical; - private Optional url = Optional.empty(); + private String fhirVersion; - private Optional version = Optional.empty(); + private String implementationGuide; - private Optional fhirVersion = Optional.empty(); + private OffsetDateTime createdAt; - private Optional implementationGuide = Optional.empty(); + private OffsetDateTime updatedAt; - private Optional createdAt = Optional.empty(); + private Map structureDefinition = new LinkedHashMap<>(); - private Optional updatedAt = Optional.empty(); + private Optional date = Optional.empty(); - private Optional> structureDefinition = Optional.empty(); + private Optional status = Optional.empty(); @JsonAnySetter private Map additionalProperties = new HashMap<>(); private Builder() {} + @java.lang.Override public Builder from(ProfileGetResponse other) { id(other.getId()); source(other.getSource()); resourceType(other.getResourceType()); url(other.getUrl()); version(other.getVersion()); + status(other.getStatus()); + date(other.getDate()); + canonical(other.getCanonical()); fhirVersion(other.getFhirVersion()); implementationGuide(other.getImplementationGuide()); createdAt(other.getCreatedAt()); @@ -241,138 +412,187 @@ public Builder from(ProfileGetResponse other) { /** *

The lowercase StructureDefinition id, used as the profile's lookup key.

+ * @return Reference to {@code this} so that method calls can be chained together. */ - @JsonSetter(value = "id", nulls = Nulls.SKIP) - public Builder id(Optional id) { - this.id = id; - return this; - } - - public Builder id(String id) { - this.id = Optional.ofNullable(id); + @java.lang.Override + @JsonSetter("id") + public SourceStage id(@NotNull String id) { + this.id = Objects.requireNonNull(id, "id must not be null"); return this; } /** - *

The profile's origin. Listings currently return only custom (uploaded) profiles, so this is always "custom" today; built-in (US Core / R4 base) profiles would be surfaced via an opt-in parameter in a future release, not by changing the default behavior.

+ *

The profile's origin. Profile management responses currently return custom (uploaded) profiles, so this is always "custom" today.

+ * @return Reference to {@code this} so that method calls can be chained together. */ - @JsonSetter(value = "source", nulls = Nulls.SKIP) - public Builder source(Optional source) { - this.source = source; - return this; - } - - public Builder source(ProfileSummarySource source) { - this.source = Optional.ofNullable(source); + @java.lang.Override + @JsonSetter("source") + public ResourceTypeStage source(@NotNull ProfileSummarySource source) { + this.source = Objects.requireNonNull(source, "source must not be null"); return this; } /** *

The FHIR resource type from the StructureDefinition.

+ * @return Reference to {@code this} so that method calls can be chained together. */ - @JsonSetter(value = "resource_type", nulls = Nulls.SKIP) - public Builder resourceType(Optional resourceType) { - this.resourceType = resourceType; - return this; - } - - public Builder resourceType(String resourceType) { - this.resourceType = Optional.ofNullable(resourceType); + @java.lang.Override + @JsonSetter("resource_type") + public UrlStage resourceType(@NotNull String resourceType) { + this.resourceType = Objects.requireNonNull(resourceType, "resourceType must not be null"); return this; } /** *

The canonical URL from the StructureDefinition.

+ * @return Reference to {@code this} so that method calls can be chained together. */ - @JsonSetter(value = "url", nulls = Nulls.SKIP) - public Builder url(Optional url) { - this.url = url; + @java.lang.Override + @JsonSetter("url") + public VersionStage url(@NotNull String url) { + this.url = Objects.requireNonNull(url, "url must not be null"); return this; } - public Builder url(String url) { - this.url = Optional.ofNullable(url); + /** + *

The version from the StructureDefinition.version field.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("version") + public CanonicalStage version(@NotNull String version) { + this.version = Objects.requireNonNull(version, "version must not be null"); return this; } /** - *

The version from the StructureDefinition.version field.

+ *

The canonical profile reference, including the version pin when present.

+ * @return Reference to {@code this} so that method calls can be chained together. */ - @JsonSetter(value = "version", nulls = Nulls.SKIP) - public Builder version(Optional version) { - this.version = version; + @java.lang.Override + @JsonSetter("canonical") + public FhirVersionStage canonical(@NotNull String canonical) { + this.canonical = Objects.requireNonNull(canonical, "canonical must not be null"); return this; } - public Builder version(String version) { - this.version = Optional.ofNullable(version); + /** + *

The base FHIR version the StructureDefinition targets.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("fhir_version") + public ImplementationGuideStage fhirVersion(@NotNull String fhirVersion) { + this.fhirVersion = Objects.requireNonNull(fhirVersion, "fhirVersion must not be null"); return this; } /** - *

The base FHIR version the StructureDefinition targets.

+ *

The implementation guide the profile belongs to.

+ * @return Reference to {@code this} so that method calls can be chained together. */ - @JsonSetter(value = "fhir_version", nulls = Nulls.SKIP) - public Builder fhirVersion(Optional fhirVersion) { - this.fhirVersion = fhirVersion; + @java.lang.Override + @JsonSetter("implementation_guide") + public CreatedAtStage implementationGuide(@NotNull String implementationGuide) { + this.implementationGuide = + Objects.requireNonNull(implementationGuide, "implementationGuide must not be null"); return this; } - public Builder fhirVersion(String fhirVersion) { - this.fhirVersion = Optional.ofNullable(fhirVersion); + @java.lang.Override + @JsonSetter("created_at") + public UpdatedAtStage createdAt(@NotNull OffsetDateTime createdAt) { + this.createdAt = Objects.requireNonNull(createdAt, "createdAt must not be null"); return this; } /** - *

The implementation guide the profile belongs to.

+ *

Last update timestamp for the profile's current StructureDefinition. For retained versions, this equals created_at.

+ * @return Reference to {@code this} so that method calls can be chained together. */ - @JsonSetter(value = "implementation_guide", nulls = Nulls.SKIP) - public Builder implementationGuide(Optional implementationGuide) { - this.implementationGuide = implementationGuide; + @java.lang.Override + @JsonSetter("updated_at") + public _FinalStage updatedAt(@NotNull OffsetDateTime updatedAt) { + this.updatedAt = Objects.requireNonNull(updatedAt, "updatedAt must not be null"); return this; } - public Builder implementationGuide(String implementationGuide) { - this.implementationGuide = Optional.ofNullable(implementationGuide); + /** + *

The full FHIR StructureDefinition JSON.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage structureDefinition(String key, Object value) { + this.structureDefinition.put(key, value); return this; } - @JsonSetter(value = "created_at", nulls = Nulls.SKIP) - public Builder createdAt(Optional createdAt) { - this.createdAt = createdAt; + /** + *

The full FHIR StructureDefinition JSON.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage putAllStructureDefinition(Map structureDefinition) { + if (structureDefinition != null) { + this.structureDefinition.putAll(structureDefinition); + } return this; } - public Builder createdAt(OffsetDateTime createdAt) { - this.createdAt = Optional.ofNullable(createdAt); + /** + *

The full FHIR StructureDefinition JSON.

+ */ + @java.lang.Override + @JsonSetter(value = "structure_definition", nulls = Nulls.SKIP) + public _FinalStage structureDefinition(Map structureDefinition) { + this.structureDefinition.clear(); + if (structureDefinition != null) { + this.structureDefinition.putAll(structureDefinition); + } return this; } - @JsonSetter(value = "updated_at", nulls = Nulls.SKIP) - public Builder updatedAt(Optional updatedAt) { - this.updatedAt = updatedAt; + /** + *

The authored publication date from StructureDefinition.date, when present. This is a FHIR dateTime string and may be less precise than a full timestamp.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage date(String date) { + this.date = Optional.ofNullable(date); return this; } - public Builder updatedAt(OffsetDateTime updatedAt) { - this.updatedAt = Optional.ofNullable(updatedAt); + /** + *

The authored publication date from StructureDefinition.date, when present. This is a FHIR dateTime string and may be less precise than a full timestamp.

+ */ + @java.lang.Override + @JsonSetter(value = "date", nulls = Nulls.SKIP) + public _FinalStage date(Optional date) { + this.date = date; return this; } /** - *

The full FHIR StructureDefinition JSON.

+ *

The publication status from StructureDefinition.status. Expected FHIR values include draft, active, retired, and unknown; the server preserves authored strings.

+ * @return Reference to {@code this} so that method calls can be chained together. */ - @JsonSetter(value = "structure_definition", nulls = Nulls.SKIP) - public Builder structureDefinition(Optional> structureDefinition) { - this.structureDefinition = structureDefinition; + @java.lang.Override + public _FinalStage status(String status) { + this.status = Optional.ofNullable(status); return this; } - public Builder structureDefinition(Map structureDefinition) { - this.structureDefinition = Optional.ofNullable(structureDefinition); + /** + *

The publication status from StructureDefinition.status. Expected FHIR values include draft, active, retired, and unknown; the server preserves authored strings.

+ */ + @java.lang.Override + @JsonSetter(value = "status", nulls = Nulls.SKIP) + public _FinalStage status(Optional status) { + this.status = status; return this; } + @java.lang.Override public ProfileGetResponse build() { return new ProfileGetResponse( id, @@ -380,6 +600,9 @@ public ProfileGetResponse build() { resourceType, url, version, + status, + date, + canonical, fhirVersion, implementationGuide, createdAt, @@ -388,11 +611,13 @@ public ProfileGetResponse build() { additionalProperties); } + @java.lang.Override public Builder additionalProperty(String key, Object value) { this.additionalProperties.put(key, value); return this; } + @java.lang.Override public Builder additionalProperties(Map additionalProperties) { this.additionalProperties.putAll(additionalProperties); return this; diff --git a/src/main/java/com/phenoml/api/resources/profiles/types/ProfileListResponse.java b/src/main/java/com/phenoml/api/resources/profiles/types/ProfileListResponse.java index e17514ba..d888afd2 100644 --- a/src/main/java/com/phenoml/api/resources/profiles/types/ProfileListResponse.java +++ b/src/main/java/com/phenoml/api/resources/profiles/types/ProfileListResponse.java @@ -12,26 +12,26 @@ import com.fasterxml.jackson.annotation.Nulls; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.phenoml.api.core.ObjectMappers; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = ProfileListResponse.Builder.class) public final class ProfileListResponse { - private final Optional> profiles; + private final List profiles; private final Map additionalProperties; - private ProfileListResponse(Optional> profiles, Map additionalProperties) { + private ProfileListResponse(List profiles, Map additionalProperties) { this.profiles = profiles; this.additionalProperties = additionalProperties; } @JsonProperty("profiles") - public Optional> getProfiles() { + public List getProfiles() { return profiles; } @@ -66,7 +66,7 @@ public static Builder builder() { @JsonIgnoreProperties(ignoreUnknown = true) public static final class Builder { - private Optional> profiles = Optional.empty(); + private List profiles = new ArrayList<>(); @JsonAnySetter private Map additionalProperties = new HashMap<>(); @@ -79,13 +79,23 @@ public Builder from(ProfileListResponse other) { } @JsonSetter(value = "profiles", nulls = Nulls.SKIP) - public Builder profiles(Optional> profiles) { - this.profiles = profiles; + public Builder profiles(List profiles) { + this.profiles.clear(); + if (profiles != null) { + this.profiles.addAll(profiles); + } return this; } - public Builder profiles(List profiles) { - this.profiles = Optional.ofNullable(profiles); + public Builder addProfiles(ProfileSummary profiles) { + this.profiles.add(profiles); + return this; + } + + public Builder addAllProfiles(List profiles) { + if (profiles != null) { + this.profiles.addAll(profiles); + } return this; } diff --git a/src/main/java/com/phenoml/api/resources/profiles/types/ProfileSummary.java b/src/main/java/com/phenoml/api/resources/profiles/types/ProfileSummary.java index 31c9cb1a..06762e07 100644 --- a/src/main/java/com/phenoml/api/resources/profiles/types/ProfileSummary.java +++ b/src/main/java/com/phenoml/api/resources/profiles/types/ProfileSummary.java @@ -17,46 +17,59 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import org.jetbrains.annotations.NotNull; @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonDeserialize(builder = ProfileSummary.Builder.class) public final class ProfileSummary implements IProfileSummary { - private final Optional id; + private final String id; - private final Optional source; + private final ProfileSummarySource source; - private final Optional resourceType; + private final String resourceType; - private final Optional url; + private final String url; - private final Optional version; + private final String version; - private final Optional fhirVersion; + private final Optional status; - private final Optional implementationGuide; + private final Optional date; - private final Optional createdAt; + private final String canonical; - private final Optional updatedAt; + private final String fhirVersion; + + private final String implementationGuide; + + private final OffsetDateTime createdAt; + + private final OffsetDateTime updatedAt; private final Map additionalProperties; private ProfileSummary( - Optional id, - Optional source, - Optional resourceType, - Optional url, - Optional version, - Optional fhirVersion, - Optional implementationGuide, - Optional createdAt, - Optional updatedAt, + String id, + ProfileSummarySource source, + String resourceType, + String url, + String version, + Optional status, + Optional date, + String canonical, + String fhirVersion, + String implementationGuide, + OffsetDateTime createdAt, + OffsetDateTime updatedAt, Map additionalProperties) { this.id = id; this.source = source; this.resourceType = resourceType; this.url = url; this.version = version; + this.status = status; + this.date = date; + this.canonical = canonical; this.fhirVersion = fhirVersion; this.implementationGuide = implementationGuide; this.createdAt = createdAt; @@ -69,15 +82,15 @@ private ProfileSummary( */ @JsonProperty("id") @java.lang.Override - public Optional getId() { + public String getId() { return id; } /** - * @return The profile's origin. Listings currently return only custom (uploaded) profiles, so this is always "custom" today; built-in (US Core / R4 base) profiles would be surfaced via an opt-in parameter in a future release, not by changing the default behavior. + * @return The profile's origin. Profile management responses currently return custom (uploaded) profiles, so this is always "custom" today. */ @JsonProperty("source") - public Optional getSource() { + public ProfileSummarySource getSource() { return source; } @@ -86,7 +99,7 @@ public Optional getSource() { */ @JsonProperty("resource_type") @java.lang.Override - public Optional getResourceType() { + public String getResourceType() { return resourceType; } @@ -95,7 +108,7 @@ public Optional getResourceType() { */ @JsonProperty("url") @java.lang.Override - public Optional getUrl() { + public String getUrl() { return url; } @@ -104,16 +117,43 @@ public Optional getUrl() { */ @JsonProperty("version") @java.lang.Override - public Optional getVersion() { + public String getVersion() { return version; } + /** + * @return The publication status from StructureDefinition.status. Expected FHIR values include draft, active, retired, and unknown; the server preserves authored strings. + */ + @JsonProperty("status") + @java.lang.Override + public Optional getStatus() { + return status; + } + + /** + * @return The authored publication date from StructureDefinition.date, when present. This is a FHIR dateTime string and may be less precise than a full timestamp. + */ + @JsonProperty("date") + @java.lang.Override + public Optional getDate() { + return date; + } + + /** + * @return The canonical profile reference, including the version pin when present. + */ + @JsonProperty("canonical") + @java.lang.Override + public String getCanonical() { + return canonical; + } + /** * @return The base FHIR version the StructureDefinition targets. */ @JsonProperty("fhir_version") @java.lang.Override - public Optional getFhirVersion() { + public String getFhirVersion() { return fhirVersion; } @@ -122,19 +162,22 @@ public Optional getFhirVersion() { */ @JsonProperty("implementation_guide") @java.lang.Override - public Optional getImplementationGuide() { + public String getImplementationGuide() { return implementationGuide; } @JsonProperty("created_at") @java.lang.Override - public Optional getCreatedAt() { + public OffsetDateTime getCreatedAt() { return createdAt; } + /** + * @return Last update timestamp for the profile's current StructureDefinition. For retained versions, this equals created_at. + */ @JsonProperty("updated_at") @java.lang.Override - public Optional getUpdatedAt() { + public OffsetDateTime getUpdatedAt() { return updatedAt; } @@ -155,6 +198,9 @@ private boolean equalTo(ProfileSummary other) { && resourceType.equals(other.resourceType) && url.equals(other.url) && version.equals(other.version) + && status.equals(other.status) + && date.equals(other.date) + && canonical.equals(other.canonical) && fhirVersion.equals(other.fhirVersion) && implementationGuide.equals(other.implementationGuide) && createdAt.equals(other.createdAt) @@ -169,6 +215,9 @@ public int hashCode() { this.resourceType, this.url, this.version, + this.status, + this.date, + this.canonical, this.fhirVersion, this.implementationGuide, this.createdAt, @@ -180,41 +229,153 @@ public String toString() { return ObjectMappers.stringify(this); } - public static Builder builder() { + public static IdStage builder() { return new Builder(); } + public interface IdStage { + /** + *

The lowercase StructureDefinition id, used as the profile's lookup key.

+ */ + SourceStage id(@NotNull String id); + + Builder from(ProfileSummary other); + } + + public interface SourceStage { + /** + *

The profile's origin. Profile management responses currently return custom (uploaded) profiles, so this is always "custom" today.

+ */ + ResourceTypeStage source(@NotNull ProfileSummarySource source); + } + + public interface ResourceTypeStage { + /** + *

The FHIR resource type from the StructureDefinition.

+ */ + UrlStage resourceType(@NotNull String resourceType); + } + + public interface UrlStage { + /** + *

The canonical URL from the StructureDefinition.

+ */ + VersionStage url(@NotNull String url); + } + + public interface VersionStage { + /** + *

The version from the StructureDefinition.version field.

+ */ + CanonicalStage version(@NotNull String version); + } + + public interface CanonicalStage { + /** + *

The canonical profile reference, including the version pin when present.

+ */ + FhirVersionStage canonical(@NotNull String canonical); + } + + public interface FhirVersionStage { + /** + *

The base FHIR version the StructureDefinition targets.

+ */ + ImplementationGuideStage fhirVersion(@NotNull String fhirVersion); + } + + public interface ImplementationGuideStage { + /** + *

The implementation guide the profile belongs to.

+ */ + CreatedAtStage implementationGuide(@NotNull String implementationGuide); + } + + public interface CreatedAtStage { + UpdatedAtStage createdAt(@NotNull OffsetDateTime createdAt); + } + + public interface UpdatedAtStage { + /** + *

Last update timestamp for the profile's current StructureDefinition. For retained versions, this equals created_at.

+ */ + _FinalStage updatedAt(@NotNull OffsetDateTime updatedAt); + } + + public interface _FinalStage { + ProfileSummary build(); + + _FinalStage additionalProperty(String key, Object value); + + _FinalStage additionalProperties(Map additionalProperties); + + /** + *

The publication status from StructureDefinition.status. Expected FHIR values include draft, active, retired, and unknown; the server preserves authored strings.

+ */ + _FinalStage status(Optional status); + + _FinalStage status(String status); + + /** + *

The authored publication date from StructureDefinition.date, when present. This is a FHIR dateTime string and may be less precise than a full timestamp.

+ */ + _FinalStage date(Optional date); + + _FinalStage date(String date); + } + @JsonIgnoreProperties(ignoreUnknown = true) - public static final class Builder { - private Optional id = Optional.empty(); + public static final class Builder + implements IdStage, + SourceStage, + ResourceTypeStage, + UrlStage, + VersionStage, + CanonicalStage, + FhirVersionStage, + ImplementationGuideStage, + CreatedAtStage, + UpdatedAtStage, + _FinalStage { + private String id; + + private ProfileSummarySource source; + + private String resourceType; + + private String url; - private Optional source = Optional.empty(); + private String version; - private Optional resourceType = Optional.empty(); + private String canonical; - private Optional url = Optional.empty(); + private String fhirVersion; - private Optional version = Optional.empty(); + private String implementationGuide; - private Optional fhirVersion = Optional.empty(); + private OffsetDateTime createdAt; - private Optional implementationGuide = Optional.empty(); + private OffsetDateTime updatedAt; - private Optional createdAt = Optional.empty(); + private Optional date = Optional.empty(); - private Optional updatedAt = Optional.empty(); + private Optional status = Optional.empty(); @JsonAnySetter private Map additionalProperties = new HashMap<>(); private Builder() {} + @java.lang.Override public Builder from(ProfileSummary other) { id(other.getId()); source(other.getSource()); resourceType(other.getResourceType()); url(other.getUrl()); version(other.getVersion()); + status(other.getStatus()); + date(other.getDate()); + canonical(other.getCanonical()); fhirVersion(other.getFhirVersion()); implementationGuide(other.getImplementationGuide()); createdAt(other.getCreatedAt()); @@ -224,124 +385,152 @@ public Builder from(ProfileSummary other) { /** *

The lowercase StructureDefinition id, used as the profile's lookup key.

+ * @return Reference to {@code this} so that method calls can be chained together. */ - @JsonSetter(value = "id", nulls = Nulls.SKIP) - public Builder id(Optional id) { - this.id = id; - return this; - } - - public Builder id(String id) { - this.id = Optional.ofNullable(id); + @java.lang.Override + @JsonSetter("id") + public SourceStage id(@NotNull String id) { + this.id = Objects.requireNonNull(id, "id must not be null"); return this; } /** - *

The profile's origin. Listings currently return only custom (uploaded) profiles, so this is always "custom" today; built-in (US Core / R4 base) profiles would be surfaced via an opt-in parameter in a future release, not by changing the default behavior.

+ *

The profile's origin. Profile management responses currently return custom (uploaded) profiles, so this is always "custom" today.

+ * @return Reference to {@code this} so that method calls can be chained together. */ - @JsonSetter(value = "source", nulls = Nulls.SKIP) - public Builder source(Optional source) { - this.source = source; - return this; - } - - public Builder source(ProfileSummarySource source) { - this.source = Optional.ofNullable(source); + @java.lang.Override + @JsonSetter("source") + public ResourceTypeStage source(@NotNull ProfileSummarySource source) { + this.source = Objects.requireNonNull(source, "source must not be null"); return this; } /** *

The FHIR resource type from the StructureDefinition.

+ * @return Reference to {@code this} so that method calls can be chained together. */ - @JsonSetter(value = "resource_type", nulls = Nulls.SKIP) - public Builder resourceType(Optional resourceType) { - this.resourceType = resourceType; - return this; - } - - public Builder resourceType(String resourceType) { - this.resourceType = Optional.ofNullable(resourceType); + @java.lang.Override + @JsonSetter("resource_type") + public UrlStage resourceType(@NotNull String resourceType) { + this.resourceType = Objects.requireNonNull(resourceType, "resourceType must not be null"); return this; } /** *

The canonical URL from the StructureDefinition.

+ * @return Reference to {@code this} so that method calls can be chained together. */ - @JsonSetter(value = "url", nulls = Nulls.SKIP) - public Builder url(Optional url) { - this.url = url; - return this; - } - - public Builder url(String url) { - this.url = Optional.ofNullable(url); + @java.lang.Override + @JsonSetter("url") + public VersionStage url(@NotNull String url) { + this.url = Objects.requireNonNull(url, "url must not be null"); return this; } /** *

The version from the StructureDefinition.version field.

+ * @return Reference to {@code this} so that method calls can be chained together. */ - @JsonSetter(value = "version", nulls = Nulls.SKIP) - public Builder version(Optional version) { - this.version = version; + @java.lang.Override + @JsonSetter("version") + public CanonicalStage version(@NotNull String version) { + this.version = Objects.requireNonNull(version, "version must not be null"); return this; } - public Builder version(String version) { - this.version = Optional.ofNullable(version); + /** + *

The canonical profile reference, including the version pin when present.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("canonical") + public FhirVersionStage canonical(@NotNull String canonical) { + this.canonical = Objects.requireNonNull(canonical, "canonical must not be null"); return this; } /** *

The base FHIR version the StructureDefinition targets.

+ * @return Reference to {@code this} so that method calls can be chained together. */ - @JsonSetter(value = "fhir_version", nulls = Nulls.SKIP) - public Builder fhirVersion(Optional fhirVersion) { - this.fhirVersion = fhirVersion; - return this; - } - - public Builder fhirVersion(String fhirVersion) { - this.fhirVersion = Optional.ofNullable(fhirVersion); + @java.lang.Override + @JsonSetter("fhir_version") + public ImplementationGuideStage fhirVersion(@NotNull String fhirVersion) { + this.fhirVersion = Objects.requireNonNull(fhirVersion, "fhirVersion must not be null"); return this; } /** *

The implementation guide the profile belongs to.

+ * @return Reference to {@code this} so that method calls can be chained together. */ - @JsonSetter(value = "implementation_guide", nulls = Nulls.SKIP) - public Builder implementationGuide(Optional implementationGuide) { - this.implementationGuide = implementationGuide; + @java.lang.Override + @JsonSetter("implementation_guide") + public CreatedAtStage implementationGuide(@NotNull String implementationGuide) { + this.implementationGuide = + Objects.requireNonNull(implementationGuide, "implementationGuide must not be null"); return this; } - public Builder implementationGuide(String implementationGuide) { - this.implementationGuide = Optional.ofNullable(implementationGuide); + @java.lang.Override + @JsonSetter("created_at") + public UpdatedAtStage createdAt(@NotNull OffsetDateTime createdAt) { + this.createdAt = Objects.requireNonNull(createdAt, "createdAt must not be null"); return this; } - @JsonSetter(value = "created_at", nulls = Nulls.SKIP) - public Builder createdAt(Optional createdAt) { - this.createdAt = createdAt; + /** + *

Last update timestamp for the profile's current StructureDefinition. For retained versions, this equals created_at.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + @JsonSetter("updated_at") + public _FinalStage updatedAt(@NotNull OffsetDateTime updatedAt) { + this.updatedAt = Objects.requireNonNull(updatedAt, "updatedAt must not be null"); return this; } - public Builder createdAt(OffsetDateTime createdAt) { - this.createdAt = Optional.ofNullable(createdAt); + /** + *

The authored publication date from StructureDefinition.date, when present. This is a FHIR dateTime string and may be less precise than a full timestamp.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage date(String date) { + this.date = Optional.ofNullable(date); return this; } - @JsonSetter(value = "updated_at", nulls = Nulls.SKIP) - public Builder updatedAt(Optional updatedAt) { - this.updatedAt = updatedAt; + /** + *

The authored publication date from StructureDefinition.date, when present. This is a FHIR dateTime string and may be less precise than a full timestamp.

+ */ + @java.lang.Override + @JsonSetter(value = "date", nulls = Nulls.SKIP) + public _FinalStage date(Optional date) { + this.date = date; return this; } - public Builder updatedAt(OffsetDateTime updatedAt) { - this.updatedAt = Optional.ofNullable(updatedAt); + /** + *

The publication status from StructureDefinition.status. Expected FHIR values include draft, active, retired, and unknown; the server preserves authored strings.

+ * @return Reference to {@code this} so that method calls can be chained together. + */ + @java.lang.Override + public _FinalStage status(String status) { + this.status = Optional.ofNullable(status); + return this; + } + + /** + *

The publication status from StructureDefinition.status. Expected FHIR values include draft, active, retired, and unknown; the server preserves authored strings.

+ */ + @java.lang.Override + @JsonSetter(value = "status", nulls = Nulls.SKIP) + public _FinalStage status(Optional status) { + this.status = status; return this; } + @java.lang.Override public ProfileSummary build() { return new ProfileSummary( id, @@ -349,6 +538,9 @@ public ProfileSummary build() { resourceType, url, version, + status, + date, + canonical, fhirVersion, implementationGuide, createdAt, @@ -356,11 +548,13 @@ public ProfileSummary build() { additionalProperties); } + @java.lang.Override public Builder additionalProperty(String key, Object value) { this.additionalProperties.put(key, value); return this; } + @java.lang.Override public Builder additionalProperties(Map additionalProperties) { this.additionalProperties.putAll(additionalProperties); return this; diff --git a/src/main/java/com/phenoml/api/resources/profiles/types/ProfileVersionListResponse.java b/src/main/java/com/phenoml/api/resources/profiles/types/ProfileVersionListResponse.java new file mode 100644 index 00000000..6572548a --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/profiles/types/ProfileVersionListResponse.java @@ -0,0 +1,116 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.profiles.types; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.phenoml.api.core.ObjectMappers; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@JsonInclude(JsonInclude.Include.NON_ABSENT) +@JsonDeserialize(builder = ProfileVersionListResponse.Builder.class) +public final class ProfileVersionListResponse { + private final List versions; + + private final Map additionalProperties; + + private ProfileVersionListResponse(List versions, Map additionalProperties) { + this.versions = versions; + this.additionalProperties = additionalProperties; + } + + @JsonProperty("versions") + public List getVersions() { + return versions; + } + + @java.lang.Override + public boolean equals(Object other) { + if (this == other) return true; + return other instanceof ProfileVersionListResponse && equalTo((ProfileVersionListResponse) other); + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + private boolean equalTo(ProfileVersionListResponse other) { + return versions.equals(other.versions); + } + + @java.lang.Override + public int hashCode() { + return Objects.hash(this.versions); + } + + @java.lang.Override + public String toString() { + return ObjectMappers.stringify(this); + } + + public static Builder builder() { + return new Builder(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Builder { + private List versions = new ArrayList<>(); + + @JsonAnySetter + private Map additionalProperties = new HashMap<>(); + + private Builder() {} + + public Builder from(ProfileVersionListResponse other) { + versions(other.getVersions()); + return this; + } + + @JsonSetter(value = "versions", nulls = Nulls.SKIP) + public Builder versions(List versions) { + this.versions.clear(); + if (versions != null) { + this.versions.addAll(versions); + } + return this; + } + + public Builder addVersions(ProfileSummary versions) { + this.versions.add(versions); + return this; + } + + public Builder addAllVersions(List versions) { + if (versions != null) { + this.versions.addAll(versions); + } + return this; + } + + public ProfileVersionListResponse build() { + return new ProfileVersionListResponse(versions, additionalProperties); + } + + public Builder additionalProperty(String key, Object value) { + this.additionalProperties.put(key, value); + return this; + } + + public Builder additionalProperties(Map additionalProperties) { + this.additionalProperties.putAll(additionalProperties); + return this; + } + } +} diff --git a/src/main/java/com/phenoml/api/resources/profiles/versions/AsyncRawVersionsClient.java b/src/main/java/com/phenoml/api/resources/profiles/versions/AsyncRawVersionsClient.java new file mode 100644 index 00000000..20f981af --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/profiles/versions/AsyncRawVersionsClient.java @@ -0,0 +1,507 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.profiles.versions; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.phenoml.api.core.ClientOptions; +import com.phenoml.api.core.MediaTypes; +import com.phenoml.api.core.ObjectMappers; +import com.phenoml.api.core.PhenomlClientApiException; +import com.phenoml.api.core.PhenomlClientException; +import com.phenoml.api.core.PhenomlClientHttpResponse; +import com.phenoml.api.core.RequestOptions; +import com.phenoml.api.core.RetryInterceptor; +import com.phenoml.api.resources.profiles.errors.BadRequestError; +import com.phenoml.api.resources.profiles.errors.ConflictError; +import com.phenoml.api.resources.profiles.errors.ForbiddenError; +import com.phenoml.api.resources.profiles.errors.InternalServerError; +import com.phenoml.api.resources.profiles.errors.NotFoundError; +import com.phenoml.api.resources.profiles.errors.UnauthorizedError; +import com.phenoml.api.resources.profiles.types.ProfileGetResponse; +import com.phenoml.api.resources.profiles.types.ProfileSummary; +import com.phenoml.api.resources.profiles.types.ProfileVersionListResponse; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Headers; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.jetbrains.annotations.NotNull; + +public class AsyncRawVersionsClient { + protected final ClientOptions clientOptions; + + public AsyncRawVersionsClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + } + + /** + * Returns retained versions for a custom profile. + */ + public CompletableFuture> list(String id) { + return list(id, null); + } + + /** + * Returns retained versions for a custom profile. + */ + public CompletableFuture> list( + String id, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("fhir/profiles") + .addPathSegment(id) + .addPathSegments("versions"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("GET", null) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue( + responseBodyString, ProfileVersionListResponse.class), + response)); + return; + } + try { + switch (response.code()) { + case 400: + future.completeExceptionally(new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 401: + future.completeExceptionally(new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 403: + future.completeExceptionally(new ForbiddenError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 404: + future.completeExceptionally(new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 500: + future.completeExceptionally(new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + }); + return future; + } + + /** + * Adds an immutable StructureDefinition version to a custom profile. If + * the profile does not exist, it is created from the submitted version. + * The StructureDefinition must include a non-empty version; its + * canonical URL and resource type must match the profile when one already + * exists. If it includes an id, that id must match the path parameter; + * if it omits id, the path parameter is used. Profiles created through + * this endpoint are grouped under custom. Posting the profile's current + * StructureDefinition unchanged retains it as a version. + * Version strings may contain letters, numbers, and the punctuation + * characters ., _, ~, +, and -; they cannot be exactly . or + * ... Each profile can retain up to 250 versions; delete old + * versions before adding more. + */ + public CompletableFuture> create(String id, Map request) { + return create(id, request, null); + } + + /** + * Adds an immutable StructureDefinition version to a custom profile. If + * the profile does not exist, it is created from the submitted version. + * The StructureDefinition must include a non-empty version; its + * canonical URL and resource type must match the profile when one already + * exists. If it includes an id, that id must match the path parameter; + * if it omits id, the path parameter is used. Profiles created through + * this endpoint are grouped under custom. Posting the profile's current + * StructureDefinition unchanged retains it as a version. + * Version strings may contain letters, numbers, and the punctuation + * characters ., _, ~, +, and -; they cannot be exactly . or + * ... Each profile can retain up to 250 versions; delete old + * versions before adding more. + */ + public CompletableFuture> create( + String id, Map request, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("fhir/profiles") + .addPathSegment(id) + .addPathSegments("versions"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, ProfileSummary.class), + response)); + return; + } + try { + switch (response.code()) { + case 400: + future.completeExceptionally(new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 401: + future.completeExceptionally(new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 403: + future.completeExceptionally(new ForbiddenError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 404: + future.completeExceptionally(new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 409: + future.completeExceptionally(new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 500: + future.completeExceptionally(new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + }); + return future; + } + + /** + * Returns metadata and the full StructureDefinition for one retained + * version. The returned StructureDefinition's id is the profile id. The + * path version is the authored StructureDefinition.version value. + */ + public CompletableFuture> get(String id, String version) { + return get(id, version, null); + } + + /** + * Returns metadata and the full StructureDefinition for one retained + * version. The returned StructureDefinition's id is the profile id. The + * path version is the authored StructureDefinition.version value. + */ + public CompletableFuture> get( + String id, String version, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("fhir/profiles") + .addPathSegment(id) + .addPathSegments("versions") + .addPathSegment(version); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("GET", null) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + future.complete(new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, ProfileGetResponse.class), + response)); + return; + } + try { + switch (response.code()) { + case 400: + future.completeExceptionally(new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 401: + future.completeExceptionally(new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 403: + future.completeExceptionally(new ForbiddenError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 404: + future.completeExceptionally(new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 500: + future.completeExceptionally(new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + }); + return future; + } + + /** + * Deletes one retained version from a custom profile. The path + * version is the authored StructureDefinition.version value. + */ + public CompletableFuture> delete(String id, String version) { + return delete(id, version, null); + } + + /** + * Deletes one retained version from a custom profile. The path + * version is the authored StructureDefinition.version value. + */ + public CompletableFuture> delete( + String id, String version, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("fhir/profiles") + .addPathSegment(id) + .addPathSegments("versions") + .addPathSegment(version); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("DELETE", null) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + CompletableFuture> future = new CompletableFuture<>(); + client.newCall(okhttpRequest).enqueue(new Callback() { + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + try (ResponseBody responseBody = response.body()) { + if (response.isSuccessful()) { + future.complete(new PhenomlClientHttpResponse<>(null, response)); + return; + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + try { + switch (response.code()) { + case 400: + future.completeExceptionally(new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 401: + future.completeExceptionally(new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 403: + future.completeExceptionally(new ForbiddenError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 404: + future.completeExceptionally(new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 409: + future.completeExceptionally(new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + case 500: + future.completeExceptionally(new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), + response)); + return; + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + future.completeExceptionally(new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response)); + return; + } catch (JsonProcessingException e) { + future.completeExceptionally( + new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e)); + } catch (IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + } + + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + future.completeExceptionally(new PhenomlClientException("Network error executing HTTP request", e)); + } + }); + return future; + } +} diff --git a/src/main/java/com/phenoml/api/resources/profiles/versions/AsyncVersionsClient.java b/src/main/java/com/phenoml/api/resources/profiles/versions/AsyncVersionsClient.java new file mode 100644 index 00000000..282171f4 --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/profiles/versions/AsyncVersionsClient.java @@ -0,0 +1,115 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.profiles.versions; + +import com.phenoml.api.core.ClientOptions; +import com.phenoml.api.core.RequestOptions; +import com.phenoml.api.resources.profiles.types.ProfileGetResponse; +import com.phenoml.api.resources.profiles.types.ProfileSummary; +import com.phenoml.api.resources.profiles.types.ProfileVersionListResponse; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class AsyncVersionsClient { + protected final ClientOptions clientOptions; + + private final AsyncRawVersionsClient rawClient; + + public AsyncVersionsClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.rawClient = new AsyncRawVersionsClient(clientOptions); + } + + /** + * Get responses with HTTP metadata like headers + */ + public AsyncRawVersionsClient withRawResponse() { + return this.rawClient; + } + + /** + * Returns retained versions for a custom profile. + */ + public CompletableFuture list(String id) { + return this.rawClient.list(id).thenApply(response -> response.body()); + } + + /** + * Returns retained versions for a custom profile. + */ + public CompletableFuture list(String id, RequestOptions requestOptions) { + return this.rawClient.list(id, requestOptions).thenApply(response -> response.body()); + } + + /** + * Adds an immutable StructureDefinition version to a custom profile. If + * the profile does not exist, it is created from the submitted version. + * The StructureDefinition must include a non-empty version; its + * canonical URL and resource type must match the profile when one already + * exists. If it includes an id, that id must match the path parameter; + * if it omits id, the path parameter is used. Profiles created through + * this endpoint are grouped under custom. Posting the profile's current + * StructureDefinition unchanged retains it as a version. + * Version strings may contain letters, numbers, and the punctuation + * characters ., _, ~, +, and -; they cannot be exactly . or + * ... Each profile can retain up to 250 versions; delete old + * versions before adding more. + */ + public CompletableFuture create(String id, Map request) { + return this.rawClient.create(id, request).thenApply(response -> response.body()); + } + + /** + * Adds an immutable StructureDefinition version to a custom profile. If + * the profile does not exist, it is created from the submitted version. + * The StructureDefinition must include a non-empty version; its + * canonical URL and resource type must match the profile when one already + * exists. If it includes an id, that id must match the path parameter; + * if it omits id, the path parameter is used. Profiles created through + * this endpoint are grouped under custom. Posting the profile's current + * StructureDefinition unchanged retains it as a version. + * Version strings may contain letters, numbers, and the punctuation + * characters ., _, ~, +, and -; they cannot be exactly . or + * ... Each profile can retain up to 250 versions; delete old + * versions before adding more. + */ + public CompletableFuture create( + String id, Map request, RequestOptions requestOptions) { + return this.rawClient.create(id, request, requestOptions).thenApply(response -> response.body()); + } + + /** + * Returns metadata and the full StructureDefinition for one retained + * version. The returned StructureDefinition's id is the profile id. The + * path version is the authored StructureDefinition.version value. + */ + public CompletableFuture get(String id, String version) { + return this.rawClient.get(id, version).thenApply(response -> response.body()); + } + + /** + * Returns metadata and the full StructureDefinition for one retained + * version. The returned StructureDefinition's id is the profile id. The + * path version is the authored StructureDefinition.version value. + */ + public CompletableFuture get(String id, String version, RequestOptions requestOptions) { + return this.rawClient.get(id, version, requestOptions).thenApply(response -> response.body()); + } + + /** + * Deletes one retained version from a custom profile. The path + * version is the authored StructureDefinition.version value. + */ + public CompletableFuture delete(String id, String version) { + return this.rawClient.delete(id, version).thenApply(response -> response.body()); + } + + /** + * Deletes one retained version from a custom profile. The path + * version is the authored StructureDefinition.version value. + */ + public CompletableFuture delete(String id, String version, RequestOptions requestOptions) { + return this.rawClient.delete(id, version, requestOptions).thenApply(response -> response.body()); + } +} diff --git a/src/main/java/com/phenoml/api/resources/profiles/versions/RawVersionsClient.java b/src/main/java/com/phenoml/api/resources/profiles/versions/RawVersionsClient.java new file mode 100644 index 00000000..6e9d311f --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/profiles/versions/RawVersionsClient.java @@ -0,0 +1,397 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.profiles.versions; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.phenoml.api.core.ClientOptions; +import com.phenoml.api.core.MediaTypes; +import com.phenoml.api.core.ObjectMappers; +import com.phenoml.api.core.PhenomlClientApiException; +import com.phenoml.api.core.PhenomlClientException; +import com.phenoml.api.core.PhenomlClientHttpResponse; +import com.phenoml.api.core.RequestOptions; +import com.phenoml.api.core.RetryInterceptor; +import com.phenoml.api.resources.profiles.errors.BadRequestError; +import com.phenoml.api.resources.profiles.errors.ConflictError; +import com.phenoml.api.resources.profiles.errors.ForbiddenError; +import com.phenoml.api.resources.profiles.errors.InternalServerError; +import com.phenoml.api.resources.profiles.errors.NotFoundError; +import com.phenoml.api.resources.profiles.errors.UnauthorizedError; +import com.phenoml.api.resources.profiles.types.ProfileGetResponse; +import com.phenoml.api.resources.profiles.types.ProfileSummary; +import com.phenoml.api.resources.profiles.types.ProfileVersionListResponse; +import java.io.IOException; +import java.util.Map; +import okhttp3.Headers; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; + +public class RawVersionsClient { + protected final ClientOptions clientOptions; + + public RawVersionsClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + } + + /** + * Returns retained versions for a custom profile. + */ + public PhenomlClientHttpResponse list(String id) { + return list(id, null); + } + + /** + * Returns retained versions for a custom profile. + */ + public PhenomlClientHttpResponse list(String id, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("fhir/profiles") + .addPathSegment(id) + .addPathSegments("versions"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("GET", null) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, ProfileVersionListResponse.class), + response); + } + try { + switch (response.code()) { + case 400: + throw new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 401: + throw new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 403: + throw new ForbiddenError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 404: + throw new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 500: + throw new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new PhenomlClientException("Network error executing HTTP request", e); + } + } + + /** + * Adds an immutable StructureDefinition version to a custom profile. If + * the profile does not exist, it is created from the submitted version. + * The StructureDefinition must include a non-empty version; its + * canonical URL and resource type must match the profile when one already + * exists. If it includes an id, that id must match the path parameter; + * if it omits id, the path parameter is used. Profiles created through + * this endpoint are grouped under custom. Posting the profile's current + * StructureDefinition unchanged retains it as a version. + * Version strings may contain letters, numbers, and the punctuation + * characters ., _, ~, +, and -; they cannot be exactly . or + * ... Each profile can retain up to 250 versions; delete old + * versions before adding more. + */ + public PhenomlClientHttpResponse create(String id, Map request) { + return create(id, request, null); + } + + /** + * Adds an immutable StructureDefinition version to a custom profile. If + * the profile does not exist, it is created from the submitted version. + * The StructureDefinition must include a non-empty version; its + * canonical URL and resource type must match the profile when one already + * exists. If it includes an id, that id must match the path parameter; + * if it omits id, the path parameter is used. Profiles created through + * this endpoint are grouped under custom. Posting the profile's current + * StructureDefinition unchanged retains it as a version. + * Version strings may contain letters, numbers, and the punctuation + * characters ., _, ~, +, and -; they cannot be exactly . or + * ... Each profile can retain up to 250 versions; delete old + * versions before adding more. + */ + public PhenomlClientHttpResponse create( + String id, Map request, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("fhir/profiles") + .addPathSegment(id) + .addPathSegments("versions"); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + RequestBody body; + try { + body = RequestBody.create( + ObjectMappers.JSON_MAPPER.writeValueAsBytes(request), MediaTypes.APPLICATION_JSON); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to serialize request", e); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("POST", body) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Content-Type", "application/json") + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, ProfileSummary.class), response); + } + try { + switch (response.code()) { + case 400: + throw new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 401: + throw new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 403: + throw new ForbiddenError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 404: + throw new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 409: + throw new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 500: + throw new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new PhenomlClientException("Network error executing HTTP request", e); + } + } + + /** + * Returns metadata and the full StructureDefinition for one retained + * version. The returned StructureDefinition's id is the profile id. The + * path version is the authored StructureDefinition.version value. + */ + public PhenomlClientHttpResponse get(String id, String version) { + return get(id, version, null); + } + + /** + * Returns metadata and the full StructureDefinition for one retained + * version. The returned StructureDefinition's id is the profile id. The + * path version is the authored StructureDefinition.version value. + */ + public PhenomlClientHttpResponse get(String id, String version, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("fhir/profiles") + .addPathSegment(id) + .addPathSegments("versions") + .addPathSegment(version); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("GET", null) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + if (response.isSuccessful()) { + return new PhenomlClientHttpResponse<>( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, ProfileGetResponse.class), response); + } + try { + switch (response.code()) { + case 400: + throw new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 401: + throw new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 403: + throw new ForbiddenError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 404: + throw new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 500: + throw new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new PhenomlClientException("Network error executing HTTP request", e); + } + } + + /** + * Deletes one retained version from a custom profile. The path + * version is the authored StructureDefinition.version value. + */ + public PhenomlClientHttpResponse delete(String id, String version) { + return delete(id, version, null); + } + + /** + * Deletes one retained version from a custom profile. The path + * version is the authored StructureDefinition.version value. + */ + public PhenomlClientHttpResponse delete(String id, String version, RequestOptions requestOptions) { + HttpUrl.Builder httpUrl = HttpUrl.parse(this.clientOptions.environment().getUrl()) + .newBuilder() + .addPathSegments("fhir/profiles") + .addPathSegment(id) + .addPathSegments("versions") + .addPathSegment(version); + if (requestOptions != null) { + requestOptions.getQueryParameters().forEach((_key, _value) -> { + httpUrl.addQueryParameter(_key, _value); + }); + } + Request okhttpRequest = new Request.Builder() + .url(httpUrl.build()) + .method("DELETE", null) + .headers(Headers.of(clientOptions.headers(requestOptions))) + .addHeader("Accept", "application/json") + .build(); + OkHttpClient client = clientOptions.httpClient(); + if (requestOptions != null && requestOptions.getTimeout().isPresent()) { + client = clientOptions.httpClientWithTimeout(requestOptions); + } + if (requestOptions != null && requestOptions.getMaxRetries().isPresent()) { + okhttpRequest = okhttpRequest + .newBuilder() + .tag( + RetryInterceptor.MaxRetriesOverride.class, + new RetryInterceptor.MaxRetriesOverride( + requestOptions.getMaxRetries().get())) + .build(); + } + try (Response response = client.newCall(okhttpRequest).execute()) { + ResponseBody responseBody = response.body(); + if (response.isSuccessful()) { + return new PhenomlClientHttpResponse<>(null, response); + } + String responseBodyString = responseBody != null ? responseBody.string() : "{}"; + try { + switch (response.code()) { + case 400: + throw new BadRequestError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 401: + throw new UnauthorizedError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 403: + throw new ForbiddenError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 404: + throw new NotFoundError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 409: + throw new ConflictError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + case 500: + throw new InternalServerError( + ObjectMappers.JSON_MAPPER.readValue(responseBodyString, Object.class), response); + } + } catch (JsonProcessingException ignored) { + // unable to map error response, throwing generic error + } + Object errorBody = ObjectMappers.parseErrorBody(responseBodyString); + throw new PhenomlClientApiException( + "Error with status code " + response.code(), response.code(), errorBody, response); + } catch (JsonProcessingException e) { + throw new PhenomlClientException("Failed to deserialize response: " + e.getMessage(), e); + } catch (IOException e) { + throw new PhenomlClientException("Network error executing HTTP request", e); + } + } +} diff --git a/src/main/java/com/phenoml/api/resources/profiles/versions/VersionsClient.java b/src/main/java/com/phenoml/api/resources/profiles/versions/VersionsClient.java new file mode 100644 index 00000000..8de92ac0 --- /dev/null +++ b/src/main/java/com/phenoml/api/resources/profiles/versions/VersionsClient.java @@ -0,0 +1,113 @@ +/** + * This file was auto-generated by Fern from our API Definition. + */ +package com.phenoml.api.resources.profiles.versions; + +import com.phenoml.api.core.ClientOptions; +import com.phenoml.api.core.RequestOptions; +import com.phenoml.api.resources.profiles.types.ProfileGetResponse; +import com.phenoml.api.resources.profiles.types.ProfileSummary; +import com.phenoml.api.resources.profiles.types.ProfileVersionListResponse; +import java.util.Map; + +public class VersionsClient { + protected final ClientOptions clientOptions; + + private final RawVersionsClient rawClient; + + public VersionsClient(ClientOptions clientOptions) { + this.clientOptions = clientOptions; + this.rawClient = new RawVersionsClient(clientOptions); + } + + /** + * Get responses with HTTP metadata like headers + */ + public RawVersionsClient withRawResponse() { + return this.rawClient; + } + + /** + * Returns retained versions for a custom profile. + */ + public ProfileVersionListResponse list(String id) { + return this.rawClient.list(id).body(); + } + + /** + * Returns retained versions for a custom profile. + */ + public ProfileVersionListResponse list(String id, RequestOptions requestOptions) { + return this.rawClient.list(id, requestOptions).body(); + } + + /** + * Adds an immutable StructureDefinition version to a custom profile. If + * the profile does not exist, it is created from the submitted version. + * The StructureDefinition must include a non-empty version; its + * canonical URL and resource type must match the profile when one already + * exists. If it includes an id, that id must match the path parameter; + * if it omits id, the path parameter is used. Profiles created through + * this endpoint are grouped under custom. Posting the profile's current + * StructureDefinition unchanged retains it as a version. + * Version strings may contain letters, numbers, and the punctuation + * characters ., _, ~, +, and -; they cannot be exactly . or + * ... Each profile can retain up to 250 versions; delete old + * versions before adding more. + */ + public ProfileSummary create(String id, Map request) { + return this.rawClient.create(id, request).body(); + } + + /** + * Adds an immutable StructureDefinition version to a custom profile. If + * the profile does not exist, it is created from the submitted version. + * The StructureDefinition must include a non-empty version; its + * canonical URL and resource type must match the profile when one already + * exists. If it includes an id, that id must match the path parameter; + * if it omits id, the path parameter is used. Profiles created through + * this endpoint are grouped under custom. Posting the profile's current + * StructureDefinition unchanged retains it as a version. + * Version strings may contain letters, numbers, and the punctuation + * characters ., _, ~, +, and -; they cannot be exactly . or + * ... Each profile can retain up to 250 versions; delete old + * versions before adding more. + */ + public ProfileSummary create(String id, Map request, RequestOptions requestOptions) { + return this.rawClient.create(id, request, requestOptions).body(); + } + + /** + * Returns metadata and the full StructureDefinition for one retained + * version. The returned StructureDefinition's id is the profile id. The + * path version is the authored StructureDefinition.version value. + */ + public ProfileGetResponse get(String id, String version) { + return this.rawClient.get(id, version).body(); + } + + /** + * Returns metadata and the full StructureDefinition for one retained + * version. The returned StructureDefinition's id is the profile id. The + * path version is the authored StructureDefinition.version value. + */ + public ProfileGetResponse get(String id, String version, RequestOptions requestOptions) { + return this.rawClient.get(id, version, requestOptions).body(); + } + + /** + * Deletes one retained version from a custom profile. The path + * version is the authored StructureDefinition.version value. + */ + public void delete(String id, String version) { + this.rawClient.delete(id, version).body(); + } + + /** + * Deletes one retained version from a custom profile. The path + * version is the authored StructureDefinition.version value. + */ + public void delete(String id, String version, RequestOptions requestOptions) { + this.rawClient.delete(id, version, requestOptions).body(); + } +} diff --git a/src/main/resources/openapi/openapi.json b/src/main/resources/openapi/openapi.json index 5e7bdf7b..1a38016d 100644 --- a/src/main/resources/openapi/openapi.json +++ b/src/main/resources/openapi/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.0.3", "info": { "title": "Phenoml API", - "version": "4a08550f5db230949c7423d0ce5aa7055e8f0d65" + "version": "70c52e949023cff024c8c9dfaccaecd12617548e" }, "x-services": [ { @@ -3274,7 +3274,7 @@ "post": { "operationId": "fhir2omop_create", "summary": "Map FHIR resources to OMOP CDM v5.4", - "description": "Maps a FHIR R4 resource or Bundle into OMOP Common Data Model v5.4 rows\n(person, visit_occurrence, condition_occurrence, drug_exposure,\nprocedure_occurrence, measurement, observation).\n\nResource support is intentionally limited to the OMOP tables returned by\nthis endpoint:\n- `Patient` -> `person`\n- `Encounter` -> `visit_occurrence`\n- `Condition` -> `condition_occurrence`\n- `Procedure` -> `procedure_occurrence`\n- `MedicationRequest`, `MedicationStatement`, and\n `MedicationAdministration` -> `drug_exposure`\n- `Immunization` -> `drug_exposure`\n- `Observation` with a numeric `valueQuantity`, `valueInteger`, or\n numeric-looking `valueString` (for example `\"<2\"`) -> `measurement`\n- non-numeric `Observation` -> `observation`\n- `AllergyIntolerance` -> `observation`\n\n`Medication` is supported only as reference data for medication\nresources; it is not emitted as its own row because OMOP CDM has no\nMedication table. Other reference/admin resources such as `Practitioner`,\n`Organization`, `Location`, `Coverage`, and `Claim`, and clinical\nworkflow/document resources such as `DiagnosticReport`, `ServiceRequest`,\n`CarePlan`, `DocumentReference`, `Composition`, `Specimen`, and\n`DeviceUseStatement`, are currently accepted in a Bundle but are not\nshaped into OMOP rows. Unsupported resource types are ignored rather than\nlisted under `dropped`; `dropped` is reserved for supported resource types\nthat were missing the subject/patient, code, or medication reference data\nneeded to produce a valid row.\n\nEach resource's primary clinical coding is resolved to a standard OMOP\n`concept_id`. Alongside the OMOP rows grouped by table (`tables`), the\nresponse carries `mappings` (how each source coding resolved, linked back\nto the row it produced), `dropped` (resources that could not be shaped\ninto a row), `vocab_version` (the OMOP vocabulary release codes were\nresolved against), and a small `summary` of the resolution outcomes.\n\nA `concept_id` of `0` is reported, not omitted (OMOP \"no matching\nconcept\" semantics): it covers both a coding with no standard match\n(`UNMAPPED`) and an unverified suggestion for a text-only resource\n(`UNCHECKED`). Only the primary clinical coding is resolved, so\n`gender`/`race`/`ethnicity`/`visit`/`value`/`unit` `concept_id`s are\nalways `0`; the one populated non-resolved concept is measurement\n`operator_concept_id`, set from a value comparator (`<`, `<=`, `>`, `>=`)\nrather than the resolver. Each `*_source_value` carries the verbatim FHIR\ncoding (`system#code`), and `*_type_concept_id` is set to `32817` (EHR).\n\nMedication codes are resolved whether they appear inline\n(`medicationCodeableConcept`) or via a `medicationReference` to a contained,\nrelative (`Type/id`), or bundle-entry (`urn:uuid`) `Medication` resource.\nResources that cannot be shaped into a row \u2014 a medication with no usable\ncode, resolvable reference, or display, or any clinical resource whose\nsubject/patient reference cannot be tied to a person \u2014 are reported under\n`dropped` rather than emitted as blank rows. The\nbundle must contain at least one Patient resource.\n", + "description": "Maps a FHIR R4 resource or Bundle into OMOP Common Data Model v5.4 rows,\ngrouped by destination table in `tables`.\n\nCurrent resource coverage:\n- `Patient` -> `person`; `deceased[x]` can also produce `death`, and the\n first address can produce `location`\n- `observation_period` -> one derived row per person with dated visit,\n clinical, or death rows, spanning those dates\n- `Location` -> `location` and `care_site`\n- `Organization` -> `care_site`; its first address can produce `location`\n- `HealthcareService` -> `care_site`\n- `Practitioner` and `PractitionerRole` -> `provider`\n- `Encounter` -> `visit_occurrence`\n- `Condition` -> `condition_occurrence`\n- `Procedure` -> `procedure_occurrence`\n- `MedicationRequest`, `MedicationStatement`, and\n `MedicationAdministration` -> `drug_exposure`\n- `Immunization` -> `drug_exposure`\n- `Observation` -> `measurement` or `observation`. For coded\n Observations, the resolved OMOP concept domain selects the table; value\n form only breaks ties. For text-only Observations, numeric values route\n to `measurement` and nonnumeric values to `observation`.\n- `AllergyIntolerance` -> `observation`\n\n`Medication` is reference data for medication resources; it does not\ncreate its own row because OMOP CDM has no Medication table. Administrative\nlinkages (provider, care site, and location) are best-effort and limited to\nreferences supplied in the request. Their supporting concepts, including\nprovider specialty, country, and place of service, are not mapped.\n\n`DiagnosticReport`, `ServiceRequest`, `CarePlan`, `DocumentReference`,\n`Composition`, `Specimen`, `DeviceUseStatement`, `Coverage`, `Claim`, and\nother unsupported resource types are accepted in a Bundle but ignored: they\ncreate no row and no `dropped` entry. `dropped` is reserved for supported\nrow-producing resources that could not be shaped because the subject/patient,\nclinical code/text, or medication data was not usable. A single-Patient\nBundle can attribute a supported clinical resource with a missing or\nunresolvable subject to that sole person; in a multi-Patient Bundle, that\nresource is dropped instead.\n\nCoded Observation routing is selected from the resolved OMOP concept\ndomain. Numeric and nonnumeric `value[x]` forms establish the preferred\ntarget only when the code is valid for both tables. A text-only\nObservation has no resolver target, so numeric values route to\n`measurement` and nonnumeric values to `observation`. Numeric values\npopulate `value_as_number` in the selected row; nonnumeric values\npopulate `value_as_string` for an `observation` or `value_source_value`\nfor a `measurement`. `valueCodeableConcept` remains source text and does\nnot populate `value_as_concept_id`; other unsupported `value[x]` forms\nand Observation components do not populate separate converted values. A\nnumeric comparator (`<`, `<=`, `>`, `>=`) is represented only by a\nmeasurement's `operator_concept_id`; units remain source text and have\n`unit_concept_id` of `0`.\n\nA single standard OMOP `concept_id` is selected for each clinical row\nafter considering all of the resource's supplied codings. Alongside the\nOMOP rows grouped by table (`tables`), the response carries `mappings`\n(an entry for every source coding, linked back to the row it produced),\n`dropped` (resources that could not be shaped into a row),\n`vocab_version` (the OMOP vocabulary release codes were resolved\nagainst), and a small `summary` of the resolution outcomes.\n\nA `concept_id` of `0` is reported, not omitted (OMOP \"no matching\nconcept\" semantics): it covers both a coding with no standard match\n(`UNMAPPED`) and an unverified suggestion for a text-only resource\n(`UNCHECKED`). Demographic, visit, categorical-value, and unit concept\nfields currently remain `0`; the one populated non-resolved concept is\nmeasurement\n`operator_concept_id`, set from a value comparator (`<`, `<=`, `>`, `>=`)\nrather than terminology resolution. Clinical `*_source_value` fields\npreserve the selected FHIR coding (`system#code`, or `code` when no\nsystem is supplied), falling back to source text for text-only resources.\nKnown OID-form coding systems are accepted as either FHIR OID URNs (for\nexample, `urn:oid:2.16.840.1.113883.6.1` for LOINC) or bare OIDs, and\nare normalized to their canonical system URLs before terminology\nresolution. `*_source_value` and `mappings[].source_system` report that\ncanonical URL, so the OID and URL forms produce the same mapping. An\nunknown OID is not rewritten and may be `UNMAPPED`.\nOther `*_source_value` fields preserve row-specific raw source values,\nsuch as resource identifiers, names, units, or status codes, and\n`*_type_concept_id` is set to `32817` (EHR).\n\nMedication codes are resolved whether they appear inline\n(`medicationCodeableConcept`) or via a `medicationReference` to a contained,\nrelative (`Type/id`), or bundle-entry (`urn:uuid`) `Medication` resource.\nResources that cannot be shaped into a row \u2014 a medication with no usable\ncode, resolvable reference, or display, or any clinical resource whose\nsubject/patient reference cannot be tied to a person \u2014 are reported under\n`dropped` rather than emitted as blank rows. The Bundle must contain at\nleast one Patient resource.\n\nAll row IDs start at `1` for each request and are not stable or global.\nFor clinical conversion rows whose resource supplies an `id`, `mappings`\nassociates each row with that source FHIR resource ID. A `person` row\nretains the Patient ID or its first identifier value in\n`person_source_value`, when present; other reference and derived rows do\nnot uniformly carry a FHIR resource ID. Input resources without those\nsource identifiers cannot be correlated across responses from the\nreturned rows alone. Consumers combining responses need to establish\ntheir own stable keys and remap every primary and foreign key together.\n", "requestBody": { "required": true, "content": { @@ -3350,6 +3350,46 @@ ] } } + }, + "oid_form_coding": { + "summary": "OID-form LOINC coding", + "description": "Known FHIR OID URNs are accepted for `Coding.system`. This\nLOINC OID is resolved as `http://loinc.org`; responses use\nthat canonical URL in source fields and mappings.\n", + "value": { + "fhir_resources": { + "resourceType": "Bundle", + "type": "collection", + "entry": [ + { + "resource": { + "resourceType": "Patient", + "id": "patient-1" + } + }, + { + "resource": { + "resourceType": "Observation", + "id": "hemoglobin-1", + "subject": { + "reference": "Patient/patient-1" + }, + "code": { + "coding": [ + { + "system": "urn:oid:2.16.840.1.113883.6.1", + "code": "718-7", + "display": "Hemoglobin" + } + ] + }, + "valueQuantity": { + "value": 13.5, + "unit": "g/dL" + } + } + } + ] + } + } } } } @@ -3366,7 +3406,7 @@ "examples": { "mapping_result": { "summary": "Mapping result", - "description": "The example bundle mapped to OMOP. Both source codes are already\nstandard, so each clinical row carries its own OMOP `concept_id`\nwith `ALREADY_STANDARD` status and a `target_code` equal to the\nsource code. Illustrative `concept_id` values.\n", + "description": "The example bundle mapped to OMOP. Both source codes are already\nstandard, so each clinical row carries its own OMOP `concept_id`\nwith `ALREADY_STANDARD` status and a `target_code` equal to the\nsource code. The concept IDs and vocabulary version illustrate\nthe response shape; production values depend on the vocabulary\nrelease used for the request.\n", "value": { "success": true, "message": "FHIR resources mapped to OMOP CDM v5.4", @@ -4582,67 +4622,49 @@ "x-service": "fhir" } }, - "/cohort": { + "/lang2fhir/batch": { "post": { - "operationId": "cohort_analyze", - "summary": "Analyze text for patient cohort criteria", - "description": "Converts natural language text into structured FHIR search queries for patient cohort analysis", + "operationId": "batch_create", + "summary": "Create a batch job", + "description": "Opens an empty batch job. Items arrive on later upload calls and the set\nis sealed at finalize.\n\nSupplying `request_id` makes the create idempotent on that token: a\nretried submit whose response was lost returns the original job rather\nthan opening a second one. This dedupe is scoped to the calling\ncredential. A `request_id` whose job was canceled or failed before it\nfinalized is released for a fresh replay; once a job is finalized, its\n`request_id` keeps resolving to it even after cancellation.\n\nThere is no limit on how many jobs an instance may hold at once; how many\nitems run in parallel is a property of the instance, not of the job count.\n", "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/cohort_CohortRequest" + "$ref": "#/components/schemas/batch_CreateBatchRequest" }, "example": { - "text": "female patients over 65 with diabetes but not hypertension" + "request_id": "submit-2025-09-02-batch-001" } } } }, "responses": { - "200": { - "description": "Successfully analyzed cohort criteria", + "202": { + "description": "Batch job created (or an idempotent replay of an existing job)", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/cohort_CohortResponse" - }, - "example": { - "success": true, - "message": "Cohort analysis completed successfully. Generated 3 search queries from 3 concepts.", - "queries": [ - { - "resource_type": "Patient", - "search_params": "gender=female&birthdate=le1959-01-01", - "concept": "female patients over 65", - "exclude": false - }, - { - "resource_type": "Condition", - "search_params": "code=44054006", - "concept": "diabetes", - "exclude": false - }, - { - "resource_type": "Condition", - "search_params": "code=38341003", - "concept": "hypertension", - "exclude": true - } - ] + "$ref": "#/components/schemas/batch_BatchJob" } } } }, "400": { - "description": "Invalid request" + "description": "Invalid request (e.g. request_id over its 256-byte limit)" }, "401": { "description": "Unauthorized" }, + "499": { + "description": "Client closed request before response was ready" + }, "500": { "description": "Server error" + }, + "504": { + "description": "Request timed out" } }, "security": [ @@ -4651,146 +4673,58 @@ } ], "x-service": "lang2fhir" - } - }, - "/lang2fhir/create": { - "post": { - "operationId": "lang2fhir_create", - "summary": "Create FHIR resource from text", - "description": "Converts natural language text into a structured FHIR resource.\n\n**Patient identifier handling.** When generating a `patient` (or `patient-canvas`) resource, US Core requires `Patient.identifier` (a business identifier such as an MRN). When the source text contains an identifier, it is extracted with an appropriate URI system. When the source text does not contain a detectable identifier, a synthetic one is generated with `system: \"urn:phenoml:lang2fhir-generated-id\"` and a UUID `value` so the resource remains FHIR-valid and US Core conformant. Callers who need a tenant-specific namespace should rewrite the synthetic system after extraction.\n", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/lang2fhir_CreateRequest" - }, - "examples": { - "condition_resource": { - "summary": "Condition Resource", - "description": "Create a FHIR Condition from a diagnosis description", - "value": { - "text": "Patient has severe persistent asthma with acute exacerbation", - "version": "R4", - "resource": "condition-encounter-diagnosis" - } - }, - "medication_request": { - "summary": "MedicationRequest", - "description": "Create a FHIR MedicationRequest from prescription text", - "value": { - "text": "Prescribe Amoxicillin 500mg capsules, take one capsule three times daily for 10 days", - "version": "R4", - "resource": "medicationrequest" - } - } - } + }, + "get": { + "operationId": "batch_list", + "summary": "List batch jobs", + "description": "Returns a page of the instance's batch jobs, newest first, without\nper-job counts. Jobs are shared across the instance's credentials, so\nthis lists every batch job on the instance, not just the calling\ncredential's.\n", + "parameters": [ + { + "name": "cursor", + "in": "query", + "required": false, + "description": "Opaque pagination cursor from a previous page's next_cursor.", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "description": "Page size. Defaults to 20; values above 100 are clamped to 100.", + "schema": { + "type": "integer", + "default": 20, + "maximum": 100 } } - }, + ], "responses": { "200": { - "description": "Successfully created FHIR resource", + "description": "A page of batch jobs", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/lang2fhir_FHIRResource" - }, - "examples": { - "condition_resource": { - "summary": "Condition Resource", - "value": { - "resourceType": "Condition", - "clinicalStatus": { - "coding": [ - { - "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", - "code": "active" - } - ] - }, - "code": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "195967001", - "display": "Asthma" - } - ], - "text": "Severe persistent asthma with acute exacerbation" - }, - "severity": { - "coding": [ - { - "system": "http://snomed.info/sct", - "code": "24484000", - "display": "Severe" - } - ] - } - } - }, - "medication_request": { - "summary": "MedicationRequest", - "value": { - "resourceType": "MedicationRequest", - "status": "active", - "intent": "order", - "medicationCodeableConcept": { - "coding": [ - { - "system": "http://www.nlm.nih.gov/research/umls/rxnorm", - "code": "308182", - "display": "Amoxicillin 500 MG Oral Capsule" - } - ] - }, - "dosageInstruction": [ - { - "text": "Take one capsule three times daily", - "timing": { - "repeat": { - "frequency": 3, - "period": 1, - "periodUnit": "d" - } - }, - "doseAndRate": [ - { - "doseQuantity": { - "value": 500, - "unit": "mg" - } - } - ] - } - ], - "dispenseRequest": { - "expectedSupplyDuration": { - "value": 10, - "unit": "days" - } - } - } - } + "$ref": "#/components/schemas/batch_JobListResponse" } } } }, "400": { - "description": "Invalid request" + "description": "Invalid request (e.g. non-integer limit or malformed cursor)" }, "401": { "description": "Unauthorized" }, - "404": { - "description": "Profile not found" - }, - "422": { - "description": "Generated FHIR resource failed validation" + "499": { + "description": "Client closed request before response was ready" }, "500": { "description": "Server error" + }, + "504": { + "description": "Request timed out" } }, "security": [ @@ -4801,45 +4735,684 @@ "x-service": "lang2fhir" } }, - "/lang2fhir/create/multi": { + "/lang2fhir/batch/{job_id}/items": { "post": { - "operationId": "lang2fhir_createMulti", - "summary": "Extract multiple FHIR resources from text", - "description": "Analyzes natural language text and extracts multiple FHIR resources, returning them as a transaction Bundle.\nAutomatically detects Patient, Condition, MedicationRequest, Observation, and other resource types from the text.\nResources are linked with proper references (e.g., Conditions reference the Patient).\n\n**Patient identifier handling.** US Core requires `Patient.identifier` (a business identifier such as an MRN). When the source text contains an identifier, it is extracted with an appropriate URI system. When the source text does not contain a detectable identifier, a synthetic one is generated with `system: \"urn:phenoml:lang2fhir-generated-id\"` and a UUID `value` so the bundle remains FHIR-valid and US Core conformant. Callers who need a tenant-specific namespace should rewrite the synthetic system after extraction.\n", + "operationId": "batch_uploadItem", + "summary": "Upload one batch item", + "description": "Stores one item of a job from a multipart upload. A batch's items arrive\none per request. The item carries **either** a `document` extraction\n(whose input file rides as raw bytes in the `file` part) **or** a\n`create` extraction (JSON only, no file).\n\nThe upload enforces these rules:\n- Set **exactly one** of `document` or `create`. Setting both, or\n neither, is a `400`.\n- When `document` is set, `file` is **required** \u2014 it supplies the\n document's file content (PDF, image, RTF, or XML/C-CDA).\n- When `create` is set, `file` is **forbidden** \u2014 a create item carries\n no file.\n- `document` and `create` must each be a JSON **object**.\n\nOnly the item's structure is checked here: the fields inside `document`\nor `create` are not validated at upload. A body that is well-formed JSON\nbut not a valid request for its endpoint is still accepted with `202`\nand fails later during processing, recorded as an item `error`. A\nwrong-typed field the endpoint cannot decode fails as `invalid_input`; a\nbody that decodes but the pipeline rejects (for example, a missing\nrequired field) fails as `processing_failed`.\n\nSupplying `request_id` makes the upload idempotent on that token. A\nre-upload under the same token overwrites the same item rather than\nadding a second, so a client that lost an upload's response can safely\nre-send it. The response's `deduplicated` is `true` only when the\nre-uploaded payload matches the one already stored; a same-token upload\nwith a changed payload overwrites in place and returns `false`.\n\nSet a `request_id` on **every** upload: re-sending under the same token\nis the only way to repair a lost or incomplete upload, including the one\na finalize `409` reports. Without one, a re-send adds a new item instead\nof replacing the missing one, and the job cannot be finalized.\n\nUploads are rejected once the job has been finalized (`409`), once it\nholds its 500-item limit (`409`), or when the item is too large (`413` \u2014\nsee the raw-file limit in the API description).\n", + "parameters": [ + { + "name": "job_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], "requestBody": { "required": true, "content": { - "application/json": { + "multipart/form-data": { "schema": { - "$ref": "#/components/schemas/lang2fhir_CreateMultiRequest" - }, - "examples": { - "full_patient_record": { - "summary": "Full Patient Record", - "description": "Extract multiple FHIR resources from a clinical note", - "value": { - "text": "John Smith, 45-year-old male, diagnosed with Type 2 Diabetes. Prescribed Metformin 500mg twice daily. Blood pressure 140/90.", - "version": "R4" + "type": "object", + "properties": { + "document": { + "type": "object", + "description": "The JSON body of `POST /lang2fhir/document/multi`, **without**\nits base64 `content` field \u2014 the uploaded `file` supplies the\ncontent. Accepts that endpoint's fields (`version`, `provider`,\n`primary_patient`, `patient_reference` (deprecated), `implementation_guide`, `detection_effort`,\n`validation_method`, `config`). This is the **multi**-resource\nbody: it has no single-`resource` field, and the item's result\nis a `DocumentMultiResponse` (a Bundle of resources). Mutually\nexclusive with `create`; requires `file`. Do not combine\n`primary_patient` with `patient_reference`.\n" + }, + "create": { + "type": "object", + "description": "The JSON body of `POST /lang2fhir/create/multi`. Accepts that\nendpoint's fields (`text`, `version`, `provider`,\n`primary_patient`, `patient_reference` (deprecated), `implementation_guide`, `detection_effort`,\n`validation_method`, `resource_review`). This is the\n**multi**-resource body: it has no single-`resource` field, and\nthe item's result is a `CreateMultiResponse` (a Bundle of\nresources). Mutually exclusive with `document`; must **not** be\naccompanied by a `file`. Do not combine `primary_patient` with\n`patient_reference`.\n" + }, + "file": { + "type": "string", + "format": "binary", + "description": "The document's file content (PDF, PNG, JPEG, TIFF, RTF, or\nXML/C-CDA). The document pipeline accepts files up to 20 MiB;\nan upload that passes the storage cap but exceeds this limit\nfails during processing. RTF and XML/C-CDA documents whose\nextracted text exceeds 1 MiB also fail during processing.\nGeneric XML must include an XML declaration; C-CDA documents\nrooted at `ClinicalDocument` may omit it.\nRequired with `document`; forbidden with `create`.\n" + }, + "request_id": { + "type": "string", + "description": "Optional idempotency token (at most 256 UTF-8 bytes).\nRe-uploading under the same token overwrites the same item\ninstead of adding a new one. The token is scoped to this job;\nthe same token in another job is independent and creates a\nseparate item.\n" + }, + "id": { + "type": "string", + "description": "Optional caller-supplied correlation label (at most 512 UTF-8\nbytes), echoed back on status and result listings so you can\nmatch the server's item_id to your own record.\n" } } + }, + "encoding": { + "document": { + "contentType": "application/json" + }, + "create": { + "contentType": "application/json" + } } } } }, "responses": { - "200": { - "description": "Successfully extracted FHIR resources", + "202": { + "description": "Item accepted", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/lang2fhir_CreateMultiResponse" - }, - "examples": { - "full_patient_record": { - "summary": "Full Patient Record", - "value": { - "success": true, - "message": "Successfully extracted 3 resources", + "$ref": "#/components/schemas/batch_UploadItemResponse" + } + } + } + }, + "400": { + "description": "Invalid upload \u2014 e.g. both or neither of document/create set, a\nnon-object document/create, a document item missing its file, a\ncreate item carrying a file, or an over-length request_id/id.\n" + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Batch job not found" + }, + "409": { + "description": "The job is finalized (no longer accepting items) or holds its 500-item limit" + }, + "413": { + "description": "The upload is too large. Both size trips return 413: the request body\nexceeding the 32 MiB upload cap, and the item's stored payload\nexceeding the 28 MiB per-item limit (~21 MiB of raw file once\nbase64-encoded).\n" + }, + "499": { + "description": "Client closed request before response was ready" + }, + "500": { + "description": "Server error" + }, + "504": { + "description": "Request timed out" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "lang2fhir" + } + }, + "/lang2fhir/batch/{job_id}/finalize": { + "post": { + "operationId": "batch_finalize", + "summary": "Finalize a batch job", + "description": "Seals the job's item set and starts processing. Takes no request body.\nFinalize is idempotent: a retried finalize succeeds again.\n\nIf a previous upload did not complete, finalize returns a `409`; re-send\nthe missing upload (with the same `request_id`), then finalize.\nFinalizing a job with no items is a `400`.\n", + "parameters": [ + { + "name": "job_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "202": { + "description": "Job finalized and processing started", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/batch_BatchJob" + } + } + } + }, + "400": { + "description": "The batch has no items to finalize" + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Batch job not found" + }, + "409": { + "description": "A previous upload did not complete; re-send it with the same request_id, then finalize" + }, + "499": { + "description": "Client closed request before response was ready" + }, + "500": { + "description": "Server error" + }, + "504": { + "description": "Request timed out" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "lang2fhir" + } + }, + "/lang2fhir/batch/{job_id}/cancel": { + "post": { + "operationId": "batch_cancel", + "summary": "Cancel a batch job", + "description": "Drives a job to the terminal `canceled` state on request. Takes no\nrequest body.\n\nCancel does not delete the job: the job record and any results already\nproduced are preserved for the normal retention window, the same as a\n`completed` or `failed` job. Items stop being processed and keep the state\nthey held at cancellation, so a canceled job's `counts` may show\nunfinished items that never resolve.\n\nCancel is idempotent: canceling an already-`canceled` job returns `200`\nwith the job. Canceling a job that has already `completed` or `failed` is\na `409`.\n", + "parameters": [ + { + "name": "job_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The job, now canceled", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/batch_BatchJob" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Batch job not found" + }, + "409": { + "description": "The job has already completed or failed and cannot be canceled" + }, + "499": { + "description": "Client closed request before response was ready" + }, + "500": { + "description": "Server error" + }, + "504": { + "description": "Request timed out" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "lang2fhir" + } + }, + "/lang2fhir/batch/{job_id}": { + "get": { + "operationId": "batch_get", + "summary": "Get a batch job", + "description": "Returns a job's record, its per-status item counts, and one page of\nper-item statuses.\n\nItems are listed in a stable order that is not upload order and is the\nsame across pages. Match each entry to your own records by its `id`\n(your correlation label) or `item_id` (from the upload response),\nnever by position.\n", + "parameters": [ + { + "name": "job_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "description": "Opaque pagination cursor from a previous page's next_cursor.", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "description": "Page size for the item-status page. Defaults to 20; values above 100 are clamped to 100.", + "schema": { + "type": "integer", + "default": 20, + "maximum": 100 + } + } + ], + "responses": { + "200": { + "description": "The job record, counts, and a page of item statuses", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/batch_JobDetailResponse" + } + } + } + }, + "400": { + "description": "Invalid request (e.g. non-integer limit or malformed cursor)" + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Batch job not found" + }, + "499": { + "description": "Client closed request before response was ready" + }, + "500": { + "description": "Server error" + }, + "504": { + "description": "Request timed out" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "lang2fhir" + } + }, + "/lang2fhir/batch/{job_id}/results": { + "get": { + "operationId": "batch_getResults", + "summary": "List a batch job's results", + "description": "A lighter status page. Returns the same per-item status entries as\n`GET /lang2fhir/batch/{job_id}`, but without the job record or counts,\nand the entries carry `result_size` rather than any result content. Use\neach entry's `item_id` to fetch that item's result from\n`GET /lang2fhir/batch/{job_id}/results/{item_id}`.\n\nEntries are listed in a stable order that is not upload order and is\nthe same across pages. Match each entry to your own records by its `id`\n(your correlation label) or `item_id` (from the upload response),\nnever by position.\n", + "parameters": [ + { + "name": "job_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "description": "Opaque pagination cursor from a previous page's next_cursor.", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "description": "Page size. Defaults to 20; values above 100 are clamped to 100.", + "schema": { + "type": "integer", + "default": 20, + "maximum": 100 + } + } + ], + "responses": { + "200": { + "description": "A page of per-item result statuses", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/batch_ResultsPageResponse" + } + } + } + }, + "400": { + "description": "Invalid request (e.g. non-integer limit or malformed cursor)" + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Batch job not found" + }, + "499": { + "description": "Client closed request before response was ready" + }, + "500": { + "description": "Server error" + }, + "504": { + "description": "Request timed out" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "lang2fhir" + } + }, + "/lang2fhir/batch/{job_id}/results/{item_id}": { + "get": { + "operationId": "batch_getResult", + "summary": "Get one batch item's result", + "description": "Streams one item's stored result bytes verbatim as `application/json`.\nThe body is the response the item's synchronous multi endpoint would have\nreturned \u2014 a `DocumentMultiResponse` for a document item or a\n`CreateMultiResponse` for a create item.\n\nOnly a succeeded item has a result: an item that has not succeeded\n(pending, processing, or failed) is a `409`, and a result that has\nexpired is a `404`.\n", + "parameters": [ + { + "name": "job_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The item's raw result JSON", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "The item's extraction result \u2014 a DocumentMultiResponse (document\nitem) or CreateMultiResponse (create item).\n" + } + } + } + }, + "400": { + "description": "Invalid request (e.g. empty item_id)" + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Batch job or item not found, or the result has expired" + }, + "409": { + "description": "The item has not succeeded, so it has no result to serve" + }, + "499": { + "description": "Client closed request before response was ready" + }, + "500": { + "description": "Server error" + }, + "504": { + "description": "Request timed out" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "lang2fhir" + } + }, + "/cohort": { + "post": { + "operationId": "cohort_analyze", + "summary": "Analyze text for patient cohort criteria", + "description": "Converts natural language text into structured FHIR search queries for patient cohort analysis", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/cohort_CohortRequest" + }, + "example": { + "text": "female patients over 65 with diabetes but not hypertension" + } + } + } + }, + "responses": { + "200": { + "description": "Successfully analyzed cohort criteria", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/cohort_CohortResponse" + }, + "example": { + "success": true, + "message": "Cohort analysis completed successfully. Generated 3 search queries from 3 concepts.", + "queries": [ + { + "resource_type": "Patient", + "search_params": "gender=female&birthdate=le1959-01-01", + "concept": "female patients over 65", + "exclude": false + }, + { + "resource_type": "Condition", + "search_params": "code=44054006", + "concept": "diabetes", + "exclude": false + }, + { + "resource_type": "Condition", + "search_params": "code=38341003", + "concept": "hypertension", + "exclude": true + } + ] + } + } + } + }, + "400": { + "description": "Invalid request" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Server error" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "lang2fhir" + } + }, + "/lang2fhir/create": { + "post": { + "operationId": "lang2fhir_create", + "summary": "Create FHIR resource from text", + "description": "Converts natural language text into a structured FHIR resource.\n\n**Patient identifier handling.** When generating a `patient` (or `patient-canvas`) resource, US Core requires `Patient.identifier` (a business identifier such as an MRN). When the source text contains an identifier, it is extracted with an appropriate URI system. When the source text does not contain a detectable identifier, a synthetic one is generated with `system: \"urn:phenoml:lang2fhir-generated-id\"` and a UUID `value` so the resource remains FHIR-valid and US Core conformant. Callers who need a tenant-specific namespace should rewrite the synthetic system after extraction.\n", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/lang2fhir_CreateRequest" + }, + "examples": { + "condition_resource": { + "summary": "Condition Resource", + "description": "Create a FHIR Condition from a diagnosis description", + "value": { + "text": "Patient has severe persistent asthma with acute exacerbation", + "version": "R4", + "resource": "condition-encounter-diagnosis" + } + }, + "medication_request": { + "summary": "MedicationRequest", + "description": "Create a FHIR MedicationRequest from prescription text", + "value": { + "text": "Prescribe Amoxicillin 500mg capsules, take one capsule three times daily for 10 days", + "version": "R4", + "resource": "medicationrequest" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully created FHIR resource", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/lang2fhir_FHIRResource" + }, + "examples": { + "condition_resource": { + "summary": "Condition Resource", + "value": { + "resourceType": "Condition", + "clinicalStatus": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", + "code": "active" + } + ] + }, + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "195967001", + "display": "Asthma" + } + ], + "text": "Severe persistent asthma with acute exacerbation" + }, + "severity": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "24484000", + "display": "Severe" + } + ] + } + } + }, + "medication_request": { + "summary": "MedicationRequest", + "value": { + "resourceType": "MedicationRequest", + "status": "active", + "intent": "order", + "medicationCodeableConcept": { + "coding": [ + { + "system": "http://www.nlm.nih.gov/research/umls/rxnorm", + "code": "308182", + "display": "Amoxicillin 500 MG Oral Capsule" + } + ] + }, + "dosageInstruction": [ + { + "text": "Take one capsule three times daily", + "timing": { + "repeat": { + "frequency": 3, + "period": 1, + "periodUnit": "d" + } + }, + "doseAndRate": [ + { + "doseQuantity": { + "value": 500, + "unit": "mg" + } + } + ] + } + ], + "dispenseRequest": { + "expectedSupplyDuration": { + "value": 10, + "unit": "days" + } + } + } + } + } + } + } + }, + "400": { + "description": "Invalid request" + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Profile not found" + }, + "422": { + "description": "Generated FHIR resource failed validation or terminology constraints" + }, + "500": { + "description": "Server error" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "lang2fhir" + } + }, + "/lang2fhir/create/multi": { + "post": { + "operationId": "lang2fhir_createMulti", + "summary": "Extract multiple FHIR resources from text", + "description": "Analyzes natural language text and extracts multiple FHIR resources, returning them as a transaction Bundle.\nAutomatically detects Patient, Condition, MedicationRequest, Observation, and other resource types from the text.\nResources are linked with proper references (e.g., Conditions reference the Patient).\n\n**Patient identifier handling.** US Core requires `Patient.identifier` (a business identifier such as an MRN). When the source text contains an identifier, it is extracted with an appropriate URI system. When the source text does not contain a detectable identifier, a synthetic one is generated with `system: \"urn:phenoml:lang2fhir-generated-id\"` and a UUID `value` so the bundle remains FHIR-valid and US Core conformant. Callers who need a tenant-specific namespace should rewrite the synthetic system after extraction.\n", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/lang2fhir_CreateMultiRequest" + }, + "examples": { + "full_patient_record": { + "summary": "Full Patient Record", + "description": "Extract multiple FHIR resources from a clinical note", + "value": { + "text": "John Smith, 45-year-old male, diagnosed with Type 2 Diabetes. Prescribed Metformin 500mg twice daily. Blood pressure 140/90.", + "version": "R4" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Successfully extracted FHIR resources", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/lang2fhir_CreateMultiResponse" + }, + "examples": { + "full_patient_record": { + "summary": "Full Patient Record", + "value": { + "success": true, + "message": "Successfully extracted 3 resources", "bundle": { "resourceType": "Bundle", "type": "transaction", @@ -4907,7 +5480,7 @@ "description": "Profile not found" }, "422": { - "description": "Generated FHIR resource failed validation" + "description": "Generated FHIR resource failed validation or terminology constraints" }, "500": { "description": "Server error" @@ -5067,7 +5640,7 @@ "post": { "operationId": "lang2fhir_document", "summary": "Convert document to FHIR resource", - "description": "Extracts text from a document (PDF or image) and converts it into a structured FHIR resource.\n\n**Patient identifier handling.** When generating a `patient` (or `patient-canvas`) resource, US Core requires `Patient.identifier` (a business identifier such as an MRN). When the source text contains an identifier, it is extracted with an appropriate URI system. When the source text does not contain a detectable identifier, a synthetic one is generated with `system: \"urn:phenoml:lang2fhir-generated-id\"` and a UUID `value` so the resource remains FHIR-valid and US Core conformant. Callers who need a tenant-specific namespace should rewrite the synthetic system after extraction.\n", + "description": "Extracts text from a PDF, image, RTF, or XML/C-CDA document and converts it into a structured FHIR resource.\n\n**Patient identifier handling.** When generating a `patient` (or `patient-canvas`) resource, US Core requires `Patient.identifier` (a business identifier such as an MRN). When the source text contains an identifier, it is extracted with an appropriate URI system. When the source text does not contain a detectable identifier, a synthetic one is generated with `system: \"urn:phenoml:lang2fhir-generated-id\"` and a UUID `value` so the resource remains FHIR-valid and US Core conformant. Callers who need a tenant-specific namespace should rewrite the synthetic system after extraction.\n", "requestBody": { "required": true, "content": { @@ -5078,7 +5651,7 @@ "example": { "version": "R4", "resource": "questionnaire", - "content": "JVBERi0xLjQKJeLjz9MK...(base64-encoded PDF or image bytes)" + "content": "JVBERi0xLjQKJeLjz9MK...(base64-encoded document bytes)" } } } @@ -5117,6 +5690,9 @@ "401": { "description": "Unauthorized" }, + "403": { + "description": "Forbidden - RTF and XML/C-CDA uploads are only available on dedicated instances" + }, "404": { "description": "Profile not found" }, @@ -5145,7 +5721,7 @@ "post": { "operationId": "lang2fhir_documentMulti", "summary": "Extract multiple FHIR resources from a document", - "description": "Extracts text from a document (PDF or image) and converts it into multiple FHIR resources,\nreturned as a transaction Bundle. Combines document text extraction with multi-resource detection.\nAutomatically detects Patient, Condition, MedicationRequest, Observation, and other resource types.\nResources are linked with proper references (e.g., Conditions reference the Patient).\n\n**Patient identifier handling.** US Core requires `Patient.identifier` (a business identifier such as an MRN). When the source text contains an identifier, it is extracted with an appropriate URI system. When the source text does not contain a detectable identifier, a synthetic one is generated with `system: \"urn:phenoml:lang2fhir-generated-id\"` and a UUID `value` so the bundle remains FHIR-valid and US Core conformant. Callers who need a tenant-specific namespace should rewrite the synthetic system after extraction.\n\n**Split classifications (optional).** `config.split_classifications` is a caller-defined list, not a fixed taxonomy. Choose each classification `id` and write a natural-language `description` for the per-page classifier. For each page, the classifier assigns the best-matching classification or leaves the page ungrouped. Classifications with `operation: \"group\"` keep matching pages and label resources extracted from those pages; classifications with `operation: \"drop\"` remove matching pages before extraction. The `clinical` and `admin` ids in the example are illustrative, not a fixed set.\n", + "description": "Extracts text from a PDF, image, RTF, or XML/C-CDA document and converts it into multiple FHIR resources,\nreturned as a transaction Bundle. Combines document text extraction with multi-resource detection.\nAutomatically detects Patient, Condition, MedicationRequest, Observation, and other resource types.\nResources are linked with proper references (e.g., Conditions reference the Patient).\n\n**Patient identifier handling.** US Core requires `Patient.identifier` (a business identifier such as an MRN). When the source text contains an identifier, it is extracted with an appropriate URI system. When the source text does not contain a detectable identifier, a synthetic one is generated with `system: \"urn:phenoml:lang2fhir-generated-id\"` and a UUID `value` so the bundle remains FHIR-valid and US Core conformant. Callers who need a tenant-specific namespace should rewrite the synthetic system after extraction.\n\n**Split classifications (optional).** `config.split_classifications` is a caller-defined list, not a fixed taxonomy. Choose each classification `id` and write a natural-language `description` for the per-page classifier. For each page, the classifier assigns the best-matching classification or leaves the page ungrouped. Classifications with `operation: \"group\"` keep matching pages and label resources extracted from those pages; classifications with `operation: \"drop\"` remove matching pages before extraction. The `clinical` and `admin` ids in the example are illustrative, not a fixed set.\n", "requestBody": { "required": true, "content": { @@ -5155,7 +5731,7 @@ }, "example": { "version": "R4", - "content": "JVBERi0xLjQKJeLjz9MK...(base64-encoded PDF or image bytes)", + "content": "JVBERi0xLjQKJeLjz9MK...(base64-encoded document bytes)", "provider": "medplum", "config": { "split_classifications": [ @@ -5330,6 +5906,9 @@ "401": { "description": "Unauthorized" }, + "403": { + "description": "Forbidden - RTF and XML/C-CDA uploads are only available on dedicated instances" + }, "404": { "description": "Profile not found" }, @@ -5834,39 +6413,376 @@ }, "responses": { "200": { - "description": "Summary generated successfully", + "description": "Summary generated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/summary_CreateSummaryResponse" + }, + "examples": { + "narrative_summary": { + "summary": "Narrative Summary", + "value": { + "success": true, + "summary": "Patient John Doe is a 45-year-old male diagnosed with Type 2 Diabetes Mellitus on January 15, 2024. Current treatment plan includes lifestyle modifications and medication management.", + "warnings": [] + } + }, + "international_patient_summary": { + "summary": "International Patient Summary", + "value": { + "success": true, + "summary": "International Patient Summary for Maria Garcia\n\nAllergies and Intolerances:\n- Penicillin (high criticality)\n\nActive Problems: None documented\nMedications: None documented\nImmunizations: None documented", + "warnings": [] + } + } + } + } + } + }, + "400": { + "description": "Bad request - invalid input. Possible reasons:\n- Invalid or unsupported mode value\n- Missing template_id (required for narrative mode)\n- No Patient resource found in bundle (IPS mode)\n- Multiple Patient resources found in bundle - IPS requires exactly one (IPS mode)\n- Patient resource has no identifiers for reference matching (IPS mode)\n", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/summary_CreateSummaryResponse" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Access denied - template belongs to another user" + }, + "404": { + "description": "Template not found (narrative mode with invalid template_id)" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "summary" + } + }, + "/fhir/implementation-guides": { + "get": { + "operationId": "implementation-guides_list", + "tags": [ + "FHIR Artifacts / Implementation Guides" + ], + "summary": "List implementation guides", + "description": "Returns every implementation guide on this instance \u2014 both guides that\nhave stored metadata (a profile_context) and guides referenced by at\nleast one custom profile \u2014 with the number of profiles in each.\n", + "responses": { + "200": { + "description": "List of implementation guides", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/implementation-guides_ImplementationGuideListResponse" + } + } + } + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden - custom profiles are only available on dedicated instances" + }, + "500": { + "description": "Server error" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "fhir_artifacts" + } + }, + "/fhir/implementation-guides/{name}": { + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "description": "The implementation guide name.", + "schema": { + "type": "string" + }, + "example": "acme-cardiology" + } + ], + "get": { + "operationId": "implementation-guides_get", + "tags": [ + "FHIR Artifacts / Implementation Guides" + ], + "summary": "Get an implementation guide", + "description": "Returns a single implementation guide, including its profile_context and\nthe ids of the profiles that belong to it.\n", + "responses": { + "200": { + "description": "The requested implementation guide", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/implementation-guides_ImplementationGuideDetail" + } + } + } + }, + "400": { + "description": "The name is reserved or malformed" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden - custom profiles are only available on dedicated instances" + }, + "404": { + "description": "Implementation guide not found" + }, + "500": { + "description": "Server error" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "fhir_artifacts" + }, + "put": { + "operationId": "implementation-guides_update", + "tags": [ + "FHIR Artifacts / Implementation Guides" + ], + "summary": "Set an implementation guide's profile context", + "description": "Sets (or clears, with an empty value) the natural-language profile_context\nfor an implementation guide. The context is injected into the LLM during\nresource detection to help select the right profiles from this guide.\nIt applies to every profile in the guide.\n", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/implementation-guides_UpdateImplementationGuideRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Implementation guide successfully updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/implementation-guides_ImplementationGuideSummary" + } + } + } + }, + "400": { + "description": "Invalid implementation guide name or profile_context too long" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden - custom profiles are only available on dedicated instances" + }, + "500": { + "description": "Server error" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "fhir_artifacts" + }, + "delete": { + "operationId": "implementation-guides_delete", + "tags": [ + "FHIR Artifacts / Implementation Guides" + ], + "summary": "Delete an implementation guide family", + "description": "Deletes the stored name-level metadata and any exact canonical package\nversions beneath the guide. Legacy member profile assignments are not\nchanged.\n", + "responses": { + "204": { + "description": "Implementation guide family successfully deleted" + }, + "400": { + "description": "The name is reserved or malformed" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden - custom profiles are only available on dedicated instances" + }, + "404": { + "description": "No implementation guide family exists for this name" + }, + "500": { + "description": "Server error" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "fhir_artifacts" + } + }, + "/fhir/implementation-guides/{name}/versions": { + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "post": { + "operationId": "implementation-guides_createVersion", + "tags": [ + "FHIR Artifacts / Implementation Guides" + ], + "summary": "Publish an exact custom-profile package", + "description": "Publishes an exact package beneath this guide family. PR 2 temporarily\npermits one exact package version per guide family; publishing another\nversion returns `409 Conflict` until multi-version package support lands.\n", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/implementation-guides_CreateCanonicalImplementationGuideRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Canonical package published", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/implementation-guides_ImplementationGuideVersionDetail" + } + } + } + }, + "400": { + "description": "Invalid ImplementationGuide, profile reference, version, or context" + }, + "404": { + "description": "An exact profile reference was not found" + }, + "409": { + "description": "A package family URL or exact version already exists" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "fhir_artifacts" + } + }, + "/fhir/implementation-guides/{name}/versions/{version}": { + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "version", + "in": "path", + "required": true, + "description": "The authored ImplementationGuide.version. It may contain letters, numbers, and the punctuation characters `.`, `_`, `~`, `+`, and `-`; it cannot be exactly `.` or `..`.\n", + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9._~+-]+$" + }, + "example": "1.0.0" + } + ], + "get": { + "operationId": "implementation-guides_getVersion", + "tags": [ + "FHIR Artifacts / Implementation Guides" + ], + "summary": "Get an exact custom-profile package", + "responses": { + "200": { + "description": "Exact canonical package", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/summary_CreateSummaryResponse" - }, - "examples": { - "narrative_summary": { - "summary": "Narrative Summary", - "value": { - "success": true, - "summary": "Patient John Doe is a 45-year-old male diagnosed with Type 2 Diabetes Mellitus on January 15, 2024. Current treatment plan includes lifestyle modifications and medication management.", - "warnings": [] - } - }, - "international_patient_summary": { - "summary": "International Patient Summary", - "value": { - "success": true, - "summary": "International Patient Summary for Maria Garcia\n\nAllergies and Intolerances:\n- Penicillin (high criticality)\n\nActive Problems: None documented\nMedications: None documented\nImmunizations: None documented", - "warnings": [] - } - } + "$ref": "#/components/schemas/implementation-guides_ImplementationGuideVersionDetail" } } } }, "400": { - "description": "Bad request - invalid input. Possible reasons:\n- Invalid or unsupported mode value\n- Missing template_id (required for narrative mode)\n- No Patient resource found in bundle (IPS mode)\n- Multiple Patient resources found in bundle - IPS requires exactly one (IPS mode)\n- Patient resource has no identifiers for reference matching (IPS mode)\n", + "description": "Invalid guide name or version" + }, + "404": { + "description": "Package version not found" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "fhir_artifacts" + } + }, + "/fhir/profiles": { + "get": { + "operationId": "profiles_list", + "tags": [ + "FHIR Artifacts / Profiles" + ], + "summary": "List custom FHIR profiles", + "description": "Returns metadata for every custom (uploaded) FHIR profile on this\ninstance, across all implementation guides. The full StructureDefinition\nJSON is omitted from each entry; fetch a single profile by id to retrieve it.\n\nThe `url` query parameter filters by canonical URL. The canonical URL is the\nstable key other platform features use to reference a profile (FHIR's\n`meta.profile`, `baseDefinition`), since StructureDefinition ids are only\nunique within a package. An unpinned `url` filter returns metadata for\nthe profile's current StructureDefinition. Pinned `url|version` filters\nresolve a retained version when present; otherwise they can fall back to\nthe profile's current StructureDefinition, whose content can change\nthrough the profile update endpoint. A non-matching filter returns an\nempty list, not a 404.\n", + "parameters": [ + { + "name": "url", + "in": "query", + "required": false, + "description": "Filter by canonical URL. Accepts the FHIR pinned form `url|version`; without a version pin, returns the profile's current StructureDefinition metadata.\n", + "schema": { + "type": "string" + }, + "example": "http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0" + } + ], + "responses": { + "200": { + "description": "List of uploaded profiles", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/summary_CreateSummaryResponse" + "$ref": "#/components/schemas/profiles_ProfileListResponse" } } } @@ -5875,13 +6791,10 @@ "description": "Unauthorized" }, "403": { - "description": "Access denied - template belongs to another user" - }, - "404": { - "description": "Template not found (narrative mode with invalid template_id)" + "description": "Forbidden - custom profiles are only available on dedicated instances" }, "500": { - "description": "Internal server error" + "description": "Server error" } }, "security": [ @@ -5889,28 +6802,39 @@ "bearerAuth": [] } ], - "x-service": "summary" - } - }, - "/fhir/implementation-guides": { - "get": { - "operationId": "implementation-guides_list", + "x-service": "fhir_artifacts" + }, + "post": { + "operationId": "profiles_create", "tags": [ - "FHIR Artifacts / Implementation Guides" + "FHIR Artifacts / Profiles" ], - "summary": "List implementation guides", - "description": "Returns every implementation guide on this instance \u2014 both guides that\nhave stored metadata (a profile_context) and guides referenced by at\nleast one custom profile \u2014 with the number of profiles in each.\n", + "summary": "Upload a custom FHIR profile", + "description": "Creates a custom profile from a FHIR StructureDefinition supplied as a JSON\nobject. Metadata such as version, resource type, and url is read from the\nStructureDefinition; the lowercase StructureDefinition id becomes the\nprofile's lookup key. When id is omitted, a random UUID is assigned.\nOptionally group the profile under a named implementation guide.\n", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/profiles_ProfileUploadRequest" + } + } + } + }, "responses": { - "200": { - "description": "List of implementation guides", + "201": { + "description": "Profile successfully created", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/implementation-guides_ImplementationGuideListResponse" + "$ref": "#/components/schemas/profiles_ProfileSummary" } } } }, + "400": { + "description": "Invalid request or profile validation failed. Possible reasons:\n- Missing or invalid StructureDefinition JSON\n- The id collides with a built-in US Core or R4 base profile\n- A custom profile with the same id or url already exists\n- The implementation_guide is reserved or invalid\n" + }, "401": { "description": "Unauthorized" }, @@ -5929,39 +6853,39 @@ "x-service": "fhir_artifacts" } }, - "/fhir/implementation-guides/{name}": { + "/fhir/profiles/{id}": { "parameters": [ { - "name": "name", + "name": "id", "in": "path", "required": true, - "description": "The implementation guide name.", + "description": "The lowercase StructureDefinition id of the custom profile.", "schema": { "type": "string" }, - "example": "acme-cardiology" + "example": "custom-patient" } ], "get": { - "operationId": "implementation-guides_get", + "operationId": "profiles_get", "tags": [ - "FHIR Artifacts / Implementation Guides" + "FHIR Artifacts / Profiles" ], - "summary": "Get an implementation guide", - "description": "Returns a single implementation guide, including its profile_context and\nthe ids of the profiles that belong to it.\n", + "summary": "Get a custom FHIR profile", + "description": "Returns a single custom profile by id, including its full StructureDefinition\nJSON.\n", "responses": { "200": { - "description": "The requested implementation guide", + "description": "The requested profile", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/implementation-guides_ImplementationGuideDetail" + "$ref": "#/components/schemas/profiles_ProfileGetResponse" } } } }, "400": { - "description": "The name is reserved or malformed" + "description": "The id is not a valid profile id" }, "401": { "description": "Unauthorized" @@ -5970,7 +6894,7 @@ "description": "Forbidden - custom profiles are only available on dedicated instances" }, "404": { - "description": "Implementation guide not found" + "description": "Profile not found" }, "500": { "description": "Server error" @@ -5984,35 +6908,35 @@ "x-service": "fhir_artifacts" }, "put": { - "operationId": "implementation-guides_update", + "operationId": "profiles_update", "tags": [ - "FHIR Artifacts / Implementation Guides" + "FHIR Artifacts / Profiles" ], - "summary": "Set an implementation guide's profile context", - "description": "Sets (or clears, with an empty value) the natural-language profile_context\nfor an implementation guide. The context is injected into the LLM during\nresource detection to help select the right profiles from this guide.\nIt applies to every profile in the guide.\n", + "summary": "Update a custom FHIR profile", + "description": "Replaces an existing custom profile with a new StructureDefinition. The\n`id` path parameter is authoritative: if the StructureDefinition includes\nan `id` it must match the path parameter, and if it omits one the path\nparameter is used. The FHIR resource type of the profile cannot change.\nWhen `implementation_guide` is omitted, the profile keeps its existing\nimplementation guide. A retained version string is allowed only when\nre-submitting the profile's current version with an unchanged\nStructureDefinition; otherwise it returns a conflict. While the profile\nhas retained versions, its\ncanonical URL cannot be changed.\n", "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/implementation-guides_UpdateImplementationGuideRequest" + "$ref": "#/components/schemas/profiles_ProfileUploadRequest" } } } }, "responses": { "200": { - "description": "Implementation guide successfully updated", + "description": "Profile successfully updated", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/implementation-guides_ImplementationGuideSummary" + "$ref": "#/components/schemas/profiles_ProfileSummary" } } } }, "400": { - "description": "Invalid implementation guide name or profile_context too long" + "description": "Invalid request or profile validation failed. Possible reasons:\n- Missing or invalid StructureDefinition JSON\n- The StructureDefinition id does not match the id path parameter\n- The resource type differs from the existing profile\n- The url collides with a built-in profile or another custom profile\n- The implementation_guide is reserved or invalid\n" }, "401": { "description": "Unauthorized" @@ -6020,6 +6944,12 @@ "403": { "description": "Forbidden - custom profiles are only available on dedicated instances" }, + "404": { + "description": "Profile not found" + }, + "409": { + "description": "Conflict. Possible reasons:\n- The requested StructureDefinition.version is already used by a retained version and this is not an unchanged re-submission of the profile's current version\n- The profile has retained versions and the request changes its canonical URL\n" + }, "500": { "description": "Server error" } @@ -6032,18 +6962,18 @@ "x-service": "fhir_artifacts" }, "delete": { - "operationId": "implementation-guides_delete", + "operationId": "profiles_delete", "tags": [ - "FHIR Artifacts / Implementation Guides" + "FHIR Artifacts / Profiles" ], - "summary": "Delete an implementation guide's metadata", - "description": "Deletes the stored metadata for an implementation guide \u2014 its\nprofile_context and timestamps. Member profiles keep their\nimplementation_guide assignment, so a guide still referenced by at least\none profile continues to appear in listings, just without context or\ntimestamps.\n", + "summary": "Delete a custom FHIR profile", + "description": "Permanently deletes a custom profile by id. This also deletes all retained\nversions for that profile so the canonical URL can be reused by a later\nupload.\n", "responses": { "204": { - "description": "Implementation guide metadata successfully deleted" + "description": "Profile successfully deleted" }, "400": { - "description": "The name is reserved or malformed" + "description": "The id is not a valid profile id" }, "401": { "description": "Unauthorized" @@ -6052,7 +6982,10 @@ "description": "Forbidden - custom profiles are only available on dedicated instances" }, "404": { - "description": "No implementation guide metadata exists for this name" + "description": "Profile not found" + }, + "409": { + "description": "Profile is pinned by an exact implementation guide package" }, "500": { "description": "Server error" @@ -6066,43 +6999,49 @@ "x-service": "fhir_artifacts" } }, - "/fhir/profiles": { + "/fhir/profiles/{id}/versions": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The lowercase StructureDefinition id of the custom profile.", + "schema": { + "type": "string" + }, + "example": "custom-patient" + } + ], "get": { - "operationId": "profiles_list", + "operationId": "profiles_listVersions", "tags": [ "FHIR Artifacts / Profiles" ], - "summary": "List custom FHIR profiles", - "description": "Returns metadata for every custom (uploaded) FHIR profile on this\ninstance, across all implementation guides. The full StructureDefinition\nJSON is omitted from each entry; fetch a single profile by id to retrieve it.\n\nThe `url` query parameter filters by canonical URL. The canonical URL is the\nstable key other platform features use to reference a profile (FHIR's\n`meta.profile`, `baseDefinition`), since StructureDefinition ids are only\nunique within a package. A non-matching filter returns an empty list, not a 404.\n", - "parameters": [ - { - "name": "url", - "in": "query", - "required": false, - "description": "Filter by canonical URL. Accepts the FHIR pinned form `url|version` (split on the last `|`); the bare form matches the current version.\n", - "schema": { - "type": "string" - }, - "example": "http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0" - } - ], + "summary": "List versions for a custom FHIR profile", + "description": "Returns retained versions for a custom profile.\n", "responses": { "200": { - "description": "List of uploaded profiles", + "description": "List of profile versions", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/profiles_ProfileListResponse" + "$ref": "#/components/schemas/profiles_ProfileVersionListResponse" } } } }, + "400": { + "description": "The id is not a valid profile id" + }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden - custom profiles are only available on dedicated instances" }, + "404": { + "description": "Profile not found" + }, "500": { "description": "Server error" } @@ -6115,25 +7054,34 @@ "x-service": "fhir_artifacts" }, "post": { - "operationId": "profiles_create", + "operationId": "profiles_createVersion", "tags": [ "FHIR Artifacts / Profiles" ], - "summary": "Upload a custom FHIR profile", - "description": "Creates a custom profile from a FHIR StructureDefinition supplied as a JSON\nobject. Metadata such as version, resource type, and url is read from the\nStructureDefinition; the lowercase StructureDefinition id becomes the\nprofile's lookup key. When id is omitted, a random UUID is assigned. Code\nsystem configuration is auto-extracted from the snapshot. Optionally group\nthe profile under a named implementation guide.\n", + "summary": "Add a retained version to a custom FHIR profile", + "description": "Adds an immutable StructureDefinition version to a custom profile. If\nthe profile does not exist, it is created from the submitted version.\nThe StructureDefinition must include a non-empty `version`; its\ncanonical URL and resource type must match the profile when one already\nexists. If it includes an `id`, that id must match the path parameter;\nif it omits `id`, the path parameter is used. Profiles created through\nthis endpoint are grouped under `custom`. Posting the profile's current\nStructureDefinition unchanged retains it as a version.\nVersion strings may contain letters, numbers, and the punctuation\ncharacters `.`, `_`, `~`, `+`, and `-`; they cannot be exactly `.` or\n`..`. Each profile can retain up to 250 versions; delete old\nversions before adding more.\n", "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/profiles_ProfileUploadRequest" + "$ref": "#/components/schemas/profiles_ProfileVersionCreateRequest" } } } }, "responses": { "201": { - "description": "Profile successfully created", + "description": "Profile version successfully created", + "headers": { + "Location": { + "description": "Relative URL of the created profile version.", + "schema": { + "type": "string" + }, + "example": "/fhir/profiles/custom-patient/versions/2.0.0" + } + }, "content": { "application/json": { "schema": { @@ -6143,7 +7091,7 @@ } }, "400": { - "description": "Invalid request or profile validation failed. Possible reasons:\n- Missing or invalid StructureDefinition JSON\n- The id collides with a built-in US Core or R4 base profile\n- A custom profile with the same id or url already exists\n" + "description": "Invalid request, version does not match the profile, or the canonical URL is already used by another profile.\n" }, "401": { "description": "Unauthorized" @@ -6151,6 +7099,12 @@ "403": { "description": "Forbidden - custom profiles are only available on dedicated instances" }, + "404": { + "description": "Profile not found" + }, + "409": { + "description": "Conflict. Possible reasons:\n- The requested StructureDefinition.version is already in use by a different StructureDefinition: either the profile's current one or a retained version\n- The profile has reached the retained version limit\n" + }, "500": { "description": "Server error" } @@ -6163,7 +7117,7 @@ "x-service": "fhir_artifacts" } }, - "/fhir/profiles/{id}": { + "/fhir/profiles/{id}/versions/{version}": { "parameters": [ { "name": "id", @@ -6174,18 +7128,29 @@ "type": "string" }, "example": "custom-patient" + }, + { + "name": "version", + "in": "path", + "required": true, + "description": "The authored StructureDefinition.version. It may contain letters, numbers, and the punctuation characters `.`, `_`, `~`, `+`, and `-`; it cannot be exactly `.` or `..`.\n", + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9._~+-]+$" + }, + "example": "2.0.0" } ], "get": { - "operationId": "profiles_get", + "operationId": "profiles_getVersion", "tags": [ "FHIR Artifacts / Profiles" ], - "summary": "Get a custom FHIR profile", - "description": "Returns a single custom profile by id, including its full StructureDefinition JSON.\n", + "summary": "Get a retained custom FHIR profile version", + "description": "Returns metadata and the full StructureDefinition for one retained\nversion. The returned StructureDefinition's id is the profile id. The\npath version is the authored `StructureDefinition.version` value.\n", "responses": { "200": { - "description": "The requested profile", + "description": "The requested profile version", "content": { "application/json": { "schema": { @@ -6195,58 +7160,7 @@ } }, "400": { - "description": "The id is not a valid profile id" - }, - "401": { - "description": "Unauthorized" - }, - "403": { - "description": "Forbidden - custom profiles are only available on dedicated instances" - }, - "404": { - "description": "Profile not found" - }, - "500": { - "description": "Server error" - } - }, - "security": [ - { - "bearerAuth": [] - } - ], - "x-service": "fhir_artifacts" - }, - "put": { - "operationId": "profiles_update", - "tags": [ - "FHIR Artifacts / Profiles" - ], - "summary": "Update a custom FHIR profile", - "description": "Replaces an existing custom profile with a new StructureDefinition. The\n`id` path parameter is authoritative: if the StructureDefinition includes\nan `id` it must match the path parameter, and if it omits one the path\nparameter is used. The FHIR resource type of the profile cannot change.\nCode system configuration is\nre-derived from the new StructureDefinition. When `implementation_guide` is\nomitted, the profile keeps its existing implementation guide. The instance\nstores a single version per canonical URL, so this replaces it in place.\n", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/profiles_ProfileUploadRequest" - } - } - } - }, - "responses": { - "200": { - "description": "Profile successfully updated", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/profiles_ProfileSummary" - } - } - } - }, - "400": { - "description": "Invalid request or profile validation failed. Possible reasons:\n- Missing or invalid StructureDefinition JSON\n- The StructureDefinition id does not match the id path parameter\n- The resource type differs from the existing profile\n- The url collides with a built-in profile or another custom profile\n" + "description": "The id or version is invalid" }, "401": { "description": "Unauthorized" @@ -6255,7 +7169,7 @@ "description": "Forbidden - custom profiles are only available on dedicated instances" }, "404": { - "description": "Profile not found" + "description": "Profile or version not found" }, "500": { "description": "Server error" @@ -6269,18 +7183,18 @@ "x-service": "fhir_artifacts" }, "delete": { - "operationId": "profiles_delete", + "operationId": "profiles_deleteVersion", "tags": [ "FHIR Artifacts / Profiles" ], - "summary": "Delete a custom FHIR profile", - "description": "Permanently deletes a custom profile by id.\n", + "summary": "Delete a retained custom FHIR profile version", + "description": "Deletes one retained version from a custom profile. The path\nversion is the authored `StructureDefinition.version` value.\n", "responses": { "204": { - "description": "Profile successfully deleted" + "description": "Profile version successfully deleted" }, "400": { - "description": "The id is not a valid profile id" + "description": "The id or version is invalid" }, "401": { "description": "Unauthorized" @@ -6289,7 +7203,10 @@ "description": "Forbidden - custom profiles are only available on dedicated instances" }, "404": { - "description": "Profile not found" + "description": "Profile or version not found" + }, + "409": { + "description": "Profile version is pinned by an exact implementation guide package" }, "500": { "description": "Server error" @@ -9519,7 +10436,7 @@ "fhir_resources": { "type": "object", "additionalProperties": true, - "description": "FHIR resources (single resource or Bundle). Must contain at least one\nPatient resource. Supported row-producing resources are Patient,\nEncounter, Condition, Procedure, MedicationRequest,\nMedicationStatement, MedicationAdministration, Immunization,\nObservation, and AllergyIntolerance. Standalone Medication resources\nare consumed by medication references rather than mapped to their own\ntable. Other resource types are accepted but ignored.\n" + "description": "FHIR resources (single resource or Bundle). Must contain at least one\nPatient resource. Supported row-producing resources are Patient,\nLocation, Organization, HealthcareService, Practitioner,\nPractitionerRole, Encounter, Condition, Procedure, MedicationRequest,\nMedicationStatement, MedicationAdministration, Immunization,\nObservation, and AllergyIntolerance. Standalone Medication resources\nare consumed by medication references rather than mapped to their own\ntable. Unsupported resource types are accepted in a Bundle but ignored.\n" } } }, @@ -9551,7 +10468,7 @@ }, "vocab_version": { "type": "string", - "description": "The OMOP vocabulary release the clinical codes were resolved against\n(e.g. \"v20240229\"), for reproducibility. Present when at least one\ncoded concept was resolved.\n" + "description": "The OMOP vocabulary release returned for coded concept resolution\n(for example, \"v20240229\"), for reproducibility. It is generally\nabsent for requests containing only text-only resources.\n" }, "summary": { "$ref": "#/components/schemas/fhir2omop_Summary" @@ -9560,7 +10477,7 @@ }, "fhir2omop_OmopTables": { "type": "object", - "description": "OMOP CDM v5.4 rows grouped by destination table.", + "description": "OMOP CDM v5.4 rows grouped by destination table. IDs are sequential and\nscoped to one response; they are not stable keys across requests.\n", "properties": { "location": { "type": "array", @@ -10219,6 +11136,12 @@ }, "mapping_status": { "type": "string", + "enum": [ + "ALREADY_STANDARD", + "MAPPED", + "UNCHECKED", + "UNMAPPED" + ], "description": "ALREADY_STANDARD (source coding is already a standard OMOP concept),\nMAPPED (source coding was mapped to a standard concept), UNCHECKED (a\nstandard code was suggested \u2014 e.g. for a text-only resource \u2014 but not\nverified against the OMOP vocabulary, so `concept_id` stays `0`), or\nUNMAPPED (no standard concept found).\n" }, "note": { @@ -10228,24 +11151,24 @@ }, "fhir2omop_Summary": { "type": "object", - "description": "The request's data-quality headline: how the coded concepts split across\nresolution outcomes, and the share that was not already in a target\nstandard vocabulary. Each coded resource is counted once (per resolved\nconcept), even when it carried several codings \u2014 unlike `mappings`, which\nhas one entry per coding.\n", + "description": "The request's data-quality headline: how resolution outcomes split, and\nthe share that was not already in a target standard vocabulary. Each\nrow-producing clinical resource is counted once, even when it carried\nseveral codings \u2014 unlike `mappings`, which has one entry per coding.\n", "properties": { "codes_already_standard": { "type": "integer", - "description": "Coded concepts already a standard OMOP concept (ALREADY_STANDARD)." + "description": "Resolution outcomes already a standard OMOP concept (ALREADY_STANDARD)." }, "codes_normalized": { "type": "integer", - "description": "Coded concepts mapped or suggested to a standard concept (MAPPED or UNCHECKED)." + "description": "Resolution outcomes mapped or suggested to a standard concept (MAPPED or UNCHECKED)." }, "codes_unmapped": { "type": "integer", - "description": "Coded concepts with no standard concept found (UNMAPPED)." + "description": "Resolution outcomes with no standard concept found (UNMAPPED)." }, "off_vocab_rate": { "type": "number", "format": "double", - "description": "Share of coded concepts not already standard ((normalized + unmapped) / total)." + "description": "Share of resolution outcomes not already standard ((normalized + unmapped) / total)." } } }, @@ -11023,6 +11946,318 @@ } } }, + "batch_CreateBatchRequest": { + "type": "object", + "description": "Body of a create-batch request. Opens an empty job.", + "properties": { + "request_id": { + "type": "string", + "description": "Optional client idempotency token (at most 256 UTF-8 bytes). A\nretried create with the same token returns the original job instead\nof opening a second one.\n", + "example": "submit-2025-09-02-batch-001" + } + } + }, + "batch_BatchError": { + "type": "object", + "description": "A job- or item-level failure. Never contains document or patient content.", + "required": [ + "kind", + "message" + ], + "properties": { + "kind": { + "type": "string", + "description": "Short stable token to branch on. Item-level kinds: `invalid_input`\n(the stored body was not a valid create/document request),\n`processing_failed` (the conversion failed), `result_too_large` (the\nresult exceeded the storage cap), `input_unavailable` (the input\ncould not be read), and `retries_exhausted` / `attempts_exhausted`\n(the item could not complete after repeated interruptions).\nJob-level kinds: `timeout` (the job did not finish within 36 hours\nof creation).\n", + "example": "processing_failed" + }, + "message": { + "type": "string", + "description": "Human-readable description of the failure.", + "example": "the item could not be converted" + } + } + }, + "batch_BatchJob": { + "type": "object", + "description": "One batch job record.", + "required": [ + "job_id", + "status", + "finalized", + "total_items", + "created_at", + "updated_at", + "expires_at" + ], + "properties": { + "job_id": { + "type": "string", + "description": "Server-assigned job identifier.", + "example": "l2f_batch_6f1d2c3a-8b4e-4f5a-9c7d-0e1f2a3b4c5d" + }, + "request_id": { + "type": "string", + "description": "The idempotency token supplied at create, if any.", + "example": "submit-2025-09-02-batch-001" + }, + "status": { + "type": "string", + "description": "Job status. `completed` means every item has finished \u2014 some may have\nfailed, so check `counts` for the split. `failed` is a whole-job\nfailure (the job could not run at all), distinct from individual item\nfailures, which never fail the job. `canceled` is a caller-requested\ncancellation via `POST /lang2fhir/batch/{job_id}/cancel`; like the\nother terminal states it keeps any results already produced readable\nfor the retention window.\n", + "enum": [ + "pending", + "processing", + "completed", + "failed", + "canceled" + ], + "example": "pending" + }, + "finalized": { + "type": "boolean", + "description": "Whether the job's item set has been sealed.", + "example": false + }, + "total_items": { + "type": "integer", + "description": "The sealed item count. It is 0 until the job is finalized, so an\nupload response always reports 0; poll the job after finalize for the\nreal count.\n", + "example": 12 + }, + "error": { + "description": "A whole-job failure. Present only on a `failed` job.", + "allOf": [ + { + "$ref": "#/components/schemas/batch_BatchError" + } + ] + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "completed_at": { + "type": "string", + "format": "date-time", + "description": "When the job finished. Absent until then." + }, + "expires_at": { + "type": "string", + "format": "date-time", + "description": "When the job and its stored inputs and results are deleted. Set 7\ndays out, with the clock restarting when the job reaches `completed`,\n`failed`, or `canceled`. At expiry the job's `request_id` is freed for reuse.\n" + } + } + }, + "batch_BatchCounts": { + "type": "object", + "description": "A job's per-status item tally.", + "required": [ + "total", + "pending", + "processing", + "succeeded", + "failed" + ], + "properties": { + "total": { + "type": "integer", + "example": 12 + }, + "pending": { + "type": "integer", + "example": 3 + }, + "processing": { + "type": "integer", + "example": 2 + }, + "succeeded": { + "type": "integer", + "example": 6 + }, + "failed": { + "type": "integer", + "example": 1 + } + } + }, + "batch_BatchItemStatus": { + "type": "object", + "description": "One item's status line.", + "required": [ + "item_id", + "status", + "attempts" + ], + "properties": { + "item_id": { + "type": "string", + "description": "Server-assigned opaque item ID, used in result URLs.", + "example": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" + }, + "id": { + "type": "string", + "description": "The caller's correlation label, echoed back; omitted when none was supplied.", + "example": "chart-note-0042" + }, + "status": { + "type": "string", + "description": "Item status.", + "enum": [ + "pending", + "processing", + "succeeded", + "failed" + ], + "example": "succeeded" + }, + "attempts": { + "type": "integer", + "description": "Number of processing attempts started so far. The service may retry\ninterrupted work; a conversion error fails the item without retry.\n", + "example": 1 + }, + "detect_retries": { + "type": "integer", + "format": "int64", + "description": "How many times concept detection was re-run for the item because a\npass returned no concepts. Present only on a succeeded item whose\ndetection re-ran at least once; omitted otherwise.\n", + "example": 1 + }, + "result_size": { + "type": "integer", + "format": "int64", + "description": "Size in bytes of the item's stored result. Present once the item has succeeded.", + "example": 20482 + }, + "error": { + "$ref": "#/components/schemas/batch_BatchError" + }, + "completed_at": { + "type": "string", + "format": "date-time", + "description": "When the item finished. Absent until then." + } + } + }, + "batch_UploadItemResponse": { + "description": "The upload response: the job record plus the ID the server assigned the\nuploaded item.\n", + "allOf": [ + { + "$ref": "#/components/schemas/batch_BatchJob" + }, + { + "type": "object", + "required": [ + "item_id", + "deduplicated" + ], + "properties": { + "item_id": { + "type": "string", + "description": "The server-assigned ID of the uploaded item; use it to fetch the result.", + "example": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" + }, + "id": { + "type": "string", + "description": "The caller's correlation label, echoed back; omitted when none was supplied.", + "example": "chart-note-0042" + }, + "deduplicated": { + "type": "boolean", + "description": "True only when a repeated `request_id` resolved to an item whose\npayload matched the one already stored. A same-token upload with a\nchanged payload overwrites in place and returns false.\n", + "example": false + } + } + } + ] + }, + "batch_JobDetailResponse": { + "description": "A job's record, its counts, and one page of item statuses.\n", + "allOf": [ + { + "$ref": "#/components/schemas/batch_BatchJob" + }, + { + "type": "object", + "required": [ + "counts", + "items", + "has_more" + ], + "properties": { + "counts": { + "$ref": "#/components/schemas/batch_BatchCounts" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/batch_BatchItemStatus" + } + }, + "next_cursor": { + "type": "string", + "description": "Cursor for the next page, when has_more is true." + }, + "has_more": { + "type": "boolean", + "description": "Whether more item statuses remain beyond this page.", + "example": false + } + } + } + ] + }, + "batch_ResultsPageResponse": { + "type": "object", + "description": "A page of per-item result statuses, without the job counts.", + "required": [ + "results", + "has_more" + ], + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/batch_BatchItemStatus" + } + }, + "next_cursor": { + "type": "string", + "description": "Cursor for the next page, when has_more is true." + }, + "has_more": { + "type": "boolean", + "description": "Whether more results remain beyond this page.", + "example": false + } + } + }, + "batch_JobListResponse": { + "type": "object", + "description": "A page of job records, without per-job counts.", + "required": [ + "jobs", + "has_more" + ], + "properties": { + "jobs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/batch_BatchJob" + } + }, + "next_cursor": { + "type": "string", + "description": "Cursor for the next page, when has_more is true." + }, + "has_more": { + "type": "boolean", + "description": "Whether more jobs remain beyond this page.", + "example": false + } + } + }, "cohort_CohortRequest": { "type": "object", "required": [ @@ -11099,13 +12334,16 @@ }, "resource": { "type": "string", - "description": "Type of FHIR resource to create. Use 'auto' for automatic resource type detection, or specify a supported US Core profile. Recommended to use the supported US Core Profiles for validated results but you can also use any custom profile you've uploaded (if you're a develop or launch customer) \n", + "description": "Type of FHIR resource to create. Use 'auto' for automatic resource type detection, or specify a supported profile. The default profile set includes US Core profiles and selected base R4 resources; you can also use any custom profile you've uploaded (if you're a develop or launch customer).\n", "example": "condition-encounter-diagnosis", "enum": [ "auto", "appointment", "condition-encounter-diagnosis", + "familymemberhistory", + "medicationadministration", "medicationrequest", + "medicationstatement", "careplan", "condition-problems-health-concerns", "coverage", @@ -11218,7 +12456,7 @@ }, "lang2fhir_PatientReference": { "type": "object", - "description": "Optional reference to an existing Patient, by business identifier. If a Patient resource is extracted, this identifier is added to that Patient's identifier list (existing identifiers are kept). If no Patient is extracted, generated clinical resources are linked to this patient as a logical reference (subject.identifier) instead of the request failing, and no placeholder Patient is created. Supply the patient-level identifier (not an order or specimen identifier).\n", + "description": "Business identifier for the document's primary patient. When Lang2FHIR identifies that Patient in generated output, it adds this identifier to the Patient's identifier list (preserving existing identifiers). If no Patient is generated, Lang2FHIR uses it in logical references on generated clinical resources. Supply the patient-level identifier (not an order or specimen identifier).\n", "required": [ "system", "value" @@ -11239,6 +12477,71 @@ "value": "abc-123" } }, + "lang2fhir_PrimaryPatientName": { + "type": "object", + "description": "The known portions of the primary patient's name. Provide a non-empty family name or at least one non-empty given name.\n", + "properties": { + "family": { + "type": "string", + "description": "Family name.", + "example": "Smith" + }, + "given": { + "type": "array", + "description": "Given names. Matching succeeds when a generated name has a supplied given name.", + "items": { + "type": "string" + }, + "example": [ + "Jane" + ] + } + } + }, + "lang2fhir_PrimaryPatient": { + "type": "object", + "minProperties": 1, + "description": "Partial context for the patient the document is primarily about. This is not a complete FHIR Patient resource. Lang2FHIR uses the available context to identify a generated primary Patient reliably. An identifier supplied here is added to that Patient; when no Patient is generated, it is used in logical references on generated clinical resources.\n", + "properties": { + "identifier": { + "$ref": "#/components/schemas/lang2fhir_PatientReference" + }, + "name": { + "$ref": "#/components/schemas/lang2fhir_PrimaryPatientName" + }, + "birthDate": { + "type": "string", + "format": "date", + "description": "Complete date of birth in YYYY-MM-DD format.", + "example": "1980-03-12" + }, + "gender": { + "type": "string", + "enum": [ + "male", + "female", + "other", + "unknown" + ], + "description": "Administrative gender. This corroborates another match but does not identify a patient alone.", + "example": "female" + } + }, + "example": { + "identifier": { + "system": "urn:acmehealth:patient_index_id", + "value": "abc-123" + }, + "name": { + "family": "Smith", + "given": [ + "Jane" + ] + }, + "birthDate": "1980-03-12", + "gender": "female" + } + }, "lang2fhir_CreateMultiRequest": { "type": "object", "required": [ @@ -11261,12 +12564,17 @@ "description": "Optional FHIR provider name for provider-specific profiles", "example": "canvas" }, + "primary_patient": { + "$ref": "#/components/schemas/lang2fhir_PrimaryPatient" + }, "patient_reference": { - "$ref": "#/components/schemas/lang2fhir_PatientReference" + "$ref": "#/components/schemas/lang2fhir_PatientReference", + "deprecated": true, + "description": "Deprecated compatibility alias for primary_patient.identifier. Cannot be combined with primary_patient." }, "implementation_guide": { "type": "string", - "description": "Custom Implementation Guide name. When specified, profiles from this IG are included alongside US Core profiles during resource detection. US Core is always the base layer; custom IG profiles are additive.\n", + "description": "Custom Implementation Guide name. When specified, profiles from this IG are included alongside the default profiles during resource detection. Default profiles are always the base layer; custom IG profiles are additive.\n", "example": "acme-cardiology" }, "detection_effort": { @@ -11497,19 +12805,24 @@ }, "content": { "type": "string", - "description": "Base64 encoded file content.\nSupported file types: PDF (application/pdf), PNG (image/png), JPEG (image/jpeg), TIFF (image/tiff).\nFile type is auto-detected from content magic bytes.\n" + "description": "Base64 encoded file content.\nSupported file types: PDF (application/pdf), PNG (image/png), JPEG (image/jpeg), TIFF (image/tiff), RTF (application/rtf), XML/C-CDA (text/xml).\nRTF and XML/C-CDA uploads are available on dedicated instances only.\nFile type is auto-detected from content magic bytes.\nThe decoded file must not exceed 20 MiB. RTF and XML/C-CDA documents whose extracted text exceeds 1 MiB are rejected.\nGeneric XML must include an XML declaration; C-CDA documents rooted at `ClinicalDocument` may omit it.\n" }, "provider": { "type": "string", "description": "Optional FHIR provider name for provider-specific profiles", "example": "canvas" }, + "primary_patient": { + "$ref": "#/components/schemas/lang2fhir_PrimaryPatient" + }, "patient_reference": { - "$ref": "#/components/schemas/lang2fhir_PatientReference" + "$ref": "#/components/schemas/lang2fhir_PatientReference", + "deprecated": true, + "description": "Deprecated compatibility alias for primary_patient.identifier. Cannot be combined with primary_patient." }, "implementation_guide": { "type": "string", - "description": "Custom Implementation Guide name. When specified, profiles from this IG are included alongside US Core profiles during resource detection. US Core is always the base layer; custom IG profiles are additive.\n", + "description": "Custom Implementation Guide name. When specified, profiles from this IG are included alongside the default profiles during resource detection. Default profiles are always the base layer; custom IG profiles are additive.\n", "example": "acme-cardiology" }, "detection_effort": { @@ -11637,7 +12950,7 @@ }, "lang2fhir_ResourceReview": { "type": "object", - "description": "Opt-in, report-only faithfulness audit (honored by /lang2fhir/create/multi and /lang2fhir/document/multi). For each selected resource type an LLM checks whether selected dates and clinical code concepts are actually supported by the full source document. Resources with an unsupported field are pulled out of the returned bundle and reported under resource_review in the response.\n", + "description": "Opt-in faithfulness audit (honored by /lang2fhir/create/multi and /lang2fhir/document/multi). For each selected resource type an LLM checks whether selected dates and clinical code concepts are actually supported by the full source document. An unsupported individual coding is removed when another coding remains in its concept. Resources with an unsupported structural field, a profile-required coding, or no coding remaining in an affected concept, are pulled out of the returned bundle and reported under resource_review in the response.\n", "required": [ "targets" ], @@ -11683,14 +12996,21 @@ }, "lang2fhir_ResourceReviewResult": { "type": "object", - "description": "Present when resource_review was requested and at least one resource was flagged.\n", + "description": "Present when resource_review was requested and at least one resource was quarantined or safely remediated. The returned bundle is authoritative and contains the post-review representation of every retained resource.\n", "properties": { "flagged": { "type": "array", - "description": "Resources pulled from the bundle because a reviewed field was not supported by the source.", + "description": "Resources pulled from the bundle because an unsupported finding could not be safely repaired.", "items": { "$ref": "#/components/schemas/lang2fhir_ResourceReviewFlagged" } + }, + "remediated": { + "type": "array", + "description": "Resources retained in the bundle after unsupported codings were safely removed.", + "items": { + "$ref": "#/components/schemas/lang2fhir_ResourceReviewRemediated" + } } } }, @@ -11712,7 +13032,35 @@ }, "findings": { "type": "array", - "description": "The unsupported fields that caused the resource to be flagged.", + "description": "The findings that caused the resource to be quarantined.", + "items": { + "$ref": "#/components/schemas/lang2fhir_ResourceReviewFinding" + } + } + } + }, + "lang2fhir_ResourceReviewRemediated": { + "type": "object", + "properties": { + "tempId": { + "type": "string", + "description": "The urn:uuid of the remediated resource (its bundle fullUrl).", + "example": "urn:uuid:a842c4bc-f6cb-4555-9741-ac3aec4ef0b8" + }, + "resourceType": { + "type": "string", + "example": "Condition" + }, + "action": { + "type": "string", + "enum": [ + "removed_codings" + ], + "description": "The safe change applied to the resource in the returned bundle." + }, + "findings": { + "type": "array", + "description": "Findings for fields in the pre-remediation resource that caused this action.", "items": { "$ref": "#/components/schemas/lang2fhir_ResourceReviewFinding" } @@ -11741,9 +13089,14 @@ }, "supported": { "type": "boolean", - "description": "Always false for a flagged finding.", + "description": "False when the reviewer found the field unsupported. Do not treat this field as a verdict when unaudited is true.", "example": false }, + "unaudited": { + "type": "boolean", + "description": "True when the reviewer did not return a verdict for this field; the resource was quarantined without treating the finding as evidence that the value is unsupported.", + "example": true + }, "rationale": { "type": "string", "description": "Short explanation of why the value is not supported by the source.", @@ -11790,7 +13143,7 @@ }, "content": { "type": "string", - "description": "Base64 encoded file content.\nSupported file types: PDF (application/pdf), PNG (image/png), JPEG (image/jpeg), TIFF (image/tiff).\nFile type is auto-detected from content magic bytes.\n" + "description": "Base64 encoded file content.\nSupported file types: PDF (application/pdf), PNG (image/png), JPEG (image/jpeg), TIFF (image/tiff), RTF (application/rtf), XML/C-CDA (text/xml).\nRTF and XML/C-CDA uploads are available on dedicated instances only.\nFile type is auto-detected from content magic bytes.\nThe decoded file must not exceed 20 MiB. RTF and XML/C-CDA documents whose extracted text exceeds 1 MiB are rejected.\nGeneric XML must include an XML declaration; C-CDA documents rooted at `ClinicalDocument` may omit it.\n" }, "config": { "$ref": "#/components/schemas/lang2fhir_DocumentConfig" @@ -11985,7 +13338,7 @@ }, "implementation-guides_ImplementationGuideSummary": { "type": "object", - "description": "Metadata for an implementation guide. This is an instance-local grouping record, not a complete FHIR ImplementationGuide resource.\n", + "description": "Metadata for an implementation guide. Canonical fields are present only for published canonical packages; metadata-only legacy records omit them.\n", "properties": { "name": { "type": "string", @@ -12002,6 +13355,14 @@ "description": "The number of custom profiles in this implementation guide.", "example": 3 }, + "canonical_url": { + "type": "string", + "description": "Canonical FHIR ImplementationGuide URL, when the family has an exact package." + }, + "version_count": { + "type": "integer", + "description": "Number of retained exact package versions." + }, "created_at": { "type": "string", "format": "date-time", @@ -12048,6 +13409,125 @@ } ] }, + "implementation-guides_CreateCanonicalImplementationGuideRequest": { + "type": "object", + "required": [ + "implementation_guide", + "profile_refs" + ], + "properties": { + "implementation_guide": { + "$ref": "#/components/schemas/implementation-guides_FHIRImplementationGuide" + }, + "profile_refs": { + "type": "array", + "minItems": 1, + "maxItems": 250, + "description": "Exact canonical `url|version` references to builtin or custom profiles. A package can contain at most 250 references.\n", + "items": { + "type": "string" + } + }, + "profile_context": { + "type": "string", + "maxLength": 2000, + "description": "Natural-language profile-selection context for this package." + } + } + }, + "implementation-guides_ImplementationGuideVersionDetail": { + "type": "object", + "required": [ + "name", + "url", + "version", + "profile_context", + "profiles", + "profile_refs", + "implementation_guide", + "created_at", + "updated_at" + ], + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + }, + "version": { + "type": "string" + }, + "profile_context": { + "type": "string" + }, + "profiles": { + "type": "array", + "items": { + "type": "string" + } + }, + "profile_refs": { + "type": "array", + "items": { + "type": "string" + } + }, + "implementation_guide": { + "$ref": "#/components/schemas/implementation-guides_FHIRImplementationGuide" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + } + }, + "implementation-guides_FHIRImplementationGuide": { + "type": "object", + "description": "A complete authored FHIR ImplementationGuide JSON resource.", + "required": [ + "resourceType", + "url", + "version" + ], + "additionalProperties": true, + "properties": { + "resourceType": { + "type": "string", + "enum": [ + "ImplementationGuide" + ] + }, + "id": { + "type": "string" + }, + "url": { + "type": "string" + }, + "version": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string" + }, + "packageId": { + "type": "string" + }, + "fhirVersion": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "implementation-guides_UpdateImplementationGuideRequest": { "type": "object", "properties": { @@ -12064,6 +13544,32 @@ "required": [ "structure_definition" ], + "example": { + "implementation_guide": "acme-cardiology", + "structure_definition": { + "resourceType": "StructureDefinition", + "id": "custom-patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "name": "CustomPatient", + "status": "active", + "fhirVersion": "4.0.1", + "kind": "resource", + "abstract": false, + "type": "Patient", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient", + "derivation": "constraint", + "snapshot": { + "element": [ + { + "id": "Patient", + "path": "Patient", + "min": 0, + "max": "*" + } + ] + } + } + }, "properties": { "structure_definition": { "allOf": [ @@ -12111,6 +13617,33 @@ }, "profiles_ProfileSummary": { "type": "object", + "description": "Metadata for either a custom profile's current StructureDefinition or one retained version. For retained versions, `source` is always `custom` and `updated_at` is equal to `created_at`.\n", + "required": [ + "id", + "source", + "resource_type", + "url", + "version", + "canonical", + "fhir_version", + "implementation_guide", + "created_at", + "updated_at" + ], + "example": { + "id": "custom-patient", + "source": "custom", + "resource_type": "Patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "version": "1.0.0", + "status": "active", + "date": "2026-08-24", + "canonical": "http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0", + "fhir_version": "4.0.1", + "implementation_guide": "acme-cardiology", + "created_at": "2026-08-24T15:04:05Z", + "updated_at": "2026-08-25T16:04:05Z" + }, "properties": { "id": { "type": "string", @@ -12123,7 +13656,7 @@ "custom", "builtin" ], - "description": "The profile's origin. Listings currently return only custom (uploaded) profiles, so this is always \"custom\" today; built-in (US Core / R4 base) profiles would be surfaced via an opt-in parameter in a future release, not by changing the default behavior.\n", + "description": "The profile's origin. Profile management responses currently return custom (uploaded) profiles, so this is always \"custom\" today.\n", "example": "custom" }, "resource_type": { @@ -12141,6 +13674,21 @@ "description": "The version from the StructureDefinition.version field.", "example": "1.0.0" }, + "status": { + "type": "string", + "description": "The publication status from StructureDefinition.status. Expected FHIR values include `draft`, `active`, `retired`, and `unknown`; the server preserves authored strings.\n", + "example": "active" + }, + "date": { + "type": "string", + "description": "The authored publication date from StructureDefinition.date, when present. This is a FHIR dateTime string and may be less precise than a full timestamp.\n", + "example": "2026-08-24" + }, + "canonical": { + "type": "string", + "description": "The canonical profile reference, including the version pin when present.", + "example": "http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0" + }, "fhir_version": { "type": "string", "description": "The base FHIR version the StructureDefinition targets.", @@ -12157,12 +13705,34 @@ }, "updated_at": { "type": "string", - "format": "date-time" + "format": "date-time", + "description": "Last update timestamp for the profile's current StructureDefinition. For retained versions, this equals `created_at`.\n" } } }, "profiles_ProfileListResponse": { "type": "object", + "required": [ + "profiles" + ], + "example": { + "profiles": [ + { + "id": "custom-patient", + "source": "custom", + "resource_type": "Patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "version": "1.0.0", + "status": "active", + "date": "2026-08-24", + "canonical": "http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0", + "fhir_version": "4.0.1", + "implementation_guide": "acme-cardiology", + "created_at": "2026-08-24T15:04:05Z", + "updated_at": "2026-08-25T16:04:05Z" + } + ] + }, "properties": { "profiles": { "type": "array", @@ -12173,12 +13743,54 @@ } }, "profiles_ProfileGetResponse": { + "description": "Metadata and full StructureDefinition JSON for either a custom profile's current StructureDefinition or one retained version.\n", + "example": { + "id": "custom-patient", + "source": "custom", + "resource_type": "Patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "version": "1.0.0", + "status": "active", + "date": "2026-08-24", + "canonical": "http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0", + "fhir_version": "4.0.1", + "implementation_guide": "acme-cardiology", + "created_at": "2026-08-24T15:04:05Z", + "updated_at": "2026-08-25T16:04:05Z", + "structure_definition": { + "resourceType": "StructureDefinition", + "id": "custom-patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "version": "1.0.0", + "name": "CustomPatient", + "status": "active", + "fhirVersion": "4.0.1", + "kind": "resource", + "abstract": false, + "type": "Patient", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient", + "derivation": "constraint", + "snapshot": { + "element": [ + { + "id": "Patient", + "path": "Patient", + "min": 0, + "max": "*" + } + ] + } + } + }, "allOf": [ { "$ref": "#/components/schemas/profiles_ProfileSummary" }, { "type": "object", + "required": [ + "structure_definition" + ], "properties": { "structure_definition": { "allOf": [ @@ -12192,6 +13804,82 @@ } ] }, + "profiles_ProfileVersionCreateRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/profiles_FHIRResource" + } + ], + "description": "A FHIR StructureDefinition as a JSON object. Must include resourceType: StructureDefinition, url, type, version, and a snapshot with elements. The canonical URL and resource type must match the profile when one already exists. If id is present, its lowercased value must match the path id; if omitted, the path id is used. The version may contain letters, numbers, and the punctuation characters `.`, `_`, `~`, `+`, and `-`; it cannot be exactly `.` or `..`.\n", + "example": { + "resourceType": "StructureDefinition", + "id": "custom-patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "version": "2.0.0", + "name": "CustomPatient", + "status": "active", + "fhirVersion": "4.0.1", + "kind": "resource", + "abstract": false, + "type": "Patient", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient", + "derivation": "constraint", + "snapshot": { + "element": [ + { + "id": "Patient", + "path": "Patient" + } + ] + } + } + }, + "profiles_ProfileVersionListResponse": { + "type": "object", + "required": [ + "versions" + ], + "example": { + "versions": [ + { + "id": "custom-patient", + "source": "custom", + "resource_type": "Patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "version": "2.0.0", + "status": "active", + "date": "2026-08-26", + "canonical": "http://phenoml.com/fhir/StructureDefinition/custom-patient|2.0.0", + "fhir_version": "4.0.1", + "implementation_guide": "acme-cardiology", + "created_at": "2026-08-26T15:04:05Z", + "updated_at": "2026-08-26T15:04:05Z" + }, + { + "id": "custom-patient", + "source": "custom", + "resource_type": "Patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "version": "1.0.0", + "status": "active", + "date": "2026-08-24", + "canonical": "http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0", + "fhir_version": "4.0.1", + "implementation_guide": "acme-cardiology", + "created_at": "2026-08-24T15:04:05Z", + "updated_at": "2026-08-24T15:04:05Z" + } + ] + }, + "properties": { + "versions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/profiles_ProfileSummary" + } + } + } + }, "profiles_FHIRResource": { "type": "object", "description": "A FHIR resource (schema depends on resource type)" @@ -13482,7 +15170,7 @@ }, { "name": "FHIR Artifacts / Profiles", - "description": "Manage custom FHIR profiles (StructureDefinitions) as instance-level FHIR artifacts." + "description": "Manage custom FHIR profiles (StructureDefinitions)." }, { "name": "Tools / MCP Servers", diff --git a/src/test/java/com/phenoml/api/ImplementationGuidesImplementationGuidesWireTest.java b/src/test/java/com/phenoml/api/ImplementationGuidesImplementationGuidesWireTest.java index c268007d..533d016f 100644 --- a/src/test/java/com/phenoml/api/ImplementationGuidesImplementationGuidesWireTest.java +++ b/src/test/java/com/phenoml/api/ImplementationGuidesImplementationGuidesWireTest.java @@ -3,10 +3,14 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.phenoml.api.core.ObjectMappers; +import com.phenoml.api.resources.implementationguides.implementationguides.requests.CreateCanonicalImplementationGuideRequest; import com.phenoml.api.resources.implementationguides.implementationguides.requests.UpdateImplementationGuideRequest; +import com.phenoml.api.resources.implementationguides.types.FhirImplementationGuide; import com.phenoml.api.resources.implementationguides.types.ImplementationGuideDetail; import com.phenoml.api.resources.implementationguides.types.ImplementationGuideListResponse; import com.phenoml.api.resources.implementationguides.types.ImplementationGuideSummary; +import com.phenoml.api.resources.implementationguides.types.ImplementationGuideVersionDetail; +import java.util.Arrays; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; @@ -44,7 +48,7 @@ public void testList() throws Exception { new MockResponse() .setResponseCode(200) .setBody( - "{\"implementation_guides\":[{\"name\":\"acme-cardiology\",\"profile_context\":\"When the text mentions phenotypic features, prefer the hpo-observation profile over Condition.\",\"profile_count\":3,\"created_at\":\"2024-01-15T09:30:00Z\",\"updated_at\":\"2024-01-15T09:30:00Z\"}]}")); + "{\"implementation_guides\":[{\"name\":\"acme-cardiology\",\"profile_context\":\"When the text mentions phenotypic features, prefer the hpo-observation profile over Condition.\",\"profile_count\":3,\"canonical_url\":\"canonical_url\",\"version_count\":1,\"created_at\":\"2024-01-15T09:30:00Z\",\"updated_at\":\"2024-01-15T09:30:00Z\"}]}")); ImplementationGuideListResponse response = client.implementationGuides().implementationGuides().list(); // OAuth: consume the token request @@ -69,6 +73,8 @@ public void testList() throws Exception { + " \"name\": \"acme-cardiology\",\n" + " \"profile_context\": \"When the text mentions phenotypic features, prefer the hpo-observation profile over Condition.\",\n" + " \"profile_count\": 3,\n" + + " \"canonical_url\": \"canonical_url\",\n" + + " \"version_count\": 1,\n" + " \"created_at\": \"2024-01-15T09:30:00Z\",\n" + " \"updated_at\": \"2024-01-15T09:30:00Z\"\n" + " }\n" @@ -115,7 +121,7 @@ public void testGet() throws Exception { new MockResponse() .setResponseCode(200) .setBody( - "{\"name\":\"acme-cardiology\",\"profile_context\":\"When the text mentions phenotypic features, prefer the hpo-observation profile over Condition.\",\"profile_count\":3,\"created_at\":\"2024-01-15T09:30:00Z\",\"updated_at\":\"2024-01-15T09:30:00Z\",\"profiles\":[\"custom-patient\",\"acme-vital-signs\"]}")); + "{\"name\":\"acme-cardiology\",\"profile_context\":\"When the text mentions phenotypic features, prefer the hpo-observation profile over Condition.\",\"profile_count\":3,\"canonical_url\":\"canonical_url\",\"version_count\":1,\"created_at\":\"2024-01-15T09:30:00Z\",\"updated_at\":\"2024-01-15T09:30:00Z\",\"profiles\":[\"custom-patient\",\"acme-vital-signs\"]}")); ImplementationGuideDetail response = client.implementationGuides().implementationGuides().get("acme-cardiology"); // OAuth: consume the token request @@ -138,6 +144,8 @@ public void testGet() throws Exception { + " \"name\": \"acme-cardiology\",\n" + " \"profile_context\": \"When the text mentions phenotypic features, prefer the hpo-observation profile over Condition.\",\n" + " \"profile_count\": 3,\n" + + " \"canonical_url\": \"canonical_url\",\n" + + " \"version_count\": 1,\n" + " \"created_at\": \"2024-01-15T09:30:00Z\",\n" + " \"updated_at\": \"2024-01-15T09:30:00Z\",\n" + " \"profiles\": [\n" @@ -186,7 +194,7 @@ public void testUpdate() throws Exception { new MockResponse() .setResponseCode(200) .setBody( - "{\"name\":\"acme-cardiology\",\"profile_context\":\"When the text mentions phenotypic features, prefer the hpo-observation profile over Condition.\",\"profile_count\":3,\"created_at\":\"2024-01-15T09:30:00Z\",\"updated_at\":\"2024-01-15T09:30:00Z\"}")); + "{\"name\":\"acme-cardiology\",\"profile_context\":\"When the text mentions phenotypic features, prefer the hpo-observation profile over Condition.\",\"profile_count\":3,\"canonical_url\":\"canonical_url\",\"version_count\":1,\"created_at\":\"2024-01-15T09:30:00Z\",\"updated_at\":\"2024-01-15T09:30:00Z\"}")); ImplementationGuideSummary response = client.implementationGuides() .implementationGuides() .update( @@ -241,6 +249,8 @@ else if (actualJson.has("kind")) + " \"name\": \"acme-cardiology\",\n" + " \"profile_context\": \"When the text mentions phenotypic features, prefer the hpo-observation profile over Condition.\",\n" + " \"profile_count\": 3,\n" + + " \"canonical_url\": \"canonical_url\",\n" + + " \"version_count\": 1,\n" + " \"created_at\": \"2024-01-15T09:30:00Z\",\n" + " \"updated_at\": \"2024-01-15T09:30:00Z\"\n" + "}"; @@ -296,6 +306,226 @@ public void testDelete() throws Exception { "OAuth Authorization header should contain Bearer token from OAuth flow"); } + @Test + public void testCreateVersion() throws Exception { + // OAuth: enqueue token response (client fetches token before API call) + server.enqueue(new MockResponse() + .setResponseCode(200) + .setBody("{\"access_token\":\"test-token\",\"expires_in\":3600}")); + server.enqueue( + new MockResponse() + .setResponseCode(200) + .setBody( + "{\"name\":\"name\",\"url\":\"url\",\"version\":\"version\",\"profile_context\":\"profile_context\",\"profiles\":[\"profiles\"],\"profile_refs\":[\"profile_refs\"],\"implementation_guide\":{\"resourceType\":\"ImplementationGuide\",\"id\":\"id\",\"url\":\"url\",\"version\":\"version\",\"name\":\"name\",\"status\":\"status\",\"packageId\":\"packageId\",\"fhirVersion\":[\"fhirVersion\"]},\"created_at\":\"2024-01-15T09:30:00Z\",\"updated_at\":\"2024-01-15T09:30:00Z\"}")); + ImplementationGuideVersionDetail response = client.implementationGuides() + .implementationGuides() + .createVersion( + "name", + CreateCanonicalImplementationGuideRequest.builder() + .implementationGuide(FhirImplementationGuide.builder() + .url("url") + .version("version") + .build()) + .profileRefs(Arrays.asList("profile_refs")) + .build()); + // OAuth: consume the token request + server.takeRequest(); + RecordedRequest request = server.takeRequest(); + Assertions.assertNotNull(request); + Assertions.assertEquals("POST", request.getMethod()); + + // Validate OAuth Authorization header + Assertions.assertEquals( + "Bearer test-token", + request.getHeader("Authorization"), + "OAuth Authorization header should contain Bearer token from OAuth flow"); + // Validate request body + String actualRequestBody = request.getBody().readUtf8(); + String expectedRequestBody = "" + + "{\n" + + " \"implementation_guide\": {\n" + + " \"resourceType\": \"ImplementationGuide\",\n" + + " \"url\": \"url\",\n" + + " \"version\": \"version\"\n" + + " },\n" + + " \"profile_refs\": [\n" + + " \"profile_refs\"\n" + + " ]\n" + + "}"; + JsonNode actualJson = objectMapper.readTree(actualRequestBody); + JsonNode expectedJson = objectMapper.readTree(expectedRequestBody); + Assertions.assertTrue(jsonEquals(expectedJson, actualJson), "Request body structure does not match expected"); + if (actualJson.has("type") || actualJson.has("_type") || actualJson.has("kind")) { + String discriminator = null; + if (actualJson.has("type")) discriminator = actualJson.get("type").asText(); + else if (actualJson.has("_type")) + discriminator = actualJson.get("_type").asText(); + else if (actualJson.has("kind")) + discriminator = actualJson.get("kind").asText(); + Assertions.assertNotNull(discriminator, "Union type should have a discriminator field"); + Assertions.assertFalse(discriminator.isEmpty(), "Union discriminator should not be empty"); + } + + if (!actualJson.isNull()) { + Assertions.assertTrue( + actualJson.isObject() || actualJson.isArray() || actualJson.isValueNode(), + "request should be a valid JSON value"); + } + + if (actualJson.isArray()) { + Assertions.assertTrue(actualJson.size() >= 0, "Array should have valid size"); + } + if (actualJson.isObject()) { + Assertions.assertTrue(actualJson.size() >= 0, "Object should have valid field count"); + } + + // Validate response body + Assertions.assertNotNull(response, "Response should not be null"); + String actualResponseJson = objectMapper.writeValueAsString(response); + String expectedResponseBody = "" + + "{\n" + + " \"name\": \"name\",\n" + + " \"url\": \"url\",\n" + + " \"version\": \"version\",\n" + + " \"profile_context\": \"profile_context\",\n" + + " \"profiles\": [\n" + + " \"profiles\"\n" + + " ],\n" + + " \"profile_refs\": [\n" + + " \"profile_refs\"\n" + + " ],\n" + + " \"implementation_guide\": {\n" + + " \"resourceType\": \"ImplementationGuide\",\n" + + " \"id\": \"id\",\n" + + " \"url\": \"url\",\n" + + " \"version\": \"version\",\n" + + " \"name\": \"name\",\n" + + " \"status\": \"status\",\n" + + " \"packageId\": \"packageId\",\n" + + " \"fhirVersion\": [\n" + + " \"fhirVersion\"\n" + + " ]\n" + + " },\n" + + " \"created_at\": \"2024-01-15T09:30:00Z\",\n" + + " \"updated_at\": \"2024-01-15T09:30:00Z\"\n" + + "}"; + JsonNode actualResponseNode = objectMapper.readTree(actualResponseJson); + JsonNode expectedResponseNode = objectMapper.readTree(expectedResponseBody); + Assertions.assertTrue( + jsonEquals(expectedResponseNode, actualResponseNode), + "Response body structure does not match expected"); + if (actualResponseNode.has("type") || actualResponseNode.has("_type") || actualResponseNode.has("kind")) { + String discriminator = null; + if (actualResponseNode.has("type")) + discriminator = actualResponseNode.get("type").asText(); + else if (actualResponseNode.has("_type")) + discriminator = actualResponseNode.get("_type").asText(); + else if (actualResponseNode.has("kind")) + discriminator = actualResponseNode.get("kind").asText(); + Assertions.assertNotNull(discriminator, "Union type should have a discriminator field"); + Assertions.assertFalse(discriminator.isEmpty(), "Union discriminator should not be empty"); + } + + if (!actualResponseNode.isNull()) { + Assertions.assertTrue( + actualResponseNode.isObject() || actualResponseNode.isArray() || actualResponseNode.isValueNode(), + "response should be a valid JSON value"); + } + + if (actualResponseNode.isArray()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Array should have valid size"); + } + if (actualResponseNode.isObject()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Object should have valid field count"); + } + } + + @Test + public void testGetVersion() throws Exception { + // OAuth: enqueue token response (client fetches token before API call) + server.enqueue(new MockResponse() + .setResponseCode(200) + .setBody("{\"access_token\":\"test-token\",\"expires_in\":3600}")); + server.enqueue( + new MockResponse() + .setResponseCode(200) + .setBody( + "{\"name\":\"name\",\"url\":\"url\",\"version\":\"version\",\"profile_context\":\"profile_context\",\"profiles\":[\"profiles\"],\"profile_refs\":[\"profile_refs\"],\"implementation_guide\":{\"resourceType\":\"ImplementationGuide\",\"id\":\"id\",\"url\":\"url\",\"version\":\"version\",\"name\":\"name\",\"status\":\"status\",\"packageId\":\"packageId\",\"fhirVersion\":[\"fhirVersion\"]},\"created_at\":\"2024-01-15T09:30:00Z\",\"updated_at\":\"2024-01-15T09:30:00Z\"}")); + ImplementationGuideVersionDetail response = + client.implementationGuides().implementationGuides().getVersion("name", "1.0.0"); + // OAuth: consume the token request + server.takeRequest(); + RecordedRequest request = server.takeRequest(); + Assertions.assertNotNull(request); + Assertions.assertEquals("GET", request.getMethod()); + + // Validate OAuth Authorization header + Assertions.assertEquals( + "Bearer test-token", + request.getHeader("Authorization"), + "OAuth Authorization header should contain Bearer token from OAuth flow"); + + // Validate response body + Assertions.assertNotNull(response, "Response should not be null"); + String actualResponseJson = objectMapper.writeValueAsString(response); + String expectedResponseBody = "" + + "{\n" + + " \"name\": \"name\",\n" + + " \"url\": \"url\",\n" + + " \"version\": \"version\",\n" + + " \"profile_context\": \"profile_context\",\n" + + " \"profiles\": [\n" + + " \"profiles\"\n" + + " ],\n" + + " \"profile_refs\": [\n" + + " \"profile_refs\"\n" + + " ],\n" + + " \"implementation_guide\": {\n" + + " \"resourceType\": \"ImplementationGuide\",\n" + + " \"id\": \"id\",\n" + + " \"url\": \"url\",\n" + + " \"version\": \"version\",\n" + + " \"name\": \"name\",\n" + + " \"status\": \"status\",\n" + + " \"packageId\": \"packageId\",\n" + + " \"fhirVersion\": [\n" + + " \"fhirVersion\"\n" + + " ]\n" + + " },\n" + + " \"created_at\": \"2024-01-15T09:30:00Z\",\n" + + " \"updated_at\": \"2024-01-15T09:30:00Z\"\n" + + "}"; + JsonNode actualResponseNode = objectMapper.readTree(actualResponseJson); + JsonNode expectedResponseNode = objectMapper.readTree(expectedResponseBody); + Assertions.assertTrue( + jsonEquals(expectedResponseNode, actualResponseNode), + "Response body structure does not match expected"); + if (actualResponseNode.has("type") || actualResponseNode.has("_type") || actualResponseNode.has("kind")) { + String discriminator = null; + if (actualResponseNode.has("type")) + discriminator = actualResponseNode.get("type").asText(); + else if (actualResponseNode.has("_type")) + discriminator = actualResponseNode.get("_type").asText(); + else if (actualResponseNode.has("kind")) + discriminator = actualResponseNode.get("kind").asText(); + Assertions.assertNotNull(discriminator, "Union type should have a discriminator field"); + Assertions.assertFalse(discriminator.isEmpty(), "Union discriminator should not be empty"); + } + + if (!actualResponseNode.isNull()) { + Assertions.assertTrue( + actualResponseNode.isObject() || actualResponseNode.isArray() || actualResponseNode.isValueNode(), + "response should be a valid JSON value"); + } + + if (actualResponseNode.isArray()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Array should have valid size"); + } + if (actualResponseNode.isObject()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Object should have valid field count"); + } + } + /** * Compares two JsonNodes with numeric equivalence and null safety. * For objects, checks that all fields in 'expected' exist in 'actual' with matching values. diff --git a/src/test/java/com/phenoml/api/Lang2FhirBatchWireTest.java b/src/test/java/com/phenoml/api/Lang2FhirBatchWireTest.java new file mode 100644 index 00000000..c7b885f3 --- /dev/null +++ b/src/test/java/com/phenoml/api/Lang2FhirBatchWireTest.java @@ -0,0 +1,643 @@ +package com.phenoml.api; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.phenoml.api.core.ObjectMappers; +import com.phenoml.api.resources.lang2fhirbatch.requests.CreateBatchRequest; +import com.phenoml.api.resources.lang2fhirbatch.requests.GetRequest; +import com.phenoml.api.resources.lang2fhirbatch.requests.GetResultsRequest; +import com.phenoml.api.resources.lang2fhirbatch.requests.ListRequest; +import com.phenoml.api.resources.lang2fhirbatch.types.BatchJob; +import com.phenoml.api.resources.lang2fhirbatch.types.JobDetailResponse; +import com.phenoml.api.resources.lang2fhirbatch.types.JobListResponse; +import com.phenoml.api.resources.lang2fhirbatch.types.ResultsPageResponse; +import java.util.Map; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class Lang2FhirBatchWireTest { + private MockWebServer server; + private PhenomlClient client; + private ObjectMapper objectMapper = ObjectMappers.JSON_MAPPER; + + @BeforeEach + public void setup() throws Exception { + server = new MockWebServer(); + server.start(); + client = PhenomlClient.withCredentials("test-client-id", "test-client-secret") + .url(server.url("/").toString()) + .build(); + } + + @AfterEach + public void teardown() throws Exception { + server.shutdown(); + } + + @Test + public void testList() throws Exception { + // OAuth: enqueue token response (client fetches token before API call) + server.enqueue(new MockResponse() + .setResponseCode(200) + .setBody("{\"access_token\":\"test-token\",\"expires_in\":3600}")); + server.enqueue( + new MockResponse() + .setResponseCode(200) + .setBody( + "{\"jobs\":[{\"job_id\":\"l2f_batch_6f1d2c3a-8b4e-4f5a-9c7d-0e1f2a3b4c5d\",\"request_id\":\"submit-2025-09-02-batch-001\",\"status\":\"pending\",\"finalized\":false,\"total_items\":12,\"error\":{\"kind\":\"processing_failed\",\"message\":\"the item could not be converted\"},\"created_at\":\"2024-01-15T09:30:00Z\",\"updated_at\":\"2024-01-15T09:30:00Z\",\"completed_at\":\"2024-01-15T09:30:00Z\",\"expires_at\":\"2024-01-15T09:30:00Z\"}],\"next_cursor\":\"next_cursor\",\"has_more\":false}")); + JobListResponse response = client.lang2FhirBatch() + .list(ListRequest.builder().cursor("cursor").limit(1).build()); + // OAuth: consume the token request + server.takeRequest(); + RecordedRequest request = server.takeRequest(); + Assertions.assertNotNull(request); + Assertions.assertEquals("GET", request.getMethod()); + + // Validate OAuth Authorization header + Assertions.assertEquals( + "Bearer test-token", + request.getHeader("Authorization"), + "OAuth Authorization header should contain Bearer token from OAuth flow"); + + // Validate response body + Assertions.assertNotNull(response, "Response should not be null"); + String actualResponseJson = objectMapper.writeValueAsString(response); + String expectedResponseBody = "" + + "{\n" + + " \"jobs\": [\n" + + " {\n" + + " \"job_id\": \"l2f_batch_6f1d2c3a-8b4e-4f5a-9c7d-0e1f2a3b4c5d\",\n" + + " \"request_id\": \"submit-2025-09-02-batch-001\",\n" + + " \"status\": \"pending\",\n" + + " \"finalized\": false,\n" + + " \"total_items\": 12,\n" + + " \"error\": {\n" + + " \"kind\": \"processing_failed\",\n" + + " \"message\": \"the item could not be converted\"\n" + + " },\n" + + " \"created_at\": \"2024-01-15T09:30:00Z\",\n" + + " \"updated_at\": \"2024-01-15T09:30:00Z\",\n" + + " \"completed_at\": \"2024-01-15T09:30:00Z\",\n" + + " \"expires_at\": \"2024-01-15T09:30:00Z\"\n" + + " }\n" + + " ],\n" + + " \"next_cursor\": \"next_cursor\",\n" + + " \"has_more\": false\n" + + "}"; + JsonNode actualResponseNode = objectMapper.readTree(actualResponseJson); + JsonNode expectedResponseNode = objectMapper.readTree(expectedResponseBody); + Assertions.assertTrue( + jsonEquals(expectedResponseNode, actualResponseNode), + "Response body structure does not match expected"); + if (actualResponseNode.has("type") || actualResponseNode.has("_type") || actualResponseNode.has("kind")) { + String discriminator = null; + if (actualResponseNode.has("type")) + discriminator = actualResponseNode.get("type").asText(); + else if (actualResponseNode.has("_type")) + discriminator = actualResponseNode.get("_type").asText(); + else if (actualResponseNode.has("kind")) + discriminator = actualResponseNode.get("kind").asText(); + Assertions.assertNotNull(discriminator, "Union type should have a discriminator field"); + Assertions.assertFalse(discriminator.isEmpty(), "Union discriminator should not be empty"); + } + + if (!actualResponseNode.isNull()) { + Assertions.assertTrue( + actualResponseNode.isObject() || actualResponseNode.isArray() || actualResponseNode.isValueNode(), + "response should be a valid JSON value"); + } + + if (actualResponseNode.isArray()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Array should have valid size"); + } + if (actualResponseNode.isObject()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Object should have valid field count"); + } + } + + @Test + public void testCreate() throws Exception { + // OAuth: enqueue token response (client fetches token before API call) + server.enqueue(new MockResponse() + .setResponseCode(200) + .setBody("{\"access_token\":\"test-token\",\"expires_in\":3600}")); + server.enqueue( + new MockResponse() + .setResponseCode(200) + .setBody( + "{\"job_id\":\"l2f_batch_6f1d2c3a-8b4e-4f5a-9c7d-0e1f2a3b4c5d\",\"request_id\":\"submit-2025-09-02-batch-001\",\"status\":\"pending\",\"finalized\":false,\"total_items\":12,\"error\":{\"kind\":\"processing_failed\",\"message\":\"the item could not be converted\"},\"created_at\":\"2024-01-15T09:30:00Z\",\"updated_at\":\"2024-01-15T09:30:00Z\",\"completed_at\":\"2024-01-15T09:30:00Z\",\"expires_at\":\"2024-01-15T09:30:00Z\"}")); + BatchJob response = client.lang2FhirBatch() + .create(CreateBatchRequest.builder() + .requestId("submit-2025-09-02-batch-001") + .build()); + // OAuth: consume the token request + server.takeRequest(); + RecordedRequest request = server.takeRequest(); + Assertions.assertNotNull(request); + Assertions.assertEquals("POST", request.getMethod()); + + // Validate OAuth Authorization header + Assertions.assertEquals( + "Bearer test-token", + request.getHeader("Authorization"), + "OAuth Authorization header should contain Bearer token from OAuth flow"); + // Validate request body + String actualRequestBody = request.getBody().readUtf8(); + String expectedRequestBody = "" + "{\n" + " \"request_id\": \"submit-2025-09-02-batch-001\"\n" + "}"; + JsonNode actualJson = objectMapper.readTree(actualRequestBody); + JsonNode expectedJson = objectMapper.readTree(expectedRequestBody); + Assertions.assertTrue(jsonEquals(expectedJson, actualJson), "Request body structure does not match expected"); + if (actualJson.has("type") || actualJson.has("_type") || actualJson.has("kind")) { + String discriminator = null; + if (actualJson.has("type")) discriminator = actualJson.get("type").asText(); + else if (actualJson.has("_type")) + discriminator = actualJson.get("_type").asText(); + else if (actualJson.has("kind")) + discriminator = actualJson.get("kind").asText(); + Assertions.assertNotNull(discriminator, "Union type should have a discriminator field"); + Assertions.assertFalse(discriminator.isEmpty(), "Union discriminator should not be empty"); + } + + if (!actualJson.isNull()) { + Assertions.assertTrue( + actualJson.isObject() || actualJson.isArray() || actualJson.isValueNode(), + "request should be a valid JSON value"); + } + + if (actualJson.isArray()) { + Assertions.assertTrue(actualJson.size() >= 0, "Array should have valid size"); + } + if (actualJson.isObject()) { + Assertions.assertTrue(actualJson.size() >= 0, "Object should have valid field count"); + } + + // Validate response body + Assertions.assertNotNull(response, "Response should not be null"); + String actualResponseJson = objectMapper.writeValueAsString(response); + String expectedResponseBody = "" + + "{\n" + + " \"job_id\": \"l2f_batch_6f1d2c3a-8b4e-4f5a-9c7d-0e1f2a3b4c5d\",\n" + + " \"request_id\": \"submit-2025-09-02-batch-001\",\n" + + " \"status\": \"pending\",\n" + + " \"finalized\": false,\n" + + " \"total_items\": 12,\n" + + " \"error\": {\n" + + " \"kind\": \"processing_failed\",\n" + + " \"message\": \"the item could not be converted\"\n" + + " },\n" + + " \"created_at\": \"2024-01-15T09:30:00Z\",\n" + + " \"updated_at\": \"2024-01-15T09:30:00Z\",\n" + + " \"completed_at\": \"2024-01-15T09:30:00Z\",\n" + + " \"expires_at\": \"2024-01-15T09:30:00Z\"\n" + + "}"; + JsonNode actualResponseNode = objectMapper.readTree(actualResponseJson); + JsonNode expectedResponseNode = objectMapper.readTree(expectedResponseBody); + Assertions.assertTrue( + jsonEquals(expectedResponseNode, actualResponseNode), + "Response body structure does not match expected"); + if (actualResponseNode.has("type") || actualResponseNode.has("_type") || actualResponseNode.has("kind")) { + String discriminator = null; + if (actualResponseNode.has("type")) + discriminator = actualResponseNode.get("type").asText(); + else if (actualResponseNode.has("_type")) + discriminator = actualResponseNode.get("_type").asText(); + else if (actualResponseNode.has("kind")) + discriminator = actualResponseNode.get("kind").asText(); + Assertions.assertNotNull(discriminator, "Union type should have a discriminator field"); + Assertions.assertFalse(discriminator.isEmpty(), "Union discriminator should not be empty"); + } + + if (!actualResponseNode.isNull()) { + Assertions.assertTrue( + actualResponseNode.isObject() || actualResponseNode.isArray() || actualResponseNode.isValueNode(), + "response should be a valid JSON value"); + } + + if (actualResponseNode.isArray()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Array should have valid size"); + } + if (actualResponseNode.isObject()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Object should have valid field count"); + } + } + + @Test + public void testFinalize() throws Exception { + // OAuth: enqueue token response (client fetches token before API call) + server.enqueue(new MockResponse() + .setResponseCode(200) + .setBody("{\"access_token\":\"test-token\",\"expires_in\":3600}")); + server.enqueue( + new MockResponse() + .setResponseCode(200) + .setBody( + "{\"job_id\":\"l2f_batch_6f1d2c3a-8b4e-4f5a-9c7d-0e1f2a3b4c5d\",\"request_id\":\"submit-2025-09-02-batch-001\",\"status\":\"pending\",\"finalized\":false,\"total_items\":12,\"error\":{\"kind\":\"processing_failed\",\"message\":\"the item could not be converted\"},\"created_at\":\"2024-01-15T09:30:00Z\",\"updated_at\":\"2024-01-15T09:30:00Z\",\"completed_at\":\"2024-01-15T09:30:00Z\",\"expires_at\":\"2024-01-15T09:30:00Z\"}")); + BatchJob response = client.lang2FhirBatch().finalize("job_id"); + // OAuth: consume the token request + server.takeRequest(); + RecordedRequest request = server.takeRequest(); + Assertions.assertNotNull(request); + Assertions.assertEquals("POST", request.getMethod()); + + // Validate OAuth Authorization header + Assertions.assertEquals( + "Bearer test-token", + request.getHeader("Authorization"), + "OAuth Authorization header should contain Bearer token from OAuth flow"); + + // Validate response body + Assertions.assertNotNull(response, "Response should not be null"); + String actualResponseJson = objectMapper.writeValueAsString(response); + String expectedResponseBody = "" + + "{\n" + + " \"job_id\": \"l2f_batch_6f1d2c3a-8b4e-4f5a-9c7d-0e1f2a3b4c5d\",\n" + + " \"request_id\": \"submit-2025-09-02-batch-001\",\n" + + " \"status\": \"pending\",\n" + + " \"finalized\": false,\n" + + " \"total_items\": 12,\n" + + " \"error\": {\n" + + " \"kind\": \"processing_failed\",\n" + + " \"message\": \"the item could not be converted\"\n" + + " },\n" + + " \"created_at\": \"2024-01-15T09:30:00Z\",\n" + + " \"updated_at\": \"2024-01-15T09:30:00Z\",\n" + + " \"completed_at\": \"2024-01-15T09:30:00Z\",\n" + + " \"expires_at\": \"2024-01-15T09:30:00Z\"\n" + + "}"; + JsonNode actualResponseNode = objectMapper.readTree(actualResponseJson); + JsonNode expectedResponseNode = objectMapper.readTree(expectedResponseBody); + Assertions.assertTrue( + jsonEquals(expectedResponseNode, actualResponseNode), + "Response body structure does not match expected"); + if (actualResponseNode.has("type") || actualResponseNode.has("_type") || actualResponseNode.has("kind")) { + String discriminator = null; + if (actualResponseNode.has("type")) + discriminator = actualResponseNode.get("type").asText(); + else if (actualResponseNode.has("_type")) + discriminator = actualResponseNode.get("_type").asText(); + else if (actualResponseNode.has("kind")) + discriminator = actualResponseNode.get("kind").asText(); + Assertions.assertNotNull(discriminator, "Union type should have a discriminator field"); + Assertions.assertFalse(discriminator.isEmpty(), "Union discriminator should not be empty"); + } + + if (!actualResponseNode.isNull()) { + Assertions.assertTrue( + actualResponseNode.isObject() || actualResponseNode.isArray() || actualResponseNode.isValueNode(), + "response should be a valid JSON value"); + } + + if (actualResponseNode.isArray()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Array should have valid size"); + } + if (actualResponseNode.isObject()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Object should have valid field count"); + } + } + + @Test + public void testCancel() throws Exception { + // OAuth: enqueue token response (client fetches token before API call) + server.enqueue(new MockResponse() + .setResponseCode(200) + .setBody("{\"access_token\":\"test-token\",\"expires_in\":3600}")); + server.enqueue( + new MockResponse() + .setResponseCode(200) + .setBody( + "{\"job_id\":\"l2f_batch_6f1d2c3a-8b4e-4f5a-9c7d-0e1f2a3b4c5d\",\"request_id\":\"submit-2025-09-02-batch-001\",\"status\":\"pending\",\"finalized\":false,\"total_items\":12,\"error\":{\"kind\":\"processing_failed\",\"message\":\"the item could not be converted\"},\"created_at\":\"2024-01-15T09:30:00Z\",\"updated_at\":\"2024-01-15T09:30:00Z\",\"completed_at\":\"2024-01-15T09:30:00Z\",\"expires_at\":\"2024-01-15T09:30:00Z\"}")); + BatchJob response = client.lang2FhirBatch().cancel("job_id"); + // OAuth: consume the token request + server.takeRequest(); + RecordedRequest request = server.takeRequest(); + Assertions.assertNotNull(request); + Assertions.assertEquals("POST", request.getMethod()); + + // Validate OAuth Authorization header + Assertions.assertEquals( + "Bearer test-token", + request.getHeader("Authorization"), + "OAuth Authorization header should contain Bearer token from OAuth flow"); + + // Validate response body + Assertions.assertNotNull(response, "Response should not be null"); + String actualResponseJson = objectMapper.writeValueAsString(response); + String expectedResponseBody = "" + + "{\n" + + " \"job_id\": \"l2f_batch_6f1d2c3a-8b4e-4f5a-9c7d-0e1f2a3b4c5d\",\n" + + " \"request_id\": \"submit-2025-09-02-batch-001\",\n" + + " \"status\": \"pending\",\n" + + " \"finalized\": false,\n" + + " \"total_items\": 12,\n" + + " \"error\": {\n" + + " \"kind\": \"processing_failed\",\n" + + " \"message\": \"the item could not be converted\"\n" + + " },\n" + + " \"created_at\": \"2024-01-15T09:30:00Z\",\n" + + " \"updated_at\": \"2024-01-15T09:30:00Z\",\n" + + " \"completed_at\": \"2024-01-15T09:30:00Z\",\n" + + " \"expires_at\": \"2024-01-15T09:30:00Z\"\n" + + "}"; + JsonNode actualResponseNode = objectMapper.readTree(actualResponseJson); + JsonNode expectedResponseNode = objectMapper.readTree(expectedResponseBody); + Assertions.assertTrue( + jsonEquals(expectedResponseNode, actualResponseNode), + "Response body structure does not match expected"); + if (actualResponseNode.has("type") || actualResponseNode.has("_type") || actualResponseNode.has("kind")) { + String discriminator = null; + if (actualResponseNode.has("type")) + discriminator = actualResponseNode.get("type").asText(); + else if (actualResponseNode.has("_type")) + discriminator = actualResponseNode.get("_type").asText(); + else if (actualResponseNode.has("kind")) + discriminator = actualResponseNode.get("kind").asText(); + Assertions.assertNotNull(discriminator, "Union type should have a discriminator field"); + Assertions.assertFalse(discriminator.isEmpty(), "Union discriminator should not be empty"); + } + + if (!actualResponseNode.isNull()) { + Assertions.assertTrue( + actualResponseNode.isObject() || actualResponseNode.isArray() || actualResponseNode.isValueNode(), + "response should be a valid JSON value"); + } + + if (actualResponseNode.isArray()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Array should have valid size"); + } + if (actualResponseNode.isObject()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Object should have valid field count"); + } + } + + @Test + public void testGet() throws Exception { + // OAuth: enqueue token response (client fetches token before API call) + server.enqueue(new MockResponse() + .setResponseCode(200) + .setBody("{\"access_token\":\"test-token\",\"expires_in\":3600}")); + server.enqueue( + new MockResponse() + .setResponseCode(200) + .setBody( + "{\"job_id\":\"l2f_batch_6f1d2c3a-8b4e-4f5a-9c7d-0e1f2a3b4c5d\",\"request_id\":\"submit-2025-09-02-batch-001\",\"status\":\"pending\",\"finalized\":false,\"total_items\":12,\"error\":{\"kind\":\"processing_failed\",\"message\":\"the item could not be converted\"},\"created_at\":\"2024-01-15T09:30:00Z\",\"updated_at\":\"2024-01-15T09:30:00Z\",\"completed_at\":\"2024-01-15T09:30:00Z\",\"expires_at\":\"2024-01-15T09:30:00Z\",\"counts\":{\"total\":12,\"pending\":3,\"processing\":2,\"succeeded\":6,\"failed\":1},\"items\":[{\"item_id\":\"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\",\"id\":\"chart-note-0042\",\"status\":\"pending\",\"attempts\":1,\"detect_retries\":1,\"result_size\":20482,\"error\":{\"kind\":\"processing_failed\",\"message\":\"the item could not be converted\"},\"completed_at\":\"2024-01-15T09:30:00Z\"}],\"next_cursor\":\"next_cursor\",\"has_more\":false}")); + JobDetailResponse response = client.lang2FhirBatch() + .get("job_id", GetRequest.builder().cursor("cursor").limit(1).build()); + // OAuth: consume the token request + server.takeRequest(); + RecordedRequest request = server.takeRequest(); + Assertions.assertNotNull(request); + Assertions.assertEquals("GET", request.getMethod()); + + // Validate OAuth Authorization header + Assertions.assertEquals( + "Bearer test-token", + request.getHeader("Authorization"), + "OAuth Authorization header should contain Bearer token from OAuth flow"); + + // Validate response body + Assertions.assertNotNull(response, "Response should not be null"); + String actualResponseJson = objectMapper.writeValueAsString(response); + String expectedResponseBody = "" + + "{\n" + + " \"job_id\": \"l2f_batch_6f1d2c3a-8b4e-4f5a-9c7d-0e1f2a3b4c5d\",\n" + + " \"request_id\": \"submit-2025-09-02-batch-001\",\n" + + " \"status\": \"pending\",\n" + + " \"finalized\": false,\n" + + " \"total_items\": 12,\n" + + " \"error\": {\n" + + " \"kind\": \"processing_failed\",\n" + + " \"message\": \"the item could not be converted\"\n" + + " },\n" + + " \"created_at\": \"2024-01-15T09:30:00Z\",\n" + + " \"updated_at\": \"2024-01-15T09:30:00Z\",\n" + + " \"completed_at\": \"2024-01-15T09:30:00Z\",\n" + + " \"expires_at\": \"2024-01-15T09:30:00Z\",\n" + + " \"counts\": {\n" + + " \"total\": 12,\n" + + " \"pending\": 3,\n" + + " \"processing\": 2,\n" + + " \"succeeded\": 6,\n" + + " \"failed\": 1\n" + + " },\n" + + " \"items\": [\n" + + " {\n" + + " \"item_id\": \"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\",\n" + + " \"id\": \"chart-note-0042\",\n" + + " \"status\": \"pending\",\n" + + " \"attempts\": 1,\n" + + " \"detect_retries\": 1,\n" + + " \"result_size\": 20482,\n" + + " \"error\": {\n" + + " \"kind\": \"processing_failed\",\n" + + " \"message\": \"the item could not be converted\"\n" + + " },\n" + + " \"completed_at\": \"2024-01-15T09:30:00Z\"\n" + + " }\n" + + " ],\n" + + " \"next_cursor\": \"next_cursor\",\n" + + " \"has_more\": false\n" + + "}"; + JsonNode actualResponseNode = objectMapper.readTree(actualResponseJson); + JsonNode expectedResponseNode = objectMapper.readTree(expectedResponseBody); + Assertions.assertTrue( + jsonEquals(expectedResponseNode, actualResponseNode), + "Response body structure does not match expected"); + if (actualResponseNode.has("type") || actualResponseNode.has("_type") || actualResponseNode.has("kind")) { + String discriminator = null; + if (actualResponseNode.has("type")) + discriminator = actualResponseNode.get("type").asText(); + else if (actualResponseNode.has("_type")) + discriminator = actualResponseNode.get("_type").asText(); + else if (actualResponseNode.has("kind")) + discriminator = actualResponseNode.get("kind").asText(); + Assertions.assertNotNull(discriminator, "Union type should have a discriminator field"); + Assertions.assertFalse(discriminator.isEmpty(), "Union discriminator should not be empty"); + } + + if (!actualResponseNode.isNull()) { + Assertions.assertTrue( + actualResponseNode.isObject() || actualResponseNode.isArray() || actualResponseNode.isValueNode(), + "response should be a valid JSON value"); + } + + if (actualResponseNode.isArray()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Array should have valid size"); + } + if (actualResponseNode.isObject()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Object should have valid field count"); + } + } + + @Test + public void testGetResults() throws Exception { + // OAuth: enqueue token response (client fetches token before API call) + server.enqueue(new MockResponse() + .setResponseCode(200) + .setBody("{\"access_token\":\"test-token\",\"expires_in\":3600}")); + server.enqueue( + new MockResponse() + .setResponseCode(200) + .setBody( + "{\"results\":[{\"item_id\":\"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\",\"id\":\"chart-note-0042\",\"status\":\"pending\",\"attempts\":1,\"detect_retries\":1,\"result_size\":20482,\"error\":{\"kind\":\"processing_failed\",\"message\":\"the item could not be converted\"},\"completed_at\":\"2024-01-15T09:30:00Z\"}],\"next_cursor\":\"next_cursor\",\"has_more\":false}")); + ResultsPageResponse response = client.lang2FhirBatch() + .getResults( + "job_id", + GetResultsRequest.builder().cursor("cursor").limit(1).build()); + // OAuth: consume the token request + server.takeRequest(); + RecordedRequest request = server.takeRequest(); + Assertions.assertNotNull(request); + Assertions.assertEquals("GET", request.getMethod()); + + // Validate OAuth Authorization header + Assertions.assertEquals( + "Bearer test-token", + request.getHeader("Authorization"), + "OAuth Authorization header should contain Bearer token from OAuth flow"); + + // Validate response body + Assertions.assertNotNull(response, "Response should not be null"); + String actualResponseJson = objectMapper.writeValueAsString(response); + String expectedResponseBody = "" + + "{\n" + + " \"results\": [\n" + + " {\n" + + " \"item_id\": \"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\",\n" + + " \"id\": \"chart-note-0042\",\n" + + " \"status\": \"pending\",\n" + + " \"attempts\": 1,\n" + + " \"detect_retries\": 1,\n" + + " \"result_size\": 20482,\n" + + " \"error\": {\n" + + " \"kind\": \"processing_failed\",\n" + + " \"message\": \"the item could not be converted\"\n" + + " },\n" + + " \"completed_at\": \"2024-01-15T09:30:00Z\"\n" + + " }\n" + + " ],\n" + + " \"next_cursor\": \"next_cursor\",\n" + + " \"has_more\": false\n" + + "}"; + JsonNode actualResponseNode = objectMapper.readTree(actualResponseJson); + JsonNode expectedResponseNode = objectMapper.readTree(expectedResponseBody); + Assertions.assertTrue( + jsonEquals(expectedResponseNode, actualResponseNode), + "Response body structure does not match expected"); + if (actualResponseNode.has("type") || actualResponseNode.has("_type") || actualResponseNode.has("kind")) { + String discriminator = null; + if (actualResponseNode.has("type")) + discriminator = actualResponseNode.get("type").asText(); + else if (actualResponseNode.has("_type")) + discriminator = actualResponseNode.get("_type").asText(); + else if (actualResponseNode.has("kind")) + discriminator = actualResponseNode.get("kind").asText(); + Assertions.assertNotNull(discriminator, "Union type should have a discriminator field"); + Assertions.assertFalse(discriminator.isEmpty(), "Union discriminator should not be empty"); + } + + if (!actualResponseNode.isNull()) { + Assertions.assertTrue( + actualResponseNode.isObject() || actualResponseNode.isArray() || actualResponseNode.isValueNode(), + "response should be a valid JSON value"); + } + + if (actualResponseNode.isArray()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Array should have valid size"); + } + if (actualResponseNode.isObject()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Object should have valid field count"); + } + } + + @Test + public void testGetResult() throws Exception { + // OAuth: enqueue token response (client fetches token before API call) + server.enqueue(new MockResponse() + .setResponseCode(200) + .setBody("{\"access_token\":\"test-token\",\"expires_in\":3600}")); + server.enqueue(new MockResponse().setResponseCode(200).setBody("{\"key\":\"value\"}")); + Map response = client.lang2FhirBatch().getResult("job_id", "item_id"); + // OAuth: consume the token request + server.takeRequest(); + RecordedRequest request = server.takeRequest(); + Assertions.assertNotNull(request); + Assertions.assertEquals("GET", request.getMethod()); + + // Validate OAuth Authorization header + Assertions.assertEquals( + "Bearer test-token", + request.getHeader("Authorization"), + "OAuth Authorization header should contain Bearer token from OAuth flow"); + + // Validate response body + Assertions.assertNotNull(response, "Response should not be null"); + String actualResponseJson = objectMapper.writeValueAsString(response); + String expectedResponseBody = "" + "{\n" + " \"key\": \"value\"\n" + "}"; + JsonNode actualResponseNode = objectMapper.readTree(actualResponseJson); + JsonNode expectedResponseNode = objectMapper.readTree(expectedResponseBody); + Assertions.assertTrue( + jsonEquals(expectedResponseNode, actualResponseNode), + "Response body structure does not match expected"); + if (actualResponseNode.has("type") || actualResponseNode.has("_type") || actualResponseNode.has("kind")) { + String discriminator = null; + if (actualResponseNode.has("type")) + discriminator = actualResponseNode.get("type").asText(); + else if (actualResponseNode.has("_type")) + discriminator = actualResponseNode.get("_type").asText(); + else if (actualResponseNode.has("kind")) + discriminator = actualResponseNode.get("kind").asText(); + Assertions.assertNotNull(discriminator, "Union type should have a discriminator field"); + Assertions.assertFalse(discriminator.isEmpty(), "Union discriminator should not be empty"); + } + + if (!actualResponseNode.isNull()) { + Assertions.assertTrue( + actualResponseNode.isObject() || actualResponseNode.isArray() || actualResponseNode.isValueNode(), + "response should be a valid JSON value"); + } + + if (actualResponseNode.isArray()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Array should have valid size"); + } + if (actualResponseNode.isObject()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Object should have valid field count"); + } + } + + /** + * Compares two JsonNodes with numeric equivalence and null safety. + * For objects, checks that all fields in 'expected' exist in 'actual' with matching values. + * Allows 'actual' to have extra fields (e.g., default values added during serialization). + */ + private boolean jsonEquals(JsonNode expected, JsonNode actual) { + if (expected == null && actual == null) return true; + if (expected == null || actual == null) return false; + if (expected.equals(actual)) return true; + if (expected.isNumber() && actual.isNumber()) + return Math.abs(expected.doubleValue() - actual.doubleValue()) < 1e-10; + if (expected.isObject() && actual.isObject()) { + java.util.Iterator> iter = expected.fields(); + while (iter.hasNext()) { + java.util.Map.Entry entry = iter.next(); + JsonNode actualValue = actual.get(entry.getKey()); + if (actualValue == null) { + if (!entry.getValue().isNull()) return false; + } else if (!jsonEquals(entry.getValue(), actualValue)) return false; + } + return true; + } + if (expected.isArray() && actual.isArray()) { + if (expected.size() != actual.size()) return false; + for (int i = 0; i < expected.size(); i++) { + if (!jsonEquals(expected.get(i), actual.get(i))) return false; + } + return true; + } + return false; + } +} diff --git a/src/test/java/com/phenoml/api/Lang2FhirWireTest.java b/src/test/java/com/phenoml/api/Lang2FhirWireTest.java index e7b650b4..647398d5 100644 --- a/src/test/java/com/phenoml/api/Lang2FhirWireTest.java +++ b/src/test/java/com/phenoml/api/Lang2FhirWireTest.java @@ -487,7 +487,7 @@ public void testDocument() throws Exception { .document(DocumentRequest.builder() .version("R4") .resource("questionnaire") - .content("JVBERi0xLjQKJeLjz9MK...(base64-encoded PDF or image bytes)") + .content("JVBERi0xLjQKJeLjz9MK...(base64-encoded document bytes)") .build()); // OAuth: consume the token request server.takeRequest(); @@ -506,7 +506,7 @@ public void testDocument() throws Exception { + "{\n" + " \"version\": \"R4\",\n" + " \"resource\": \"questionnaire\",\n" - + " \"content\": \"JVBERi0xLjQKJeLjz9MK...(base64-encoded PDF or image bytes)\"\n" + + " \"content\": \"JVBERi0xLjQKJeLjz9MK...(base64-encoded document bytes)\"\n" + "}"; JsonNode actualJson = objectMapper.readTree(actualRequestBody); JsonNode expectedJson = objectMapper.readTree(expectedRequestBody); @@ -599,7 +599,7 @@ public void testDocumentMulti() throws Exception { DocumentMultiResponse response = client.lang2Fhir() .documentMulti(DocumentMultiRequest.builder() .version("R4") - .content("JVBERi0xLjQKJeLjz9MK...(base64-encoded PDF or image bytes)") + .content("JVBERi0xLjQKJeLjz9MK...(base64-encoded document bytes)") .provider("medplum") .config(DocumentConfig.builder() .splitClassifications(Optional.of(Arrays.asList( @@ -633,7 +633,7 @@ public void testDocumentMulti() throws Exception { String expectedRequestBody = "" + "{\n" + " \"version\": \"R4\",\n" - + " \"content\": \"JVBERi0xLjQKJeLjz9MK...(base64-encoded PDF or image bytes)\",\n" + + " \"content\": \"JVBERi0xLjQKJeLjz9MK...(base64-encoded document bytes)\",\n" + " \"provider\": \"medplum\",\n" + " \"config\": {\n" + " \"split_classifications\": [\n" diff --git a/src/test/java/com/phenoml/api/ProfilesProfilesWireTest.java b/src/test/java/com/phenoml/api/ProfilesProfilesWireTest.java index bed78883..25290314 100644 --- a/src/test/java/com/phenoml/api/ProfilesProfilesWireTest.java +++ b/src/test/java/com/phenoml/api/ProfilesProfilesWireTest.java @@ -8,6 +8,8 @@ import com.phenoml.api.resources.profiles.types.ProfileListResponse; import com.phenoml.api.resources.profiles.types.ProfileSummary; import com.phenoml.api.resources.profiles.types.ProfileUploadRequest; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -46,7 +48,7 @@ public void testList() throws Exception { new MockResponse() .setResponseCode(200) .setBody( - "{\"profiles\":[{\"id\":\"custom-patient\",\"source\":\"custom\",\"resource_type\":\"Patient\",\"url\":\"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\"version\":\"1.0.0\",\"fhir_version\":\"4.0.1\",\"implementation_guide\":\"acme-cardiology\",\"created_at\":\"2024-01-15T09:30:00Z\",\"updated_at\":\"2024-01-15T09:30:00Z\"}]}")); + "{\"profiles\":[{\"id\":\"custom-patient\",\"source\":\"custom\",\"resource_type\":\"Patient\",\"url\":\"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\"version\":\"1.0.0\",\"status\":\"active\",\"date\":\"2026-08-24\",\"canonical\":\"http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0\",\"fhir_version\":\"4.0.1\",\"implementation_guide\":\"acme-cardiology\",\"created_at\":\"2026-08-24T15:04:05Z\",\"updated_at\":\"2026-08-25T16:04:05Z\"}]}")); ProfileListResponse response = client.profiles() .profiles() .list(ListRequest.builder() @@ -76,10 +78,13 @@ public void testList() throws Exception { + " \"resource_type\": \"Patient\",\n" + " \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n" + " \"version\": \"1.0.0\",\n" + + " \"status\": \"active\",\n" + + " \"date\": \"2026-08-24\",\n" + + " \"canonical\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0\",\n" + " \"fhir_version\": \"4.0.1\",\n" + " \"implementation_guide\": \"acme-cardiology\",\n" - + " \"created_at\": \"2024-01-15T09:30:00Z\",\n" - + " \"updated_at\": \"2024-01-15T09:30:00Z\"\n" + + " \"created_at\": \"2026-08-24T15:04:05Z\",\n" + + " \"updated_at\": \"2026-08-25T16:04:05Z\"\n" + " }\n" + " ]\n" + "}"; @@ -124,15 +129,40 @@ public void testCreate() throws Exception { new MockResponse() .setResponseCode(200) .setBody( - "{\"id\":\"custom-patient\",\"source\":\"custom\",\"resource_type\":\"Patient\",\"url\":\"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\"version\":\"1.0.0\",\"fhir_version\":\"4.0.1\",\"implementation_guide\":\"acme-cardiology\",\"created_at\":\"2024-01-15T09:30:00Z\",\"updated_at\":\"2024-01-15T09:30:00Z\"}")); + "{\"id\":\"custom-patient\",\"source\":\"custom\",\"resource_type\":\"Patient\",\"url\":\"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\"version\":\"1.0.0\",\"status\":\"active\",\"date\":\"2026-08-24\",\"canonical\":\"http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0\",\"fhir_version\":\"4.0.1\",\"implementation_guide\":\"acme-cardiology\",\"created_at\":\"2026-08-24T15:04:05Z\",\"updated_at\":\"2026-08-25T16:04:05Z\"}")); ProfileSummary response = client.profiles() .profiles() .create(ProfileUploadRequest.builder() .structureDefinition(new HashMap() { { - put("key", "value"); + put("resourceType", "StructureDefinition"); + put("id", "custom-patient"); + put("url", "http://phenoml.com/fhir/StructureDefinition/custom-patient"); + put("name", "CustomPatient"); + put("status", "active"); + put("fhirVersion", "4.0.1"); + put("kind", "resource"); + put("abstract", false); + put("type", "Patient"); + put("baseDefinition", "http://hl7.org/fhir/StructureDefinition/Patient"); + put("derivation", "constraint"); + put("snapshot", new HashMap() { + { + put( + "element", + new ArrayList(Arrays.asList(new HashMap() { + { + put("id", "Patient"); + put("path", "Patient"); + put("min", 0); + put("max", "*"); + } + }))); + } + }); } }) + .implementationGuide("acme-cardiology") .build()); // OAuth: consume the token request server.takeRequest(); @@ -147,8 +177,33 @@ public void testCreate() throws Exception { "OAuth Authorization header should contain Bearer token from OAuth flow"); // Validate request body String actualRequestBody = request.getBody().readUtf8(); - String expectedRequestBody = - "" + "{\n" + " \"structure_definition\": {\n" + " \"key\": \"value\"\n" + " }\n" + "}"; + String expectedRequestBody = "" + + "{\n" + + " \"structure_definition\": {\n" + + " \"resourceType\": \"StructureDefinition\",\n" + + " \"id\": \"custom-patient\",\n" + + " \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n" + + " \"name\": \"CustomPatient\",\n" + + " \"status\": \"active\",\n" + + " \"fhirVersion\": \"4.0.1\",\n" + + " \"kind\": \"resource\",\n" + + " \"abstract\": false,\n" + + " \"type\": \"Patient\",\n" + + " \"baseDefinition\": \"http://hl7.org/fhir/StructureDefinition/Patient\",\n" + + " \"derivation\": \"constraint\",\n" + + " \"snapshot\": {\n" + + " \"element\": [\n" + + " {\n" + + " \"id\": \"Patient\",\n" + + " \"path\": \"Patient\",\n" + + " \"min\": 0,\n" + + " \"max\": \"*\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " },\n" + + " \"implementation_guide\": \"acme-cardiology\"\n" + + "}"; JsonNode actualJson = objectMapper.readTree(actualRequestBody); JsonNode expectedJson = objectMapper.readTree(expectedRequestBody); Assertions.assertTrue(jsonEquals(expectedJson, actualJson), "Request body structure does not match expected"); @@ -186,10 +241,13 @@ else if (actualJson.has("kind")) + " \"resource_type\": \"Patient\",\n" + " \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n" + " \"version\": \"1.0.0\",\n" + + " \"status\": \"active\",\n" + + " \"date\": \"2026-08-24\",\n" + + " \"canonical\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0\",\n" + " \"fhir_version\": \"4.0.1\",\n" + " \"implementation_guide\": \"acme-cardiology\",\n" - + " \"created_at\": \"2024-01-15T09:30:00Z\",\n" - + " \"updated_at\": \"2024-01-15T09:30:00Z\"\n" + + " \"created_at\": \"2026-08-24T15:04:05Z\",\n" + + " \"updated_at\": \"2026-08-25T16:04:05Z\"\n" + "}"; JsonNode actualResponseNode = objectMapper.readTree(actualResponseJson); JsonNode expectedResponseNode = objectMapper.readTree(expectedResponseBody); @@ -232,7 +290,7 @@ public void testGet() throws Exception { new MockResponse() .setResponseCode(200) .setBody( - "{\"id\":\"custom-patient\",\"source\":\"custom\",\"resource_type\":\"Patient\",\"url\":\"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\"version\":\"1.0.0\",\"fhir_version\":\"4.0.1\",\"implementation_guide\":\"acme-cardiology\",\"created_at\":\"2024-01-15T09:30:00Z\",\"updated_at\":\"2024-01-15T09:30:00Z\",\"structure_definition\":{\"key\":\"value\"}}")); + "{\"id\":\"custom-patient\",\"source\":\"custom\",\"resource_type\":\"Patient\",\"url\":\"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\"version\":\"1.0.0\",\"status\":\"active\",\"date\":\"2026-08-24\",\"canonical\":\"http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0\",\"fhir_version\":\"4.0.1\",\"implementation_guide\":\"acme-cardiology\",\"created_at\":\"2026-08-24T15:04:05Z\",\"updated_at\":\"2026-08-25T16:04:05Z\",\"structure_definition\":{\"resourceType\":\"StructureDefinition\",\"id\":\"custom-patient\",\"url\":\"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\"version\":\"1.0.0\",\"name\":\"CustomPatient\",\"status\":\"active\",\"fhirVersion\":\"4.0.1\",\"kind\":\"resource\",\"abstract\":false,\"type\":\"Patient\",\"baseDefinition\":\"http://hl7.org/fhir/StructureDefinition/Patient\",\"derivation\":\"constraint\",\"snapshot\":{\"element\":[{\"id\":\"Patient\",\"path\":\"Patient\",\"min\":0,\"max\":\"*\"}]}}}")); ProfileGetResponse response = client.profiles().profiles().get("custom-patient"); // OAuth: consume the token request server.takeRequest(); @@ -256,12 +314,36 @@ public void testGet() throws Exception { + " \"resource_type\": \"Patient\",\n" + " \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n" + " \"version\": \"1.0.0\",\n" + + " \"status\": \"active\",\n" + + " \"date\": \"2026-08-24\",\n" + + " \"canonical\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0\",\n" + " \"fhir_version\": \"4.0.1\",\n" + " \"implementation_guide\": \"acme-cardiology\",\n" - + " \"created_at\": \"2024-01-15T09:30:00Z\",\n" - + " \"updated_at\": \"2024-01-15T09:30:00Z\",\n" + + " \"created_at\": \"2026-08-24T15:04:05Z\",\n" + + " \"updated_at\": \"2026-08-25T16:04:05Z\",\n" + " \"structure_definition\": {\n" - + " \"key\": \"value\"\n" + + " \"resourceType\": \"StructureDefinition\",\n" + + " \"id\": \"custom-patient\",\n" + + " \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n" + + " \"version\": \"1.0.0\",\n" + + " \"name\": \"CustomPatient\",\n" + + " \"status\": \"active\",\n" + + " \"fhirVersion\": \"4.0.1\",\n" + + " \"kind\": \"resource\",\n" + + " \"abstract\": false,\n" + + " \"type\": \"Patient\",\n" + + " \"baseDefinition\": \"http://hl7.org/fhir/StructureDefinition/Patient\",\n" + + " \"derivation\": \"constraint\",\n" + + " \"snapshot\": {\n" + + " \"element\": [\n" + + " {\n" + + " \"id\": \"Patient\",\n" + + " \"path\": \"Patient\",\n" + + " \"min\": 0,\n" + + " \"max\": \"*\"\n" + + " }\n" + + " ]\n" + + " }\n" + " }\n" + "}"; JsonNode actualResponseNode = objectMapper.readTree(actualResponseJson); @@ -305,7 +387,7 @@ public void testUpdate() throws Exception { new MockResponse() .setResponseCode(200) .setBody( - "{\"id\":\"custom-patient\",\"source\":\"custom\",\"resource_type\":\"Patient\",\"url\":\"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\"version\":\"1.0.0\",\"fhir_version\":\"4.0.1\",\"implementation_guide\":\"acme-cardiology\",\"created_at\":\"2024-01-15T09:30:00Z\",\"updated_at\":\"2024-01-15T09:30:00Z\"}")); + "{\"id\":\"custom-patient\",\"source\":\"custom\",\"resource_type\":\"Patient\",\"url\":\"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\"version\":\"1.0.0\",\"status\":\"active\",\"date\":\"2026-08-24\",\"canonical\":\"http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0\",\"fhir_version\":\"4.0.1\",\"implementation_guide\":\"acme-cardiology\",\"created_at\":\"2026-08-24T15:04:05Z\",\"updated_at\":\"2026-08-25T16:04:05Z\"}")); ProfileSummary response = client.profiles() .profiles() .update( @@ -313,9 +395,35 @@ public void testUpdate() throws Exception { ProfileUploadRequest.builder() .structureDefinition(new HashMap() { { - put("key", "value"); + put("resourceType", "StructureDefinition"); + put("id", "custom-patient"); + put("url", "http://phenoml.com/fhir/StructureDefinition/custom-patient"); + put("name", "CustomPatient"); + put("status", "active"); + put("fhirVersion", "4.0.1"); + put("kind", "resource"); + put("abstract", false); + put("type", "Patient"); + put("baseDefinition", "http://hl7.org/fhir/StructureDefinition/Patient"); + put("derivation", "constraint"); + put("snapshot", new HashMap() { + { + put( + "element", + new ArrayList( + Arrays.asList(new HashMap() { + { + put("id", "Patient"); + put("path", "Patient"); + put("min", 0); + put("max", "*"); + } + }))); + } + }); } }) + .implementationGuide("acme-cardiology") .build()); // OAuth: consume the token request server.takeRequest(); @@ -330,8 +438,33 @@ public void testUpdate() throws Exception { "OAuth Authorization header should contain Bearer token from OAuth flow"); // Validate request body String actualRequestBody = request.getBody().readUtf8(); - String expectedRequestBody = - "" + "{\n" + " \"structure_definition\": {\n" + " \"key\": \"value\"\n" + " }\n" + "}"; + String expectedRequestBody = "" + + "{\n" + + " \"structure_definition\": {\n" + + " \"resourceType\": \"StructureDefinition\",\n" + + " \"id\": \"custom-patient\",\n" + + " \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n" + + " \"name\": \"CustomPatient\",\n" + + " \"status\": \"active\",\n" + + " \"fhirVersion\": \"4.0.1\",\n" + + " \"kind\": \"resource\",\n" + + " \"abstract\": false,\n" + + " \"type\": \"Patient\",\n" + + " \"baseDefinition\": \"http://hl7.org/fhir/StructureDefinition/Patient\",\n" + + " \"derivation\": \"constraint\",\n" + + " \"snapshot\": {\n" + + " \"element\": [\n" + + " {\n" + + " \"id\": \"Patient\",\n" + + " \"path\": \"Patient\",\n" + + " \"min\": 0,\n" + + " \"max\": \"*\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " },\n" + + " \"implementation_guide\": \"acme-cardiology\"\n" + + "}"; JsonNode actualJson = objectMapper.readTree(actualRequestBody); JsonNode expectedJson = objectMapper.readTree(expectedRequestBody); Assertions.assertTrue(jsonEquals(expectedJson, actualJson), "Request body structure does not match expected"); @@ -369,10 +502,13 @@ else if (actualJson.has("kind")) + " \"resource_type\": \"Patient\",\n" + " \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n" + " \"version\": \"1.0.0\",\n" + + " \"status\": \"active\",\n" + + " \"date\": \"2026-08-24\",\n" + + " \"canonical\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0\",\n" + " \"fhir_version\": \"4.0.1\",\n" + " \"implementation_guide\": \"acme-cardiology\",\n" - + " \"created_at\": \"2024-01-15T09:30:00Z\",\n" - + " \"updated_at\": \"2024-01-15T09:30:00Z\"\n" + + " \"created_at\": \"2026-08-24T15:04:05Z\",\n" + + " \"updated_at\": \"2026-08-25T16:04:05Z\"\n" + "}"; JsonNode actualResponseNode = objectMapper.readTree(actualResponseJson); JsonNode expectedResponseNode = objectMapper.readTree(expectedResponseBody); diff --git a/src/test/java/com/phenoml/api/ProfilesVersionsWireTest.java b/src/test/java/com/phenoml/api/ProfilesVersionsWireTest.java new file mode 100644 index 00000000..ed84618b --- /dev/null +++ b/src/test/java/com/phenoml/api/ProfilesVersionsWireTest.java @@ -0,0 +1,383 @@ +package com.phenoml.api; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.phenoml.api.core.ObjectMappers; +import com.phenoml.api.resources.profiles.types.ProfileGetResponse; +import com.phenoml.api.resources.profiles.types.ProfileSummary; +import com.phenoml.api.resources.profiles.types.ProfileVersionListResponse; +import java.util.HashMap; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class ProfilesVersionsWireTest { + private MockWebServer server; + private PhenomlClient client; + private ObjectMapper objectMapper = ObjectMappers.JSON_MAPPER; + + @BeforeEach + public void setup() throws Exception { + server = new MockWebServer(); + server.start(); + client = PhenomlClient.withCredentials("test-client-id", "test-client-secret") + .url(server.url("/").toString()) + .build(); + } + + @AfterEach + public void teardown() throws Exception { + server.shutdown(); + } + + @Test + public void testList() throws Exception { + // OAuth: enqueue token response (client fetches token before API call) + server.enqueue(new MockResponse() + .setResponseCode(200) + .setBody("{\"access_token\":\"test-token\",\"expires_in\":3600}")); + server.enqueue( + new MockResponse() + .setResponseCode(200) + .setBody( + "{\"versions\":[{\"id\":\"custom-patient\",\"source\":\"custom\",\"resource_type\":\"Patient\",\"url\":\"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\"version\":\"2.0.0\",\"status\":\"active\",\"date\":\"2026-08-26\",\"canonical\":\"http://phenoml.com/fhir/StructureDefinition/custom-patient|2.0.0\",\"fhir_version\":\"4.0.1\",\"implementation_guide\":\"acme-cardiology\",\"created_at\":\"2026-08-26T15:04:05Z\",\"updated_at\":\"2026-08-26T15:04:05Z\"},{\"id\":\"custom-patient\",\"source\":\"custom\",\"resource_type\":\"Patient\",\"url\":\"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\"version\":\"1.0.0\",\"status\":\"active\",\"date\":\"2026-08-24\",\"canonical\":\"http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0\",\"fhir_version\":\"4.0.1\",\"implementation_guide\":\"acme-cardiology\",\"created_at\":\"2026-08-24T15:04:05Z\",\"updated_at\":\"2026-08-24T15:04:05Z\"}]}")); + ProfileVersionListResponse response = client.profiles().versions().list("custom-patient"); + // OAuth: consume the token request + server.takeRequest(); + RecordedRequest request = server.takeRequest(); + Assertions.assertNotNull(request); + Assertions.assertEquals("GET", request.getMethod()); + + // Validate OAuth Authorization header + Assertions.assertEquals( + "Bearer test-token", + request.getHeader("Authorization"), + "OAuth Authorization header should contain Bearer token from OAuth flow"); + + // Validate response body + Assertions.assertNotNull(response, "Response should not be null"); + String actualResponseJson = objectMapper.writeValueAsString(response); + String expectedResponseBody = "" + + "{\n" + + " \"versions\": [\n" + + " {\n" + + " \"id\": \"custom-patient\",\n" + + " \"source\": \"custom\",\n" + + " \"resource_type\": \"Patient\",\n" + + " \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n" + + " \"version\": \"2.0.0\",\n" + + " \"status\": \"active\",\n" + + " \"date\": \"2026-08-26\",\n" + + " \"canonical\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient|2.0.0\",\n" + + " \"fhir_version\": \"4.0.1\",\n" + + " \"implementation_guide\": \"acme-cardiology\",\n" + + " \"created_at\": \"2026-08-26T15:04:05Z\",\n" + + " \"updated_at\": \"2026-08-26T15:04:05Z\"\n" + + " },\n" + + " {\n" + + " \"id\": \"custom-patient\",\n" + + " \"source\": \"custom\",\n" + + " \"resource_type\": \"Patient\",\n" + + " \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n" + + " \"version\": \"1.0.0\",\n" + + " \"status\": \"active\",\n" + + " \"date\": \"2026-08-24\",\n" + + " \"canonical\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0\",\n" + + " \"fhir_version\": \"4.0.1\",\n" + + " \"implementation_guide\": \"acme-cardiology\",\n" + + " \"created_at\": \"2026-08-24T15:04:05Z\",\n" + + " \"updated_at\": \"2026-08-24T15:04:05Z\"\n" + + " }\n" + + " ]\n" + + "}"; + JsonNode actualResponseNode = objectMapper.readTree(actualResponseJson); + JsonNode expectedResponseNode = objectMapper.readTree(expectedResponseBody); + Assertions.assertTrue( + jsonEquals(expectedResponseNode, actualResponseNode), + "Response body structure does not match expected"); + if (actualResponseNode.has("type") || actualResponseNode.has("_type") || actualResponseNode.has("kind")) { + String discriminator = null; + if (actualResponseNode.has("type")) + discriminator = actualResponseNode.get("type").asText(); + else if (actualResponseNode.has("_type")) + discriminator = actualResponseNode.get("_type").asText(); + else if (actualResponseNode.has("kind")) + discriminator = actualResponseNode.get("kind").asText(); + Assertions.assertNotNull(discriminator, "Union type should have a discriminator field"); + Assertions.assertFalse(discriminator.isEmpty(), "Union discriminator should not be empty"); + } + + if (!actualResponseNode.isNull()) { + Assertions.assertTrue( + actualResponseNode.isObject() || actualResponseNode.isArray() || actualResponseNode.isValueNode(), + "response should be a valid JSON value"); + } + + if (actualResponseNode.isArray()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Array should have valid size"); + } + if (actualResponseNode.isObject()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Object should have valid field count"); + } + } + + @Test + public void testCreate() throws Exception { + // OAuth: enqueue token response (client fetches token before API call) + server.enqueue(new MockResponse() + .setResponseCode(200) + .setBody("{\"access_token\":\"test-token\",\"expires_in\":3600}")); + server.enqueue( + new MockResponse() + .setResponseCode(200) + .setBody( + "{\"id\":\"custom-patient\",\"source\":\"custom\",\"resource_type\":\"Patient\",\"url\":\"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\"version\":\"1.0.0\",\"status\":\"active\",\"date\":\"2026-08-24\",\"canonical\":\"http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0\",\"fhir_version\":\"4.0.1\",\"implementation_guide\":\"acme-cardiology\",\"created_at\":\"2026-08-24T15:04:05Z\",\"updated_at\":\"2026-08-25T16:04:05Z\"}")); + ProfileSummary response = client.profiles().versions().create("custom-patient", new HashMap() { + { + put("key", "value"); + } + }); + // OAuth: consume the token request + server.takeRequest(); + RecordedRequest request = server.takeRequest(); + Assertions.assertNotNull(request); + Assertions.assertEquals("POST", request.getMethod()); + + // Validate OAuth Authorization header + Assertions.assertEquals( + "Bearer test-token", + request.getHeader("Authorization"), + "OAuth Authorization header should contain Bearer token from OAuth flow"); + // Validate request body + String actualRequestBody = request.getBody().readUtf8(); + String expectedRequestBody = "" + "{\n" + " \"key\": \"value\"\n" + "}"; + JsonNode actualJson = objectMapper.readTree(actualRequestBody); + JsonNode expectedJson = objectMapper.readTree(expectedRequestBody); + Assertions.assertTrue(jsonEquals(expectedJson, actualJson), "Request body structure does not match expected"); + if (actualJson.has("type") || actualJson.has("_type") || actualJson.has("kind")) { + String discriminator = null; + if (actualJson.has("type")) discriminator = actualJson.get("type").asText(); + else if (actualJson.has("_type")) + discriminator = actualJson.get("_type").asText(); + else if (actualJson.has("kind")) + discriminator = actualJson.get("kind").asText(); + Assertions.assertNotNull(discriminator, "Union type should have a discriminator field"); + Assertions.assertFalse(discriminator.isEmpty(), "Union discriminator should not be empty"); + } + + if (!actualJson.isNull()) { + Assertions.assertTrue( + actualJson.isObject() || actualJson.isArray() || actualJson.isValueNode(), + "request should be a valid JSON value"); + } + + if (actualJson.isArray()) { + Assertions.assertTrue(actualJson.size() >= 0, "Array should have valid size"); + } + if (actualJson.isObject()) { + Assertions.assertTrue(actualJson.size() >= 0, "Object should have valid field count"); + } + + // Validate response body + Assertions.assertNotNull(response, "Response should not be null"); + String actualResponseJson = objectMapper.writeValueAsString(response); + String expectedResponseBody = "" + + "{\n" + + " \"id\": \"custom-patient\",\n" + + " \"source\": \"custom\",\n" + + " \"resource_type\": \"Patient\",\n" + + " \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n" + + " \"version\": \"1.0.0\",\n" + + " \"status\": \"active\",\n" + + " \"date\": \"2026-08-24\",\n" + + " \"canonical\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0\",\n" + + " \"fhir_version\": \"4.0.1\",\n" + + " \"implementation_guide\": \"acme-cardiology\",\n" + + " \"created_at\": \"2026-08-24T15:04:05Z\",\n" + + " \"updated_at\": \"2026-08-25T16:04:05Z\"\n" + + "}"; + JsonNode actualResponseNode = objectMapper.readTree(actualResponseJson); + JsonNode expectedResponseNode = objectMapper.readTree(expectedResponseBody); + Assertions.assertTrue( + jsonEquals(expectedResponseNode, actualResponseNode), + "Response body structure does not match expected"); + if (actualResponseNode.has("type") || actualResponseNode.has("_type") || actualResponseNode.has("kind")) { + String discriminator = null; + if (actualResponseNode.has("type")) + discriminator = actualResponseNode.get("type").asText(); + else if (actualResponseNode.has("_type")) + discriminator = actualResponseNode.get("_type").asText(); + else if (actualResponseNode.has("kind")) + discriminator = actualResponseNode.get("kind").asText(); + Assertions.assertNotNull(discriminator, "Union type should have a discriminator field"); + Assertions.assertFalse(discriminator.isEmpty(), "Union discriminator should not be empty"); + } + + if (!actualResponseNode.isNull()) { + Assertions.assertTrue( + actualResponseNode.isObject() || actualResponseNode.isArray() || actualResponseNode.isValueNode(), + "response should be a valid JSON value"); + } + + if (actualResponseNode.isArray()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Array should have valid size"); + } + if (actualResponseNode.isObject()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Object should have valid field count"); + } + } + + @Test + public void testGet() throws Exception { + // OAuth: enqueue token response (client fetches token before API call) + server.enqueue(new MockResponse() + .setResponseCode(200) + .setBody("{\"access_token\":\"test-token\",\"expires_in\":3600}")); + server.enqueue( + new MockResponse() + .setResponseCode(200) + .setBody( + "{\"id\":\"custom-patient\",\"source\":\"custom\",\"resource_type\":\"Patient\",\"url\":\"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\"version\":\"1.0.0\",\"status\":\"active\",\"date\":\"2026-08-24\",\"canonical\":\"http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0\",\"fhir_version\":\"4.0.1\",\"implementation_guide\":\"acme-cardiology\",\"created_at\":\"2026-08-24T15:04:05Z\",\"updated_at\":\"2026-08-25T16:04:05Z\",\"structure_definition\":{\"resourceType\":\"StructureDefinition\",\"id\":\"custom-patient\",\"url\":\"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\"version\":\"1.0.0\",\"name\":\"CustomPatient\",\"status\":\"active\",\"fhirVersion\":\"4.0.1\",\"kind\":\"resource\",\"abstract\":false,\"type\":\"Patient\",\"baseDefinition\":\"http://hl7.org/fhir/StructureDefinition/Patient\",\"derivation\":\"constraint\",\"snapshot\":{\"element\":[{\"id\":\"Patient\",\"path\":\"Patient\",\"min\":0,\"max\":\"*\"}]}}}")); + ProfileGetResponse response = client.profiles().versions().get("custom-patient", "2.0.0"); + // OAuth: consume the token request + server.takeRequest(); + RecordedRequest request = server.takeRequest(); + Assertions.assertNotNull(request); + Assertions.assertEquals("GET", request.getMethod()); + + // Validate OAuth Authorization header + Assertions.assertEquals( + "Bearer test-token", + request.getHeader("Authorization"), + "OAuth Authorization header should contain Bearer token from OAuth flow"); + + // Validate response body + Assertions.assertNotNull(response, "Response should not be null"); + String actualResponseJson = objectMapper.writeValueAsString(response); + String expectedResponseBody = "" + + "{\n" + + " \"id\": \"custom-patient\",\n" + + " \"source\": \"custom\",\n" + + " \"resource_type\": \"Patient\",\n" + + " \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n" + + " \"version\": \"1.0.0\",\n" + + " \"status\": \"active\",\n" + + " \"date\": \"2026-08-24\",\n" + + " \"canonical\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0\",\n" + + " \"fhir_version\": \"4.0.1\",\n" + + " \"implementation_guide\": \"acme-cardiology\",\n" + + " \"created_at\": \"2026-08-24T15:04:05Z\",\n" + + " \"updated_at\": \"2026-08-25T16:04:05Z\",\n" + + " \"structure_definition\": {\n" + + " \"resourceType\": \"StructureDefinition\",\n" + + " \"id\": \"custom-patient\",\n" + + " \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n" + + " \"version\": \"1.0.0\",\n" + + " \"name\": \"CustomPatient\",\n" + + " \"status\": \"active\",\n" + + " \"fhirVersion\": \"4.0.1\",\n" + + " \"kind\": \"resource\",\n" + + " \"abstract\": false,\n" + + " \"type\": \"Patient\",\n" + + " \"baseDefinition\": \"http://hl7.org/fhir/StructureDefinition/Patient\",\n" + + " \"derivation\": \"constraint\",\n" + + " \"snapshot\": {\n" + + " \"element\": [\n" + + " {\n" + + " \"id\": \"Patient\",\n" + + " \"path\": \"Patient\",\n" + + " \"min\": 0,\n" + + " \"max\": \"*\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " }\n" + + "}"; + JsonNode actualResponseNode = objectMapper.readTree(actualResponseJson); + JsonNode expectedResponseNode = objectMapper.readTree(expectedResponseBody); + Assertions.assertTrue( + jsonEquals(expectedResponseNode, actualResponseNode), + "Response body structure does not match expected"); + if (actualResponseNode.has("type") || actualResponseNode.has("_type") || actualResponseNode.has("kind")) { + String discriminator = null; + if (actualResponseNode.has("type")) + discriminator = actualResponseNode.get("type").asText(); + else if (actualResponseNode.has("_type")) + discriminator = actualResponseNode.get("_type").asText(); + else if (actualResponseNode.has("kind")) + discriminator = actualResponseNode.get("kind").asText(); + Assertions.assertNotNull(discriminator, "Union type should have a discriminator field"); + Assertions.assertFalse(discriminator.isEmpty(), "Union discriminator should not be empty"); + } + + if (!actualResponseNode.isNull()) { + Assertions.assertTrue( + actualResponseNode.isObject() || actualResponseNode.isArray() || actualResponseNode.isValueNode(), + "response should be a valid JSON value"); + } + + if (actualResponseNode.isArray()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Array should have valid size"); + } + if (actualResponseNode.isObject()) { + Assertions.assertTrue(actualResponseNode.size() >= 0, "Object should have valid field count"); + } + } + + @Test + public void testDelete() throws Exception { + // OAuth: enqueue token response (client fetches token before API call) + server.enqueue(new MockResponse() + .setResponseCode(200) + .setBody("{\"access_token\":\"test-token\",\"expires_in\":3600}")); + server.enqueue(new MockResponse().setResponseCode(200).setBody("{}")); + client.profiles().versions().delete("custom-patient", "2.0.0"); + // OAuth: consume the token request + server.takeRequest(); + RecordedRequest request = server.takeRequest(); + Assertions.assertNotNull(request); + Assertions.assertEquals("DELETE", request.getMethod()); + + // Validate OAuth Authorization header + Assertions.assertEquals( + "Bearer test-token", + request.getHeader("Authorization"), + "OAuth Authorization header should contain Bearer token from OAuth flow"); + } + + /** + * Compares two JsonNodes with numeric equivalence and null safety. + * For objects, checks that all fields in 'expected' exist in 'actual' with matching values. + * Allows 'actual' to have extra fields (e.g., default values added during serialization). + */ + private boolean jsonEquals(JsonNode expected, JsonNode actual) { + if (expected == null && actual == null) return true; + if (expected == null || actual == null) return false; + if (expected.equals(actual)) return true; + if (expected.isNumber() && actual.isNumber()) + return Math.abs(expected.doubleValue() - actual.doubleValue()) < 1e-10; + if (expected.isObject() && actual.isObject()) { + java.util.Iterator> iter = expected.fields(); + while (iter.hasNext()) { + java.util.Map.Entry entry = iter.next(); + JsonNode actualValue = actual.get(entry.getKey()); + if (actualValue == null) { + if (!entry.getValue().isNull()) return false; + } else if (!jsonEquals(entry.getValue(), actualValue)) return false; + } + return true; + } + if (expected.isArray() && actual.isArray()) { + if (expected.size() != actual.size()) return false; + for (int i = 0; i < expected.size(); i++) { + if (!jsonEquals(expected.get(i), actual.get(i))) return false; + } + return true; + } + return false; + } +} diff --git a/src/test/resources/wire-tests/Lang2FhirWireTest_testCreateMulti_response.json b/src/test/resources/wire-tests/Lang2FhirWireTest_testCreateMulti_response.json index 8e2677a0..e9aeee6f 100644 --- a/src/test/resources/wire-tests/Lang2FhirWireTest_testCreateMulti_response.json +++ b/src/test/resources/wire-tests/Lang2FhirWireTest_testCreateMulti_response.json @@ -82,6 +82,22 @@ "fieldPath": "onsetDateTime", "value": "2024-01-15", "supported": false, + "unaudited": true, + "rationale": "Date is the visit date, not when the condition began." + } + ] + } + ], + "remediated": [ + { + "tempId": "urn:uuid:a842c4bc-f6cb-4555-9741-ac3aec4ef0b8", + "resourceType": "Condition", + "findings": [ + { + "fieldPath": "onsetDateTime", + "value": "2024-01-15", + "supported": false, + "unaudited": true, "rationale": "Date is the visit date, not when the condition began." } ] diff --git a/src/test/resources/wire-tests/Lang2FhirWireTest_testDocumentMulti_response.json b/src/test/resources/wire-tests/Lang2FhirWireTest_testDocumentMulti_response.json index 48cc6209..bf0aa763 100644 --- a/src/test/resources/wire-tests/Lang2FhirWireTest_testDocumentMulti_response.json +++ b/src/test/resources/wire-tests/Lang2FhirWireTest_testDocumentMulti_response.json @@ -133,6 +133,22 @@ "fieldPath": "onsetDateTime", "value": "2024-01-15", "supported": false, + "unaudited": true, + "rationale": "Date is the visit date, not when the condition began." + } + ] + } + ], + "remediated": [ + { + "tempId": "urn:uuid:a842c4bc-f6cb-4555-9741-ac3aec4ef0b8", + "resourceType": "Condition", + "findings": [ + { + "fieldPath": "onsetDateTime", + "value": "2024-01-15", + "supported": false, + "unaudited": true, "rationale": "Date is the visit date, not when the condition began." } ]