Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for the format parameter #1299

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from

Conversation

ranim-n
Copy link
Contributor

@ranim-n ranim-n commented Dec 24, 2024

No description provided.

@ranim-n ranim-n added feature New feature or request common Relates to common behaviors ngsild-1.8.1 labels Dec 24, 2024
@ranim-n ranim-n self-assigned this Dec 24, 2024
@ranim-n ranim-n linked an issue Dec 24, 2024 that may be closed by this pull request
Copy link
Contributor

github-actions bot commented Dec 24, 2024

Test Results

   71 files  + 1     71 suites  +1   1m 25s ⏱️ -2s
1 152 tests +14  1 152 ✅ +14  0 💤 ±0  0 ❌ ±0 
1 191 runs  +14  1 191 ✅ +14  0 💤 ±0  0 ❌ ±0 

Results for commit 1de5901. ± Comparison against base commit b9bca70.

This pull request removes 190 and adds 50 tests. Note that renamed tests count towards both.
                           …, withTemporalValues=true, withAudit=false, expectation={
                      "@id": "https://uri…
                      "@type": "@json",
                      …
                    "@value": "/A/B"
                    "@value": "/C/D"
                    "@value": 20
                    "…
                    {
                  "@type": "https://uri.etsi.org/ngsi-ld/DateTime",
…
com.egm.stellio.search.entity.util.PatchAttributeTests ‑ [1] source={
    "attribute": {
        "type": "Property",
        "value": 12.0,
        "observedAt": "2024-04-14T12:34:56Z"
    }
}, target={
    "attribute": {
        "type": "Property",
        "value": 12.2,
        "unitCode": "GRM"
    }
}, expected={
    "attribute": {
        "type": "Property",
        "value": 12.2,
        "unitCode": "GRM",
        "observedAt": "2024-04-14T12:34:56Z"
    }
}
com.egm.stellio.search.entity.util.PatchAttributeTests ‑ [1] source={
    "attribute": {
        "type": "Property",
        "value": 12.0,
        "observedAt": "2024-04-14T12:34:56Z",
        "subAttribute": {
            "type": "Property",
            "value": "subAttribute"
        }
    }
}, target={
    "attribute": {
        "type": "Property",
        "value": 12.2,
        "unitCode": "GRM",
        "subAttribute": {
            "type": "Property",
            "value": "newSubAttributeValue"
        }
    }
}, expected={
    "attribute": {
        "type": "Property",
        "value": 12.2,
        "unitCode": "GRM",
        "observedAt": "2024-04-14T12:34:56Z",
        "subAttribute": {
            "type": "Property",
            "value": "newSubAttributeValue"
        }
    }
}
com.egm.stellio.search.entity.util.PatchAttributeTests ‑ [2] source={
    "attribute": {
        "type": "Property",
        "value": { "en": "car", "fr": "voiture" }
    }
}, target={
    "attribute": {
        "type": "Property",
        "value": { "fr": "vélo", "es": "bicicleta" }
    }
}, expected={
    "attribute": {
        "type": "Property",
        "value": { "en": "car", "fr": "vélo", "es": "bicicleta" }
    }
}
com.egm.stellio.search.entity.util.PatchAttributeTests ‑ [2] source={
    "incoming": {
        "type": "JsonProperty",
        "json": { "id": 1, "b": null, "c": 12.4 },
        "observedAt": "2022-12-24T14:01:22.066Z",
        "subAttribute": {
            "type": "Property",
            "value": "subAttribute"
        }
    }
}, target={
    "incoming": {
        "type": "JsonProperty",
        "json": { "id": 2, "b": "something" },
        "observedAt": "2023-12-24T14:01:22.066Z"
    }
}, expected={
    "incoming": {
        "type": "JsonProperty",
        "json": { "id": 2, "b": "something" },
        "observedAt": "2023-12-24T14:01:22.066Z",
        "subAttribute": {
            "type": "Property",
            "value": "subAttribute"
        }
    }
}
com.egm.stellio.search.entity.util.PatchAttributeTests ‑ [3] source={
    "attribute": {
        "type": "Property",
        "value": [ "car", "voiture" ]
    }
}, target={
    "attribute": {
        "type": "Property",
        "value": [ "vélo", "bicicleta" ]
    }
}, expected={
    "attribute": {
        "type": "Property",
        "value": [ "vélo", "bicicleta" ]
    }
}
com.egm.stellio.search.entity.util.PatchAttributeTests ‑ [3] source={
    "attribute": {
        "type": "VocabProperty",
        "vocab": "stellio"
    }
}, target={
    "attribute": {
        "type": "VocabProperty",
        "vocab": "egm"
    }
}, expected={
    "attribute": {
        "type": "VocabProperty",
        "vocab": "egm"
    }
}
com.egm.stellio.search.entity.util.PatchAttributeTests ‑ [4] source={
    "attribute": {
        "type": "Relationship",
        "object": "urn:ngsi-ld:Entity:01"
    }
}, target={
    "attribute": {
        "type": "Relationship",
        "object": "urn:ngsi-ld:Entity:02"
    }
}, expected={
    "attribute": {
        "type": "Relationship",
        "object": "urn:ngsi-ld:Entity:02"
    }
}
com.egm.stellio.search.entity.util.PatchAttributeTests ‑ [5] source={
    "attribute": {
        "type": "LanguageProperty",
        "languageMap": { "en": "train", "fr": "train" }
    }
}, target={
    "attribute": {
        "type": "LanguageProperty",
        "languageMap": { "fr": "TGV", "es": "tren" }
    }
}, expected={
    "attribute": {
        "type": "LanguageProperty",
        "languageMap": { "en": "train", "fr": "TGV", "es": "tren" }
    }
}
com.egm.stellio.search.entity.util.PatchAttributeTests ‑ [6] source={
    "incoming": {
        "type": "JsonProperty",
        "json": { "a": 1, "b": "thing" }
    }
}, target={
    "incoming": {
        "type": "JsonProperty",
        "json": { "a": 2, "c": "other thing" }
    }
}, expected={
    "incoming": {
        "type": "JsonProperty",
        "json": { "a": 2, "b": "thing", "c": "other thing" }
    }
}
com.egm.stellio.search.entity.util.PatchAttributeTests ‑ [7] source={
    "attribute": {
        "type": "VocabProperty",
        "vocab": "stellio"
    }
}, target={
    "attribute": {
        "type": "VocabProperty",
        "vocab": "egm"
    }
}, expected={
    "attribute": {
        "type": "VocabProperty",
        "vocab": "egm"
    }
}
…

♻️ This comment has been updated with latest results.

@@ -45,18 +45,22 @@ fun composeTemporalEntitiesQueryFromGet(

if (inQueryEntities)
entitiesQueryFromGet.validateMinimalQueryEntitiesParameters().bind()
val optionsParam = Optional.ofNullable(requestParams.getFirst(QueryParameter.OPTIONS.key))
val formatParam = Optional.ofNullable(requestParams.getFirst(QueryParameter.FORMAT.key))
val withTemporalValues = when {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could take this opportunity to return an APIException if both temporalValues and aggregatedValues are provided

@bobeal
Copy link
Member

bobeal commented Jan 4, 2025

A note about the behavior of the MultiValueMap holding the query parameters:

  • if you pass such a query string to the endpoint: ?type=Beehive&options=sysAttrs,audit&options=keyValues
  • the map of the query parameters received in the handler will be this one:
image

In other words:

  • it takes care of "accumulating" the values of query parameters with the same name
  • it does not do the split of a query parameter which has a comma separated list of values

So this has to be handled in the code.

@@ -206,3 +190,30 @@ fun buildTimerelAndTime(
} else {
"'timerel' and 'time' must be used in conjunction".left()
}

fun extractTemporalRepresentation(requestParams: MultiValueMap<String, String>):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it can be a private function?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we now call it queryParams, and no longer requestParams

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still the 1st point

@ranim-n ranim-n linked an issue Jan 7, 2025 that may be closed by this pull request
@ranim-n ranim-n marked this pull request as ready for review January 8, 2025 10:13
@bobeal bobeal requested a review from thomasBousselin January 8, 2025 17:37
@bobeal
Copy link
Member

bobeal commented Jan 8, 2025

don't know why you merged develop in your branch (we never do that, we rebase on develop), but it is a bit a mess. there is at least one commit from develop which is included in the branch...

@@ -199,7 +200,8 @@ class AttributeInstanceServiceTests : WithTimescaleContainer, WithKafkaContainer
buildDefaultTestTemporalQuery(
timerel = Timerel.AFTER,
timeAt = now.minusHours(1)
)
),
TemporalRepresentation.NORMALIZED
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this parameter is useless if equals to NORMALIZED (it is the default in gimmeTemporalEntitiesQuery). here and in every other calls below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was fixed but the merge must've removed it (same thing for the private function)

companion object {
fun fromString(key: String): Either<APIException, OptionsValue> = either {
OptionsValue.entries.find { it.value == key }
?: return InvalidRequestException("'$key' is not a valid options value").left()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
?: return InvalidRequestException("'$key' is not a valid options value").left()
?: return InvalidRequestException("'$key' is not a valid value for the options query parameter").left()


fun hasValueInOptionsParam(options: Optional<String>, optionValue: OptionsParamValue): Boolean =
options
fun hasValueInOptionsParam(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fun hasValueInOptionsParam(
    optionsParam: Optional<String>,
    optionsValue: OptionsValue
): Either<APIException, Boolean> = either {
    optionsParam
        .map { it.split(",") }
        .orElse(emptyList())
        .map { OptionsValue.fromString(it).bind() }
        .any { it == optionsValue }
}

@@ -206,3 +190,30 @@ fun buildTimerelAndTime(
} else {
"'timerel' and 'time' must be used in conjunction".left()
}

fun extractTemporalRepresentation(requestParams: MultiValueMap<String, String>):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still the 1st point

Comment on lines 201 to 212
if (!optionsParam.isEmpty) {
val hasTemporal = hasValueInOptionsParam(optionsParam, OptionsValue.TEMPORAL_VALUES).bind()
val hasAggregated = hasValueInOptionsParam(optionsParam, OptionsValue.AGGREGATED_VALUES).bind()
when {
hasTemporal && hasAggregated ->
return BadRequestDataException("Only one temporal representation can be present").left()
hasTemporal -> return TemporalRepresentation.TEMPORAL_VALUES.right()
hasAggregated -> return TemporalRepresentation.AGGREGATED_VALUES.right()
else -> return TemporalRepresentation.NORMALIZED.right()
}
} else
return TemporalRepresentation.NORMALIZED.right()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!optionsParam.isEmpty) {
val hasTemporal = hasValueInOptionsParam(optionsParam, OptionsValue.TEMPORAL_VALUES).bind()
val hasAggregated = hasValueInOptionsParam(optionsParam, OptionsValue.AGGREGATED_VALUES).bind()
when {
hasTemporal && hasAggregated ->
return BadRequestDataException("Only one temporal representation can be present").left()
hasTemporal -> return TemporalRepresentation.TEMPORAL_VALUES.right()
hasAggregated -> return TemporalRepresentation.AGGREGATED_VALUES.right()
else -> return TemporalRepresentation.NORMALIZED.right()
}
} else
return TemporalRepresentation.NORMALIZED.right()
return if (!optionsParam.isEmpty) {
val hasTemporal = hasValueInOptionsParam(optionsParam, OptionsValue.TEMPORAL_VALUES).bind()
val hasAggregated = hasValueInOptionsParam(optionsParam, OptionsValue.AGGREGATED_VALUES).bind()
when {
hasTemporal && hasAggregated ->
BadRequestDataException(
"Found different temporal representations in options query parameter, only one can be provided"
).left()
hasTemporal -> TemporalRepresentation.TEMPORAL_VALUES.right()
hasAggregated -> TemporalRepresentation.AGGREGATED_VALUES.right()
else -> TemporalRepresentation.NORMALIZED.right()
}
} else
TemporalRepresentation.NORMALIZED.right()

Comment on lines 36 to 53
.flatMap { it.split(",") }
val formatParam = queryParams.getFirst(QueryParameter.FORMAT.key)
if (formatParam != null && FormatValue.fromString(formatParam) == null) {
return InvalidRequestException("'$formatParam' is not a valid format value").left()
}

optionsParam.forEach { option ->
OptionsValue.fromString(option).bind()
}
val attributeRepresentation = when {
formatParam == FormatValue.KEY_VALUES.value ||
formatParam == FormatValue.SIMPLIFIED.value -> AttributeRepresentation.SIMPLIFIED
formatParam == FormatValue.NORMALIZED.value -> AttributeRepresentation.NORMALIZED
optionsParam.contains(FormatValue.KEY_VALUES.value) ||
optionsParam.contains(FormatValue.SIMPLIFIED.value) -> AttributeRepresentation.SIMPLIFIED
else -> AttributeRepresentation.NORMALIZED
}
val includeSysAttrs = optionsParam.contains(OptionsValue.SYS_ATTRS.value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.flatMap { it.split(",") }
val formatParam = queryParams.getFirst(QueryParameter.FORMAT.key)
if (formatParam != null && FormatValue.fromString(formatParam) == null) {
return InvalidRequestException("'$formatParam' is not a valid format value").left()
}
optionsParam.forEach { option ->
OptionsValue.fromString(option).bind()
}
val attributeRepresentation = when {
formatParam == FormatValue.KEY_VALUES.value ||
formatParam == FormatValue.SIMPLIFIED.value -> AttributeRepresentation.SIMPLIFIED
formatParam == FormatValue.NORMALIZED.value -> AttributeRepresentation.NORMALIZED
optionsParam.contains(FormatValue.KEY_VALUES.value) ||
optionsParam.contains(FormatValue.SIMPLIFIED.value) -> AttributeRepresentation.SIMPLIFIED
else -> AttributeRepresentation.NORMALIZED
}
val includeSysAttrs = optionsParam.contains(OptionsValue.SYS_ATTRS.value)
.flatMap { it.split(",") }
.map { OptionsValue.fromString(it).bind() }
val formatParam = queryParams.getFirst(QueryParameter.FORMAT.key)?.let {
FormatValue.fromString(it).bind()
}
val attributeRepresentation = when {
formatParam == FormatValue.KEY_VALUES || formatParam == FormatValue.SIMPLIFIED ->
AttributeRepresentation.SIMPLIFIED
formatParam == FormatValue.NORMALIZED ->
AttributeRepresentation.NORMALIZED
optionsParam.contains(OptionsValue.KEY_VALUES) || optionsParam.contains(OptionsValue.SIMPLIFIED) ->
AttributeRepresentation.SIMPLIFIED
else -> AttributeRepresentation.NORMALIZED
}
val includeSysAttrs = optionsParam.contains(OptionsValue.SYS_ATTRS)

Comment on lines 10 to 11
fun fromString(key: String): FormatValue? =
FormatValue.entries.find { it.value == key }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fun fromString(key: String): FormatValue? =
FormatValue.entries.find { it.value == key }
fun fromString(key: String): Either<APIException, FormatValue> = either {
FormatValue.entries.find { it.value == key }
?: return InvalidRequestException("'$key' is not a valid value for the format query parameter").left()
}

@ranim-n
Copy link
Contributor Author

ranim-n commented Jan 9, 2025

don't know why you merged develop in your branch (we never do that, we rebase on develop), but it is a bit a mess. there is at least one commit from develop which is included in the branch...

There were conflicts with develop that I fixed on github, I clicked on commit after and it did a merge. Can it be fixed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
common Relates to common behaviors feature New feature or request ngsild-1.8.1
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6.3.20 return invalidRequest for bad values of Options parameter Add support for the format parameter
3 participants