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(map): add serialisation for map<string, string> #654

Merged
merged 17 commits into from
Jul 17, 2023

Conversation

jonathan-casey
Copy link
Member

This change adds basic serialisation of a map in the form of <string, string>. Further type variations will follow in subsequent PR.

For more information on the design specification see here

Screenshots or Video

Related Issues

Author Checklist

  • Ensure you provide a DCO sign-off for your commits using the --signoff option of git commit.
  • Vital features and changes captured in unit and/or integration tests
  • Commits messages follow AP format
  • Extend the documentation, if necessary
  • Merging to main from fork:branchname

@jonathan-casey jonathan-casey requested a review from a team May 25, 2023 16:40
Signed-off-by: jonathan.casey <[email protected]>
Signed-off-by: jonathan.casey <[email protected]>
Signed-off-by: jonathan.casey <[email protected]>
Signed-off-by: jonathan.casey <[email protected]>
Signed-off-by: jonathan.casey <[email protected]>
Signed-off-by: jonathan.casey <[email protected]>
Signed-off-by: jonathan.casey <[email protected]>
@jonathan-casey jonathan-casey force-pushed the jonathan/map_serialization_basic branch from 870203a to 2cb6ca1 Compare June 2, 2023 16:58
@jonathan-casey jonathan-casey force-pushed the jonathan/map_serialization_basic branch from 2cb6ca1 to a6c87fa Compare June 2, 2023 16:58
packages/concerto-core/lib/serializer.js Outdated Show resolved Hide resolved
Copy link
Contributor

@dselman dselman left a comment

Choose a reason for hiding this comment

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

Some questions:

  1. Don't we want to support Map values other than String?
  2. Runtime type for maps, support JS object?

