Skip to content

Suggestion: Use Seq[T] instead of Option[Seq[T]] in UploadDtos#348

Open
ggVGc wants to merge 6 commits into
jonathanthiry/l3-variablesfrom
valter/simplify-option-seq-upload-dtos
Open

Suggestion: Use Seq[T] instead of Option[Seq[T]] in UploadDtos#348
ggVGc wants to merge 6 commits into
jonathanthiry/l3-variablesfrom
valter/simplify-option-seq-upload-dtos

Conversation

@ggVGc
Copy link
Copy Markdown
Contributor

@ggVGc ggVGc commented Feb 25, 2026

Wrapping a Seq in Option has the same semantic meaning in almost every case I can think of, and in the cases where it does not, I think an alternative and more descriptive solution should probably be used.

The changes in this PR should be functionally equivalent to the previous version, while simplifying the logic using these data types, but I have not tested anything in practice yet, so there may of course be edge cases I have not thought of.
I have left comments at relevant places for why I believe these changes are functionally equivalent and valid, though.

Note: Based on #341 since it touches same files.

  • There are a few more DTOs with Option[Seq[]] that should probably also be changed as part of this PR.

Testing TODO

  • Tests should be added in a new branch, based on master, verifying existing behavior. Then the tests can be merged into this one.
  • Where could this change have visible impacts?:
    • When registering a new metadata package, do we still accept the same payloads? Parsing of a payload happens here.
    • The structures in [DataObject.scala)[https://github.com/ICOS-Carbon-Portal/meta/blob/master/core/src/main/scala/se/lu/nateko/cp/meta/core/data/DataObject.scala#L1] are serialized to JSON and embedded in the source of metadata previews. That is implemented in SchemaOrg.dataJson(object: DataObject), which can be tested directly.
    • The changes affect files in meta/upload/subforms. Most likely, the best way to test it is to manually check that the interface still works as expected.
  • Are there existing tests, and what do they cover?
    • There does not seem to be any test covering the upload route. Adding it next to AlternativePathRouteTest would make sense.

@ggVGc ggVGc marked this pull request as draft February 25, 2026 11:25
Copy link
Copy Markdown
Contributor Author

@ggVGc ggVGc left a comment

Choose a reason for hiding this comment

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

Reasonings behind the main changes.

extra <- memb.role.extra;
l2 <- dobj.specificInfo.toOption;
cols <- l2.columns
l2 <- dobj.specificInfo.toOption
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Side note, but this usage of Either.toOption is why I don't really like the Either type in general. It signifies that one value (the Left one) is more important or correct than the other one. So, in that sense it is essentially an "Option with an error message".
But, our usage of it here does not actually carry that meaning.
So, I would prefer a compound type like SpatioTemporalMeta | StationTimeSeriesMeta instead, and that the uses of Either.fold be replaced by just pattern matches.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It sounds like it could be a nice improvement. I cannot think of a reason why we are using Either other than union types didn't exist when this code was written.

stationTs.columns.fold("")(_.collect{
case v if v.valueType.unit.isDefined => v.label
}.mkString(" (", ", ", ")"))
if (stationTs.columns.nonEmpty)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The original code wants to produce an empty string if there is no content, and the .mkString(...) call would not do that with an empty list. This check preserves original behaviour.

}

