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

Managing schema registry's schema references #50

Merged
merged 4 commits into from
Nov 16, 2020

Conversation

FrancescoPessina
Copy link
Contributor

This PR enables Avro schema parsing with new schema registry's schema references feature, introducing a dedicated lookup function for references got from CSR.
Changes in this PR comes from the discussion of the deprecated #49 pull request.

cc @Strech

Copy link
Owner

@Strech Strech left a comment

Choose a reason for hiding this comment

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

Thanks a lot for your effort the code looks much cleaner now 👍🏼

In addition to specific points, I have a few recommendations about the code style:

  1. Please don't prefix variables, we have a file context and it should cover that need
  2. Please don't use 1-2 char variables, it makes code harder to read and it's acceptable in few cases when a shorter name will give some benefits.
  3. Please run mix format to align the code with project formatting rules (they are pretty standard, except the line length)

@@ -33,7 +33,7 @@ defmodule Avrora.Storage.Registry do
{:ok, id} <- Map.fetch(response, "id"),
{:ok, version} <- Map.fetch(response, "version"),
{:ok, schema} <- Map.fetch(response, "schema"),
{:ok, schema} <- Schema.parse(schema) do
{:ok, schema} <- Schema.parse(schema, csr_references_lookup_function(response)) do
Copy link
Owner

Choose a reason for hiding this comment

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

Since this function is local and will be called on each reference we should apply a few adjustments:

  1. Rename csr_references_lookup_function to make_references_lookup_fun because the scope of the whole module is CSR so no need to mention it in any name here
  2. All the references must be prepared in advance otherwise (like in the current implementation) they will be extracted on each function call

As an idea:

Suggested change
{:ok, schema} <- Schema.parse(schema, csr_references_lookup_function(response)) do
{:ok, references} <- extract_references(response),
{:ok, schema} <- Schema.parse(schema, make_references_lookup_fun(references)) do

where references is a Map

@@ -93,10 +93,26 @@ defmodule Avrora.Storage.Registry do
@spec configured?() :: true | false
def configured?, do: !is_nil(registry_url())

defp csr_references_lookup_function(response) do
Copy link
Owner

Choose a reason for hiding this comment

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

To comply with typespec of reference lookup function from Avrora.Schema we should always return a function with interface like

@type reference_lookup_fun :: (String.t() -> {:ok, String.t()} | {:error, term()})

which means for us that we can re-use the default lookup function Avrora.Schema.reference_lookup/1

case Map.fetch(response, "references") do
{:ok, csr_references} ->
references = Enum.reduce(csr_references, %{}, fn(cr, map) ->
{:ok, schema_map} = get(cr["subject"])
Copy link
Owner

@Strech Strech Nov 10, 2020

Choose a reason for hiding this comment

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

This will throw a matching error, but what if we can use case + throw to take control over it and return {:error, ...} instead. Also would be nice to cover it with a test of something like "reference can't be obtained"

Comment on lines 104 to 107
fn(r) ->
json_schema = Map.get(references, r)
{:ok, json_schema}
end
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
fn(r) ->
json_schema = Map.get(references, r)
{:ok, json_schema}
end
{:ok, &Map.fetch(references, &1)}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Strech thank you for your review, I'm a newbie in Elixir so any correction is very important for me.
I think I've applied all your corrections in last commit. Please take a look and if I missed something please let me know. Thanks.

Copy link
Owner

@Strech Strech left a comment

Choose a reason for hiding this comment

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

Thanks for the refactoring, let's improve the reference extraction and we will be good to go 🚀

end
end

defp make_references_lookup_fun(references) do
Copy link
Owner

@Strech Strech Nov 12, 2020

Choose a reason for hiding this comment

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

I think a general correction for the whole file - reference_lookup because it's a function to resolve 1 reference at a time. So better to name it

{:error, subject_not_found_parsed_error()}
end)

assert catch_throw(Registry.get("io.confluent.Account") == :unknown_subject)
Copy link
Owner

Choose a reason for hiding this comment

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

I think it was a bit of misunderstanding, this assertion should look like this

Suggested change
assert catch_throw(Registry.get("io.confluent.Account") == :unknown_subject)
assert Registry.get("io.confluent.Account") == {:error, :unknown_reference_subject}

end
end

defp get_reference_schema(map, subject) do
Copy link
Owner

Choose a reason for hiding this comment

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

This method should be inlined (i.e removed) back to the extract_references/1

Comment on lines 97 to 110
defp extract_references(response) do
case Map.fetch(response, "references") do
{:ok, references} ->
references_map =
Enum.reduce(references, %{}, fn cr, map ->
get_reference_schema(map, cr["subject"])
end)

{:ok, references_map}

:error ->
{:ok, :schema_without_references}
end
end
Copy link
Owner

Choose a reason for hiding this comment

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

By using throw I mean that we need to exit from the inner loop with error, but not throw it to outside, throw it's just a control flow in this case.

I would like to suggest something like this

Suggested change
defp extract_references(response) do
case Map.fetch(response, "references") do
{:ok, references} ->
references_map =
Enum.reduce(references, %{}, fn cr, map ->
get_reference_schema(map, cr["subject"])
end)
{:ok, references_map}
:error ->
{:ok, :schema_without_references}
end
end
defp extract_references(response) do
references =
response
|> Map.get("references", [])
|> Enum.reduce(%{}, fn reference, memo ->
case get(reference["subject"]) do
{:ok, schema} -> Map.put(memo, schema.full_name, schema.json)
{:error, error} -> throw(error)
end
end)
{:ok, references}
catch
{:error, :unknown_subject} -> {:error, :unknown_reference_subject}
error -> {:error, error}
end

@@ -33,7 +33,8 @@ defmodule Avrora.Storage.Registry do
{:ok, id} <- Map.fetch(response, "id"),
{:ok, version} <- Map.fetch(response, "version"),
{:ok, schema} <- Map.fetch(response, "schema"),
{:ok, schema} <- Schema.parse(schema) do
{:ok, references} <- extract_references(response),
{:ok, schema} <- Schema.parse(schema, make_references_lookup_fun(references)) do
Copy link
Owner

Choose a reason for hiding this comment

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

Please rename make_references_lookup_fun to make_reference_lookup_fun (see below)

Comment on lines 119 to 124
defp make_references_lookup_fun(references) do
case references do
:schema_without_references -> &Avrora.Schema.reference_lookup/1
_ -> &Map.fetch(references, &1)
end
end
Copy link
Owner

Choose a reason for hiding this comment

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

Not sure why the credo silent about it, but for such a case if/else should be used

    if references == :no_references,
      do: &Avrora.Schema.reference_lookup/1
      else: &Map.fetch(references, &1)

And then we can return {:ok, %{}} in extract_references and use the same if/else

     if references == %{},
       do: &Avrora.Schema.reference_lookup/1
       else: &Map.fetch(references, &1)
    end

and from here we can go to pattern matching

Suggested change
defp make_references_lookup_fun(references) do
case references do
:schema_without_references -> &Avrora.Schema.reference_lookup/1
_ -> &Map.fetch(references, &1)
end
end
def make_reference_lookup_function(%{}), do: &Avrora.Schema.reference_lookup/1
def make_reference_lookup_function(references), do: &Map.fetch(references, &1)

@FrancescoPessina
Copy link
Contributor Author

@Strech I applied your advices. Thanks :)
I also added reference version in get call, see here:

https://github.com/FrancescoPessina/avrora/blob/e4963e1d49ac5d478d987d9b0476b389120d4df7/lib/avrora/storage/registry.ex#L103-L105

@Strech
Copy link
Owner

Strech commented Nov 16, 2020

@FrancescoPessina Wanna say thanks for amazing contribution 🎉 . I will merge your PR, but the version release will be together with #47

If it will take too long (week+) for Klarna to release klarna/erlavro#104 then I would make preparations and release the next version without nil fix.

@Strech Strech merged commit 951de80 into Strech:master Nov 16, 2020
@FrancescoPessina
Copy link
Contributor Author

@Strech thank you very much for your precise review :)
Ok, I'll wait the version release, no problem ;)

Thanks again

@Strech
Copy link
Owner

Strech commented Nov 26, 2020

Hey, @FrancescoPessina a heads-up! The new 0.14 release was published on the Hex. Thanks a lot for your help, I appreciate it!

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