@@ -161,18 +161,20 @@ class Serializer {

// create a new instance, using the identifier field name as the ID.
let resource;
if (classDeclaration.isTransaction()) {
if (classDeclaration.isTransaction?.()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need to loosen this?

Copy link
Member Author

Choose a reason for hiding this comment

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

MapDeclaration doesn't implement isTransaction()

Copy link
Member

Choose a reason for hiding this comment

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

Perhaps it should? we could move the function to the Declaration class?

packages/concerto-core/lib/modelutil.js Outdated Show resolved Hide resolved
packages/concerto-core/lib/serializer/instancegenerator.js Outdated Show resolved Hide resolved
* @return {*} an enum value.
*/
getMap() {
return new Map([[this.getString(1,10), this.getString(1,10)]]);
Copy link
Contributor

Choose a reason for hiding this comment

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

Values don't have to be strings though... won't this generate invalid maps? I think you want to return a map of N entries, with random string keys (use the same method we use to generate random strings) and then random values, based on the type of the value — again reusing the existing logic.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, but for now we are supporting <String, String> only. In future the method will generate random values of all types supported.

Copy link
Contributor

Choose a reason for hiding this comment

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

Is the grammar for CTO aligned with this?

packages/concerto-core/lib/serializer/valuegenerator.js Outdated Show resolved Hide resolved
visitMapDeclaration(mapDeclaration, parameters) {
const obj = parameters.stack.pop();

if (!((obj.value instanceof Map))) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we not want to accept simple JS objects, indexed as Maps? I think this will force users to create Maps in their code rather than just JS objects.

if (typeof key !== 'string') {
ResourceValidator.reportInvalidMap(parameters.rootResourceIdentifier, mapDeclaration, obj);
}
if (typeof value !== 'string') {
Copy link
Contributor

Choose a reason for hiding this comment

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

We only support values that are Strings?

@@ -69,6 +69,7 @@
"resourcevalidator-undeclaredfield": "Instance \"{resourceId}\" has a property named \"{propertyName}\", which is not declared in \"{fullyQualifiedTypeName}\".",
"resourcevalidator-invalidfieldassignment": "Instance \"{resourceId}\" has a property \"{propertyName}\" with type \"{objectType}\" that is not derived from \"{fieldType}\".",
"resourcevalidator-emptyidentifier": "Instance \"{resourceId}\" has an empty identifier.",
"resourcevalidator-invalidmap": "Model violation in the \"{resourceId}\" instance. Invalid Type for Map Key or Value - expected String type.",
Copy link
Contributor

Choose a reason for hiding this comment

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

Map values have to be Strings?

Copy link
Member Author

Choose a reason for hiding this comment

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

As per comment below: The alignment on the first release was <String, String> support only.

packages/concerto-core/test/serializer.js Show resolved Hide resolved
@jonathan-casey
Copy link
Member Author

Some questions:

  1. Don't we want to support Map values other than String?
  2. Runtime type for maps, support JS object?
  1. Yes, but in a future phase. The alignment on the first release was <String, String> support only. (The rest is coming, I promise!)
  2. As per spec: Maps should be implemented using the JavaScript [Map object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map). We inherit the semantics of that implementation here.

@jonathan-casey jonathan-casey enabled auto-merge (squash) July 14, 2023 06:28
@jonathan-casey jonathan-casey merged commit 39bae78 into main Jul 17, 2023
@jonathan-casey jonathan-casey deleted the jonathan/map_serialization_basic branch July 17, 2023 13:32
@DS-AdamMilazzo
Copy link

DS-AdamMilazzo commented Jul 20, 2023

@jonathan-casey @mttrbrts @dselman I think it's very unfortunate (and ugly) that the map is pushed down into a "value" property. It seems that the only reason is so that you can put Concerto metadata like $class at the top level of the map without colliding with user keys. However, I'd argue that there is virtually no reason to ever put such metadata there.

If you don't intend to allow a map to be extended, e.g. map Foo { ... } map Bar extends Foo { ... }, then there is no need for a $class, because the type will always equal the declared type of the field. (Furthermore, there is no need for $timestamp, $identifier, etc. because those are for concepts and have no meaning for a map.)

Let's say you were considering map extension. I'd argue it's a bad idea in most cases. There has to be a relation between the keys and values of the base and derived maps. In particular, the derived keys and values could either be a subset or a superset of the base keys and values. In this case, where they're a superset...

concept B { }
concept D extends B { }
concept D2 extends B { }
map M {
  o String
  o D
}
map M2 extends M {
  o String
  o B
}

This violates the basic expectations of polymorphism, because you can store a B or D2 in M2, thereby violating the requirements of the base map M. This should just never be allowed, from the standpoint of a type system.

In the other case, where they're a subset...

concept B { }
concept D extends B { }
concept D2 extends B { }
map M {
  o String
  o B
}
map M2 extends M {
  o String
  o D
}

First of all, you can already store a D (or D2) as a value in map M via basic polymorphism on B, with no need to extend map M to make that happen. Second, it's backwards from how extends would work for concepts and enums, where the derived type is a superset of the base type. Third, it violates a basic expectation of polymorphism, that a derived type can be treated as a base type. If you have an instance of map M2, you can't store a B or D2 in it. But if you treat M2 as an instance of M, which it is, then you can store a B or D2 in it. That seems kind of broken, conceptually. Do you really want to allow this?

It's not completely crazy, though. It can make some sense if we analogize to scalars. Let's say there was a way to extend a scalar type:

scalar PhoneNumber extends String regex=/^[0-9](-[0-9]+)*$/
scalar USPhoneNumber extends PhoneNumber regex=/^([1-9][0-9]{2}-){2}[0-9]{4}$/

A derived scalar would be a subset of the base scalar, just as a scalar now is a subset of the base type (e.g. String), and just as, in this case, a derived map is a subset of a base map. The important question, though, is how do you know when to apply the PhoneNumber rules versus the USPhoneNumber rules? Strings have no $class to tell you whether a string is a PhoneNumber or a USPhoneNumber. So how could you do it? It would have to be done based on the declared field type. That is to say, you have extension without polymorphism.

And if maps are extended analogously to scalars (i.e. they are a restriction of the base type, not an expansion), and if map polymorphism violates some expectations of polymorphism, then if we allow map extension at all (which I'd argue is less obviously useful than scalar extension with its layered constraints), it should also be extension without polymorphism. And without polymorphism, you need no $class property.

Let's look at an example of map extension with polymorphism anyway. Let's say you have concept C { o M map } and this instance data:

{
  "$class": "ns.C",
  "map": {
    "key": { "$class": "ns.D" }
  }
}
  • Would you need a $class property to read the data? No. All derived maps are instances of the base map type, M, which we know from the field definition. A derived map can't define additional "fields" like a derived concept can.
  • Would you need a $class property to write the data? If you're writing valid data, then no, but if you're writing invalid data, then yes.
  • Do you need a $class property to validate the data? Yes, in order to apply the added type constraint from the derived map. However, it is exactly analogous to this situation: concept C { o PhoneNumber phone }, where you want to send a USPhoneNumber and have it validated as such. How would you do that? Maybe just change the field type to USPhoneNumber. Or, we could support type restrictions in derived types, like concept C2 extends C { o USPhoneNumber phone override }. But I'd argue that whatever the solution is for scalars, the same solution should be used for maps. In that case, no $class is needed.

So to summarize:

  • Maps don't need $class unless they support both extension and polymorphism.
  • Map extension only makes sense as a restriction of the base map, analogous to extension of scalars.
  • Map extension may be less useful than scalar extension.
  • Map extension would have as much need for polymorphism as scalar extension, i.e. none.
  • Map extension also violates some expectations of polymorphism and so shouldn't have it anyway.

Overall, the case for needing $class in a map is extremely thin. So can we please move the map data out of the "value" property? This:

{
  "foo": "bar"
}

looks so much nicer than:

{
  "$class": "[email protected]",
  "value": {
    "foo": "bar"
  }
}

@jonathan-casey
Copy link
Member Author

@DS-AdamMilazzo thanks for your feedback. I agree with you on the $class value remarks, as do some others, and ultimately the communities voice takes precedent and the change has followed in a subsequent PR.

In future it would be most helpful to share your opinion before a sign off. The spec was public and shared for several months prior to implementation. In any case, your feedback is always welcome and appreciated.

@DS-AdamMilazzo
Copy link

I gladly would have, but these things don't always come to my attention.

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.

5 participants