val variableMeasured = asOptArray(dobj.specificInfo.fold(_.variables, _.columns))(
val variableMeasured = asOptArray(dobj.specificInfo.fold((_.variables), _.columns))(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

JSON serialization is one case there Option[Seq[T]] could differ from Seq[T], producing a null or [].
However, here we are using asOptArray which converts an empty list to JsNull as seen here

So, even though we are changing from an Option to a Seq here, the end result will be the same during serialization.

for valTypeLookup <- valTypeLookupV; varName <- vars do
if valTypeLookup.lookup(varName).isEmpty then errors +=
s"Variable name '$varName' is not compatible with dataset specification ${dsSpec.self.uri}"
if (spTempMeta.variables.nonEmpty) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I am not entirely sure if this empty check is necessary, but including it assures we are keeping existing behaviour.

addInstrDeplInfo(stationUri, interval, columns)
columnsOptV.sinkOption.map: columnsOpt =>
StationTimeSeriesMeta(acq, prod, nRows, lblCoverage, columnsOpt)
StationTimeSeriesMeta(acq, prod, nRows, lblCoverage, columnsOpt.getOrElse(Nil))
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The option value columnsOpt here comes from parseJsonStringArray, where the None case signifies a parsing failure (a swallowed exception).
Hence, I don't see that there would be any semantic difference between Option and empty Seq here either.

<br>

@for(variables <- varMetas){
@if(varMetas.nonEmpty){
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Preserving original behaviour, so we do not show any preview if there are no items. There is a slight difference here from previous behaviour, which I argue is actually a bug-fix.
Previously, we could have a Some(Seq.empty), which would mean we show this section but without any items, which I believe is not the intention. With this change, we only ever show the section if there are items.


def values: Try[Option[Seq[DatasetVar]]] = if(elems.isEmpty) Success(None) else Try{
Some(elems.map(_.varInfo.get).toIndexedSeq)
def values: Try[Seq[DatasetVar]] = if(elems.isEmpty) Success(Nil) else Try{
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Here we clearly see that there is no semantic change from our perspective between Option and Seq, since we are explicitly turning the empty case into a None in the previous version.

Comment on lines +100 to +110
if (selsectedVars.nonEmpty) {
datasetSpec match {
case None => ()
case Some(dataset) => {
whenDone(getVariables(dataset)) { variables =>
varInfoForm.list = variables
varInfoForm.setValues(selsectedVars.flatMap(uri =>
varInfoForm.list.find(_.uri.toString.split('/').last == uri)
))
}
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Another nonEmpty check I am not entirely sure is needed, but added to make sure we preserve prior behaviour.
I also changed the map call, since we are ignoring the result value and hence should use .foreach, but I personally think an explicit pattern match is much clearer.

val nRowsQ = nRows.fold("")(nr => s"&nRows=$nr")
val varsQ = varnames.fold(""){vns =>
val varsJson = encodeURIComponent(Json.toJson(vns).toString)
val varsQ = if (varnames.nonEmpty) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Emptiness check to make sure we produce empty string instead of $varnames= when there are no vars.

Copy link
Copy Markdown
Contributor

@jonathanthiry jonathanthiry left a comment

Choose a reason for hiding this comment

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

In general, I agree with the idea. I don't know for sure why it was decided to use Option[Seq[T]] to start with. As you are hinting in your comments, these changes would require proper testing of the impacted code paths to avoid introducing any bugs, as it is not easy to judge this at a glance.

extra <- memb.role.extra;
l2 <- dobj.specificInfo.toOption;
cols <- l2.columns
l2 <- dobj.specificInfo.toOption
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It sounds like it could be a nice improvement. I cannot think of a reason why we are using Either other than union types didn't exist when this code was written.

Copy link
Copy Markdown
Contributor

@Jonathan-Schenk Jonathan-Schenk left a comment

Choose a reason for hiding this comment

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

The changes you suggest make sense and improve the clarity of the code. I think it is a good idea to apply them after testing that nothing breaks because of them, although it seems that all current behaviors would be maintained.

}

val variableMeasured = asOptArray(dobj.specificInfo.fold(_.variables, _.columns))(
val variableMeasured = asOptArray(dobj.specificInfo.fold((_.variables), _.columns))(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
val variableMeasured = asOptArray(dobj.specificInfo.fold((_.variables), _.columns))(
val variableMeasured = asOptArray(dobj.specificInfo.fold(_.variables, _.columns))(

whenDone(getVariables(dataset)) { variables =>
varInfoForm.list = variables
varInfoForm.setValues(Some(varUris.flatMap(uri => varInfoForm.list.find(_.uri.toString.split('/').last == uri))))
if (selsectedVars.nonEmpty) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This block can be unindented. The selsectedVars could also be renamed to selectedVars to remove the typo.

@ggVGc ggVGc marked this pull request as ready for review March 18, 2026 12:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants