-
Notifications
You must be signed in to change notification settings - Fork 7
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
Add Mutable View for Struct Elements #84
Conversation
Adds a MutableStructFields interface which represents an iterable sequence of mutable fields that can be used to construct a StructElement.
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## master #84 +/- ##
=========================================
Coverage ? 90.29%
Complexity ? 491
=========================================
Files ? 31
Lines ? 1051
Branches ? 153
=========================================
Hits ? 949
Misses ? 59
Partials ? 43 ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you so much for following up on this. Overall, this is really helpful, and I think this is the right concept. After seeing some of the demo classes, I think the APIs might need to change just a little bit back in the direction of your original suggestion for an update DSL.
I think this PR is also excellent work to inform the design of similar functions for SexpElement
and ListElement
, which I can put together after we get this one merged.
/** A mutable shallow copy of this struct's fields */ | ||
public val mutableFields: MutableStructFields |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am hesitant to make this a val
given that val
implies that it is a property of the class. Maybe fun to____()
instead? (I'm not sure what that___
should be; I suppose it depends on what the final name for MutableStructFields
ends up being.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree a function makes more sense since the property always returns a new object anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another question is whether the MutableStructFields object can or should carry the annotations and metas attached to the original struct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's MutableStructFields
, then I would say that it does not need the annotations or the metas. If we were going for a MutableStructElement
, then we probably should include the annotations and metas.
I'm not opposed to going for a MutableStructElement
, but I think it's a much more drastic change since we would need to define how the mutable elements fit into the inheritance hierarchy (does it implement MutableIonElement
, and how do IonElement
and MutableIonElement
relate?) and we would probably end up implementing mutable versions for all of the Ion types.
*/ | ||
public interface MutableStructFields : Iterable<StructField> { | ||
|
||
public val fields: MutableMap<String, MutableList<AnyElement>> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question—I think this looks like an implementation detail. Is it necessary to include it in the interface?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, that looks like a mistake, it should have been private.
public fun add(field: StructField): MutableStructFields | ||
|
||
/** Removes a random occurrence of a field found with the given name or does nothing if no field exists */ | ||
public fun remove(fieldName: String): MutableStructFields |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question—do you think there is a real use case for removing one field with a given field name? Perhaps it would be more useful if the signature was fun remove(field: StructField)
so that the user of the library can remove a specific field rather than an arbitrary one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, I will update this function.
* If one or more fields with the specified name already exists, this replaces the value of the *first* field found. | ||
* | ||
* Otherwise, a new field with the given name and value is added to the collection |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably not make promises that depend on the order of StructField
s. Is there any particular reason that you think this should not replace all existing fields with the given field name? (I think it would be a sensible choice, and it is what you have chosen to do for the setAll
function.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense as the behavior you describe aligns with ion-java.
* | ||
* If multiple fields with the same name exist they are all replaced by the new fields. | ||
*/ | ||
public fun copy(fields: Iterable<StructField>): StructElement |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, I know I suggested copy
in #83, but my thinking has changed over the last few months. I suppose it's philosophical—is it really a copy if you're doing a full replacement of the fields? (Whether someone is fully replacing the fields or not, that's what this function does.) I don't necessarily have a better name right now, and I'm definitely open to suggestions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After writing this, I feel that copy is a bit anemic. This seems more straight forward.
myStruct.toMutableStructFields()
.add(...)
.add(...)
...
.toStruct()
There just needs to be a way to deal with the annotations and metas you may want to copy over.
Having a plus operator between two MutableStructFields and between StructElement and MutableStructFields also makes copy less useful.
value as AnyElement | ||
if (fieldName in fields) { | ||
fields[fieldName]?.clear() | ||
fields[fieldName]?.add(value) | ||
} else { | ||
fields[fieldName] = mutableListOf(value) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two comments about this.
- This does not match the function documentation. (It's replacing all fields with that name rather than one field with that name.) However, I believe this implementation is the better choice, and it is the documentation that should change.
- You can simplify this to just
value as AnyElement | |
if (fieldName in fields) { | |
fields[fieldName]?.clear() | |
fields[fieldName]?.add(value) | |
} else { | |
fields[fieldName] = mutableListOf(value) | |
} | |
val values = fields.getOrPut(fieldName, ::mutableListOf) | |
values.clear() | |
values.add(value as AnyElement) |
} | ||
} | ||
|
||
public fun buildStruct(body: MutableStructFields.() -> Unit): StructElement { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it might be worth adding annotations
and possibly metas
to this function, as in the ionStructOf()
functions.
Also, since this function is public, it should be moved into com.amazon.ionelement.api
.
override fun toStruct(annotations: List<String>, metas: MetaContainer): StructElement { | ||
return ionStructOf(this, annotations, metas) | ||
} | ||
|
||
override fun toStruct(): StructElement { | ||
return ionStructOf(this) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the ionStructOf()
functions exist, I think these might be redundant. (I want to be careful not to add unnecessary things because once it's out there, it's permanent at least until the next major version.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added this because the following:
myStruct.toMutableStructFields()
.add(...)
.add(...)
...
.toStruct()
seems cleaner than:
ionStructOf(myStruct.toMutableStructFields()
.add(...)
.add(...)
...)
or
myStruct.toMutableStructFields()
.add(...)
.add(...)
...
.let {ionStructOf(it)}
This is just a matter of style though, and should style warrant expanding the interface?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not opposed to ever adding them, but for now, there are other ways to build the struct, and I think it is better to start smaller and then add things if it does end up being an issue.
If we have update()
or similar methods on StructElement
, then in Java, you could write:
myStruct.updateFields(f -> f.add(...)
.set(...));
And in Kotlin:
myStruct.update {
add(...)
set(...)
}
What do you think? Do you prefer the lambda expression (Kotlin) and consumer-builder (Java) or the traditional builder syntax?
I think this is also related to the question of whether annotations should be preserved. I think that if we have an "update" method, the name implies that it would preserve annotations and metas. If you want to get rid of the annotations and metas, you can simply create a new struct out of the MutableStructFields
using ionStructOf()
. (By "new struct", I'm referring to the user's intentions—you get a new instance of StructElement
either way.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think? Do you prefer the lambda expression (Kotlin) and consumer-builder (Java) or the traditional builder syntax?
I prefer the former. But are you suggesting to add the update
method in addition to mutableStructFields()
?. I like mutableStructFields()
because you can assign it to a variable and pass it around kind of like a StringBuilder
. With update
you are completing the whole process at that moment. I would propose to add both.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine with having both.
My initial comment was that we should leave out the toStruct()
function since there's already ionStructOf()
. I'm not strongly opposed to it—it's just my default opinion that if something seems redundant, we should probably leave it out. If you have a strong opinion that toStruct()
should exist, and it should be a member (as opposed to e.g. an extension function), then let's keep it.
return fields[fieldName]?.firstOrNull() | ||
} | ||
|
||
override fun getAll(fieldName: String): Iterable<AnyElement> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion—I suspect that users of the library might want to count the number of fields for a given name, so we may want to consider returning Collection<AnyElement>
here instead. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, I will update.
Co-authored-by: Matthew Pope <[email protected]>
* Change `mutableStructFields` from property to method * Remove `copy` StructElement, method 6can be added later * Added `update` StructElement, method
Implementing MutableCollection instead of Iterator will allow extra functionality built into MutableCollection. To simplify implementing `iterator()` on MutableStructFields the internal backing field was also changed to MutableMap<String, MutableList<StructField>> at the expense of slightly more overhead because the underlying struct's field name is stored in the map key as well as value. This can be optimized in the future if desired.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! This is great.
Issue #, if available:
Description of changes:
Adds a MutableStructFields interface which represents an iterable sequence of mutable fields that can be used to construct a StructElement.
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.