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
Remove response mappings #165
Conversation
- added test coverage - placed relationship information at the individual module level - added a test for when a relationship key is not in the response map because otherwise it produces a particularly awful, vague error
…ide a conditional
Just glanced over this briefly, but based on what I see , it's amazing 🎉 This really simplifies the conversion logic. |
Also quickly looked over this, amazing work @SViccari |
|
||
defp convert_value(value) when is_map(value) do | ||
Enum.reduce(value, %{}, fn({key, value}, acc) -> | ||
Map.put(acc, String.to_atom(key), convert_value(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.
It's a bit unfortunate that String.to_atom(key)
has to be used here but it is necessary as string keys are ignored when creating a 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.
Since this should always be going to a key in a struct, presumably String.to_existing_atom/1
should be safe, right?
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.
Unfortunately, that's not necessarily true :( . A good example is legal_entity
on Account (which hasn't been added yet but I believe we would like to add it.) Here's a small example for the docs:
"legal_entity": {
"address": {
"city": null,
"country": "US",
"line1": null,
"line2": null,
"postal_code": null,
"state": null
}
}
If I use to_existing_atom
, all the keys listed within legal_entity
would need to be added to the Account's defstruct
. Right now, we can just add legal_entity
to Account and all nested keys will still be included in the 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.
Ah! I see what you mean.
Two ideas:
- I'm wondering if you could break it out so that top-level definitions are
String.to_existing_atom/1
and lower levels are taken without restriction (I believe that's the same logic, but with added complexity in the conversion implementation). - Is there a way to invert the conversion such that the input atom is converted to a string instead of vice-versa? Then you can just use the original input atom for the
Map.put/3
.
In theory the algorithm you designed is restricted enough that a user would have to intentionally misuse it (at great length, no less) to cause an atom table overflow, but I still prefer to be safer with atom conversions if we can.
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.
@SViccari @DavidAntaramian how do we feel about addressing this in a follow-up refactoring issue? I'm comfortable merging as-is and circling back if you are.
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.
@DavidAntaramian I'm afraid I don't quite follow idea #2
. Perhaps we can find a time to chat on Slack?
As for idea #1
, I believe what you're describing is what is currently happening as all "top-level definitions" are being derived from [_|struct_keys] = Map.keys(module.__struct__)
. The only time string to atom conversion is occurring is when convert_value
encounters low-level/nested maps.
I'd still love to chat about this when you have the time! I'm also okay with merging this PR and issuing another PR with any improvements our conversation uncovers :)
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.
@SViccari I think merging this and then possibly refactoring later would be the best plan. Will send a message on Slack when I get the chance. It's possible that I'm missing a key piece that makes my idea infeasible.
statement_descriptor: :string, | ||
trial_period_days: :integer | ||
@relationships %{ | ||
created: DateTime | ||
} |
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 a little uncomfortable with the naming of relationships
in the cases where we're only specifying DateTime
for some fields. Can't think of a better name, though, so this is a rather empty critique.
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.
That's a very good point, relationships
isn't a great name for "keys that qualify to be converted into a struct". I'm not sure of a better name at the moment but I'll see if I can come up with 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.
Is there any way for us to determine whether a unix timestamp is a timestamp by pattern matching instead? @DavidAntaramian?
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.
Hm, I can't think of way to distinguish an integer that is not a timestamp from an integer that does represent a timestamp.
Ideally, I'd like to remove the use of relationships
from the modules as the knowledge of "keys that qualify as structs" isn't something the modules should manage. One should not create a new module and be required to define relationships
on it. Earlier in this PR, I started down the path of removing this mapping entirely but then decided that would be best done in a separate PR.
I think we could achieve this by creating a lib/defined_struct_keys.ex
file that is the sole source of which data keys are mapped to a struct. OR, it could be done by using the object
value in the Stripe response information in combination with pattern matching functions that target the keys created, trial_end, trial_start
etc.
I would recommend the DefinedStructKeys
approach as I find it more obvious and it's easy maintain.
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.
@joshsmith In direct answer to your question, no, there is no way to determine whether an integer is a valid UNIX timestamp since any integer is conceivably a valid UNIX timestamp.
I wish that Stripe had some sort of discoverable schema we could make use of, but unfortunately that's not the case. I think @SViccari's proposes a good middle-ground in this case. Either approach is good with me, but if I were personally implementing this I would actually choose to pattern match on the key name. It's less obvious, I agree, but I think it's easier to maintain in the long run. Though only time will tell on the second part 😄
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.
This looks great 👍 !
fetch_value(response, key) | ||
|> convert_value() | ||
|
||
value = Map.get(module.relationships, key) |
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.
Above we newline on value
but here we don't. Thoughts on newlining here as well for consistency?
Also, what are your thoughts on piping everything?
value =
module.relationships
|> Map.get(key)
|> build_struct(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.
I like it! You also reminded me that build_struct
should really be maybe_build_struct
as there are two cases where build_struct
does not return a struct. I'll update in a bit.
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.
Updated
Map.put(acc, String.to_atom(key), convert_value(value)) | ||
end) | ||
end | ||
defp convert_value(value), do: 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.
This whole block is really clever! 👏 It must have been tedious to figure out. Nice work!
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.
Yes, yes it was 😬 I'm afraid it's more clever than I like as there is still sooo much happening but I think this is a good amount of change for one PR :)
defmodule PersonWithMissingRelationship do | ||
defstruct [:card] | ||
def relationships, do: %{card: Stripe.ConverterTest.CreditCard} | ||
end |
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've never seen defining modules with structs like this in a test. This is handy to know! 👍
} | ||
} | ||
|
||
result = Converter.stripe_map_to_struct(Stripe.ConverterTest.Person, json_response) |
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.
Thoughts on aliasing Stripe.ConverterTest.Person
? It may make this a bit easier to read through.
def relationships, do: %{} | ||
end | ||
|
||
defmodule PersonWithMissingRelationship do |
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.
PersonWithMissingRelationship
sounds depressing 😞
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.
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.
This is actually an amazing module and should never change. If ever needed, I am willing to constantly rearchitect everything around this, no matter how tedious or unnecessary.
test "converts a Stripe response into a struct" do | ||
expected_result = %Stripe.ConverterTest.Person{ | ||
first_name: "Leslie", | ||
last_name: "Knope", |
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.
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.
This seems to be feature-sound to me. I suggest we merge this and any further refactoring opportunities we identify can be ticketed and then addressed at a future point in time.
@SViccari do you mind opening a separate issue as you had suggested? Wanted this merged so we could work from it. |
Why:
This PR was created after the discussion I had with @joshsmith around existing PR #153 and how we want to avoid curating a
response_mapping
list and defining modules that aren't associated with a Stripe resource (ex:Address
).TO-DOs:
This PR:
response_map
defstruct
, the key will not be ignored when converted to a Struct)Concerns:
Being new to this code base, there's a strong chance I've misinterpreted what's necessary and the end goal . Also, given the lack of test coverage, I'd appreciate help/advice in ensuring untested areas are still sound.
Side-Effects:
This PR introduces the notion of a
relationships
map. This is a simplification of what used to be theresponse_mapping
. Ideally, I'd like to remove the necessity of singling out particular keys and mapping them to their proper struct but given the examplecreated: DateTime
, we can't always rely on the object type that's returned in the Stripe API response.