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

Elixir: Switch Poison to Jason #16061

Merged
merged 11 commits into from
Jul 20, 2023
Merged

Conversation

barttenbrinke
Copy link
Contributor

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work. Missing information here may result in delayed response from the community.
  • Run the following to build the project and update samples:
    ./mvnw clean package 
    ./bin/generate-samples.sh
    ./bin/utils/export_docs_generators.sh
    
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    For Windows users, please run the script in Git BASH.
  • In case you are adding a new generator, run the following additional script :
    ./bin/utils/ensure-up-to-date
    
    Commit all changed files.
  • File the PR against the correct branch: master (6.3.0) (minor release - breaking changes with fallbacks), 7.0.x (breaking changes without fallbacks)
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

@mrmstn As Poison no longer has active support and Phoenix relies on Jason, I swapped the dependency. Tried to make the change as minimal as possible. As Jason is supported and 100% compatible with Poison, I basically did a global replace and regenerated the samples.

@wing328 wing328 added this to the 7.0.0 milestone Jul 11, 2023
@mrmstn mrmstn self-assigned this Jul 15, 2023
@mrmstn mrmstn self-requested a review July 15, 2023 13:53
@mrmstn
Copy link
Contributor

mrmstn commented Jul 15, 2023

@barttenbrinke First, thank you very much for the time you put in creating this PR! It helps us in bringing the various languages and generators to a higher level.

I tried to compile the new generated samples, but it resulted in the following error:

== Compilation error in file lib/openapi_petstore/model/_special_model_name_.ex ==
** (ArgumentError) Jason.Decoder is not a protocol
    (elixir 1.14.4) lib/protocol.ex:330: Protocol.assert_protocol!/2
    lib/openapi_petstore/model/_special_model_name_.ex:19: (file)
    (elixir 1.14.4) lib/kernel/parallel_compiler.ex:340: anonymous fn/5 in Kernel.ParallelCompiler.spawn_workers/7

Were you able to compile the generated samples on your side?

The steps to reproduce this are:

cd samples/client/petstore/elixir
mix deps.get
mix compile

Copy link
Contributor

@mrmstn mrmstn left a comment

Choose a reason for hiding this comment

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

The new generated client is unable to compile

@@ -16,7 +16,7 @@
}
end

defimpl Poison.Decoder, for: {{&moduleName}}.Model.{{&classname}} do
defimpl Jason.Decoder, for: {{&moduleName}}.Model.{{&classname}} do
Copy link
Contributor

Choose a reason for hiding this comment

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

This results in the following error:

== Compilation error in file lib/openapi_petstore/model/_special_model_name_.ex ==
** (ArgumentError) Jason.Decoder is not a protocol
    (elixir 1.14.4) lib/protocol.ex:330: Protocol.assert_protocol!/2
    lib/openapi_petstore/model/_special_model_name_.ex:19: (file)
    (elixir 1.14.4) lib/kernel/parallel_compiler.ex:340: anonymous fn/5 in Kernel.ParallelCompiler.spawn_workers/7

Copy link
Contributor

Choose a reason for hiding this comment

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

@mrmstn mrmstn assigned barttenbrinke and unassigned mrmstn Jul 15, 2023
@barttenbrinke
Copy link
Contributor Author

@mrmstn Thanks! - I will try and resolve this this week.

@barttenbrinke
Copy link
Contributor Author

Ugh, Jason.Decoder no longer supports decoding into Structs by default (as it can be unsafe) - however as you are generating libs here, is not a concern - https://github.com/michalmuskala/jason#differences-to-poison Let me think about how to fix this.

@barttenbrinke
Copy link
Contributor Author

barttenbrinke commented Jul 17, 2023

So, the only thing Poison does is generate a basic protocol definition:

https://github.com/devinus/poison/blob/master/lib/poison/decoder.ex#L106C1-L125C4

defprotocol Poison.Decoder do
  @fallback_to_any true

  @typep as :: map | struct | [as]

  @type options :: %{
          optional(:keys) => :atoms | :atoms!,
          optional(:decimal) => boolean,
          optional(:as) => as
        }

  @spec decode(t, options) :: any
  def decode(value, options)
end

defimpl Poison.Decoder, for: Any do
  def decode(value, _options) do
    value
  end
end

And then relies on the different classes to provide their own, sneaking them in as an argument... Well that sort of works, but in reality you would want to pull this through an Ecto Changeset and then cast all the things :/

Like here - https://elixirforum.com/t/how-to-decode-a-json-into-a-struct-safely/14331/14?u=barttenbrinke

* parse date-time values to Elixir DateTime
* improve formatting in various places, so there's less changes by `mix
  format` later
* fix Java version in flake.nix
@wpiekutowski
Copy link
Contributor

I believe this should be a safe approach to turning maps into structs without resorting to Ecto.Schema. I added some extra tests as well.

There's one thing that I've noticed that seems to be off. When polymorphism is used in arrays or objects, then from my short testing, it seems that the concrete classes won't be used, but instead only the parent class is evaluated. I inserted a TODO in the relevant test for the posterity.

@barttenbrinke
Copy link
Contributor Author

@mrmstn @wpiekutowski did some awesome work and simplified the whole poison type decode magic to generating decoders for all types based on the swagger spec

# NOTE: This file is auto generated by OpenAPI Generator 7.0.0-SNAPSHOT (https://openapi-generator.tech).
# Do not edit this file manually.

defmodule OpenapiPetstore.Model.EnumTest do
  @moduledoc """
  
  """

  @derive Jason.Encoder
  defstruct [
    :enum_string,
    :enum_string_required,
    :enum_integer,
    :enum_number,
    :outerEnum,
    :outerEnumInteger,
    :outerEnumDefaultValue,
    :outerEnumIntegerDefaultValue
  ]

  @type t :: %__MODULE__{
    :enum_string => String.t | nil,
    :enum_string_required => String.t,
    :enum_integer => integer() | nil,
    :enum_number => float() | nil,
    :outerEnum => OpenapiPetstore.Model.OuterEnum.t | nil,
    :outerEnumInteger => OpenapiPetstore.Model.OuterEnumInteger.t | nil,
    :outerEnumDefaultValue => OpenapiPetstore.Model.OuterEnumDefaultValue.t | nil,
    :outerEnumIntegerDefaultValue => OpenapiPetstore.Model.OuterEnumIntegerDefaultValue.t | nil
  }

  alias OpenapiPetstore.Deserializer

  def decode(value) do
    value
     |> Deserializer.deserialize(:outerEnum, :struct, OpenapiPetstore.Model.OuterEnum)
     |> Deserializer.deserialize(:outerEnumInteger, :struct, OpenapiPetstore.Model.OuterEnumInteger)
     |> Deserializer.deserialize(:outerEnumDefaultValue, :struct, OpenapiPetstore.Model.OuterEnumDefaultValue)
     |> Deserializer.deserialize(:outerEnumIntegerDefaultValue, :struct, OpenapiPetstore.Model.OuterEnumIntegerDefaultValue)
  end
end

This is a very robust solution for now and the future imho 👍.
The tests are also green and there are some additional ones.

Copy link
Contributor

@mrmstn mrmstn left a comment

Choose a reason for hiding this comment

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

@wpiekutowski
Wow. I'm really impressed, great work! I really like the way you took with the decode function and the replacement with the strut with just its base type, fantastic!

Now there are just some small open questions according to the changes in the common area of the generator. As I'm just in the tech. Committee of elixir, I feel a lot better if one of the core team could have a lot of those changes, as they have a bigger picture of the impact overall. (cc @wing328 )

Thank you once again for your time and effort. Your contributions are immensely appreciated!

Copy link
Contributor

Choose a reason for hiding this comment

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

@wpiekutowski Do we need those changes? Since this yaml is used by a lot of the sample clients, we would need to regenerate all those samples as well – and get the approval from all the different tech. committees

Alternately, @wing328 could you, or someone from the core team, have a look at those changes? Would it be possible to just generate the sample clients and go on with the PR? Or would it be preferred to split this PR up to get a clean split between elixir and common changes?

Copy link
Contributor

Choose a reason for hiding this comment

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

@mrmstn Thanks for your kind words!

I agree this change to the YAML file is significant. The reason for this change is that Petstore API server is now redirecting HTTP to HTTPS and Elixir client isn't configured to follow redirects. This caused included Elixir tests to fail.

The way to avoid this change could be to add a redirection Tesla middleware (1) or set base_url in tests – avoid using the default one extracted from the API spec (2).

I'm not very keen on (1) as it doesn't seem that Ruby or Elm clients are following redirects. There's a risk of credentials leak.
(2) makes more sense – doesn't add redirect following and doesn't affect other clients.

In the long run, I think we should encourage using HTTPS by defaulting to it.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see that this fails CI, because now many samples have a different URL compiled in. It seems that HTTP -> HTTPS switch should be at least a sepearate PR, if we agree that this is something that needs to be done. I'll set the base_url in Elixir tests and revert changes to the API spec YAML file.

Copy link
Contributor

Choose a reason for hiding this comment

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

YAML reverted.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think petstore is running as a docker container in the circle CI env, so keeping this the same would be fine I guess?

Copy link
Contributor

Choose a reason for hiding this comment

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

@wpiekutowski thanks for the further explanation, I absolutely share your opportunities and would also prefer point 2 and highly welcome a PR with the changes of the yaml file and the new clients in a separate PR. Thanks for shedding some light on the issue with HTTPS.

@@ -13,7 +13,7 @@
devShells.default = pkgs.mkShell
{
buildInputs = with pkgs;[
jdk8
jdk11
Copy link
Contributor

Choose a reason for hiding this comment

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

@wing328 the run-in-docker.sh also uses jdk11, so this change looks good to me, any veto against this change in the common area?

Copy link
Contributor

Choose a reason for hiding this comment

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

Exactly. Recent maven versions require JDK >= 11.

@mrmstn mrmstn requested a review from wing328 July 19, 2023 20:59
@mrmstn
Copy link
Contributor

mrmstn commented Jul 20, 2023

Nice, CI goes through. As I'm currently quite in a hurry, I'm excited to merge this PR in the evening, (~ 18:00 UTC)

@wing328
Copy link
Member

wing328 commented Jul 20, 2023

I ran the petstore tests locally but got these errors:

[INFO] --- exec-maven-plugin:1.2.1:exec (test) @ ElixirPetstoreClientTests ---
......

  1) test fetch inventory (StoreTest)
     test/store_test.exs:10
     ** (MatchError) no match of right hand side value: {:error, :econnrefused}
     code: {:ok, inventory} = StoreApi.get_inventory(connection)
     stacktrace:
       test/store_test.exs:11: (test)



  2) test add and delete a pet (PetTest)
     test/pet_test.exs:13
     ** (MatchError) no match of right hand side value: {:error, :econnrefused}
     code: {:ok, %Tesla.Env{} = response} = PetApi.add_pet(connection, pet)
     stacktrace:
       test/pet_test.exs:24: (test)



  3) test find pet by status (PetTest)
     test/pet_test.exs:59
     ** (MatchError) no match of right hand side value: {:error, :econnrefused}
     code: {:ok, listPets} = PetApi.find_pets_by_status(connection, "available")
     stacktrace:
       test/pet_test.exs:60: (test)



  4) test update a pet (PetTest)
     test/pet_test.exs:40
     ** (MatchError) no match of right hand side value: {:error, :econnrefused}
     code: {:ok, response} = PetApi.update_pet(connection, pet)
     stacktrace:
       test/pet_test.exs:50: (test)


Finished in 1.3 seconds (1.3s async, 0.00s sync)
10 tests, 4 failures

Randomized with seed 105401

(Elixir CI tests were run at shippible.io before which is no longer freely available for open-source project if I remember correctly)

@wpiekutowski
Copy link
Contributor

@wing328 Could you share more details in what environment are you running these tests? I'm running these in a NixOS vm using the environment setup from flake.nix.

$ mvn integration-test
# ....
[INFO] --- exec-maven-plugin:1.2.1:exec (test) @ ElixirPetstoreClientTests ---
==> mime
Compiling 1 file (.ex)
Generated mime app
==> jason
Compiling 10 files (.ex)
Generated jason app
==> tesla
Compiling 34 files (.ex)
Generated tesla app
==> erlex
Compiling 2 files (.erl)
Compiling 1 file (.ex)
Generated erlex app
==> dialyxir
Compiling 64 files (.ex)
Generated dialyxir app
==> openapi_petstore
Compiling 57 files (.ex)
Generated openapi_petstore app
......
12:49:10.471 [warning] Description: 'Server authenticity is not verified since certificate path validation is not enabled'
     Reason: 'The option {verify, verify_peer} and one of the options \'cacertfile\' or \'cacerts\' are required to enable this.'


12:49:10.471 [warning] Description: 'Server authenticity is not verified since certificate path validation is not enabled'
     Reason: 'The option {verify, verify_peer} and one of the options \'cacertfile\' or \'cacerts\' are required to enable this.'

....

Finished in 2.1 seconds (2.1s async, 0.00s sync)
10 tests, 0 failures

Randomized with seed 395080
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  16.491 s
[INFO] Finished at: 2023-07-20T12:49:12+02:00
[INFO] ------------------------------------------------------------------------
[INFO] 4 goals, 4 executed

@wing328
Copy link
Member

wing328 commented Jul 20, 2023

Tested again by running mvn integration-test -f samples/client/petstore/elixir/pom.xml at project root folder but still got the same errors:

  1) test fetch inventory (StoreTest)
     test/store_test.exs:10
     ** (MatchError) no match of right hand side value: {:error, :econnrefused}
     code: {:ok, inventory} = StoreApi.get_inventory(connection)
     stacktrace:
       test/store_test.exs:11: (test)



  2) test find pet by status (PetTest)
     test/pet_test.exs:59
     ** (MatchError) no match of right hand side value: {:error, :econnrefused}
     code: {:ok, listPets} = PetApi.find_pets_by_status(connection, "available")
     stacktrace:
       test/pet_test.exs:60: (test)



  3) test add and delete a pet (PetTest)
     test/pet_test.exs:13
     ** (MatchError) no match of right hand side value: {:error, :econnrefused}
     code: {:ok, %Tesla.Env{} = response} = PetApi.add_pet(connection, pet)
     stacktrace:
       test/pet_test.exs:24: (test)



  4) test update a pet (PetTest)
     test/pet_test.exs:40
     ** (MatchError) no match of right hand side value: {:error, :econnrefused}
     code: {:ok, response} = PetApi.update_pet(connection, pet)
     stacktrace:
       test/pet_test.exs:50: (test)


Finished in 0.5 seconds (0.5s async, 0.00s sync)
10 tests, 4 failures

Randomized with seed 154361
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  12.029 s
[INFO] Finished at: 2023-07-20T19:11:12+08:00
[INFO] ------------------------------------------------------------------------
[INFO] 4 goals, 4 executed
[INFO] 
[INFO] Publishing build scan...
[INFO] https://ge.openapi-generator.tech/s/w2h6ffhb6lrcq
[INFO] 

While ruby and php tests are fine, e.g.

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) Pet pet methods should create and get pet with byte array (binary, string)
     # Temporarily skipped with xit
     # ./spec/custom/pet_spec.rb:91

  2) Pet pet methods should get pet in object
     # Temporarily skipped with xit
     # ./spec/custom/pet_spec.rb:110

  3) Store should fetch the inventory in object
     # Temporarily skipped with xit
     # ./spec/custom/store_spec.rb:30

Finished in 2.63 seconds (files took 0.70093 seconds to load)
279 examples, 0 failures, 3 pending

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  8.747 s
[INFO] Finished at: 2023-07-20T19:08:16+08:00
[INFO] ------------------------------------------------------------------------
[INFO] 3 goals, 3 executed
[INFO] 
[INFO] Publishing build scan...
[INFO] https://ge.openapi-generator.tech/s/lxikvugoflhlu

@barttenbrinke
Copy link
Contributor Author

We'll rollback the https change, as it is creating too much confusion.

HTTPS doesn't work for folks who setup petstore.swagger.io as described
in docs/faq-contributing.md.
@wpiekutowski
Copy link
Contributor

@wing328 I've reverted Elixir tests to HTTP and I'm going to use a local docker petstore server or a proxy for local testing.

@wing328
Copy link
Member

wing328 commented Jul 20, 2023

Now all tests passed locally:

Resolving Hex dependencies...
Resolution completed in 0.046s
Unchanged:
  dialyxir 1.3.0
  earmark_parser 1.4.33
  erlex 0.2.6
  ex_doc 0.30.3
  jason 1.4.1
  makeup 1.1.0
  makeup_elixir 0.16.1
  makeup_erlang 0.1.2
  mime 2.0.5
  nimble_parsec 1.3.1
  tesla 1.7.0
All dependencies are up to date
[INFO] 
[INFO] --- exec-maven-plugin:1.2.1:exec (test) @ ElixirPetstoreClientTests ---
..........
Finished in 2.1 seconds (2.1s async, 0.00s sync)
10 tests, 0 failures

Randomized with seed 939827
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  15.003 s
[INFO] Finished at: 2023-07-20T21:03:04+08:00
[INFO] ------------------------------------------------------------------------
[INFO] 4 goals, 4 executed
[INFO] 
[INFO] Publishing build scan...
[INFO] https://ge.openapi-generator.tech/s/wz355faq3gnem
[INFO] 

@wing328
Copy link
Member

wing328 commented Jul 20, 2023

FYI. We're moving away from petstore to echo server for client tests. https://github.com/OpenAPITools/openapi-generator/wiki/Integration-Tests#echo-server has more information. (not required to add it as part of this PR).

@wpiekutowski
Copy link
Contributor

Interesting. In case I commit myself one day to switching Elixir to the echo server, should I delete the petstore Elixir client sample?

@barttenbrinke
Copy link
Contributor Author

barttenbrinke commented Jul 20, 2023

Looks like you need to record the response first? So it is like ExVCR? Ah, there is the global yaml for that.

@wing328
Copy link
Member

wing328 commented Jul 20, 2023

Interesting. In case I commit myself one day to switching Elixir to the echo server, should I delete the petstore Elixir client sample?

For python, java, etc, we keep both petstore and echo client for the time being as echo client doesn't cover file upload yet (we can for sure easily add an endpoint for that in echo server spec)

After the echo client fully covers all the test cases (operations, models) in the petstore client, please feel free to remove the petstore client (elixir).

@wing328
Copy link
Member

wing328 commented Jul 20, 2023

Looks like you need to record the response first? So it is like ExVCR? Ah, there is the global yaml for that.

Basically you get back what the client sends to the server in the response body. Please give it a try when you've time and I hope you like this approach for testing the client.

@mrmstn mrmstn merged commit 4ece8e9 into OpenAPITools:master Jul 20, 2023
16 checks passed
@mrmstn
Copy link
Contributor

mrmstn commented Jul 20, 2023

Hi @wpiekutowski, @barttenbrinke

I've just merged the PR. Thank you for your excellent work and for addressing the necessary changes! Your contribution has made a significant impact on our project.

These updates will be incorporated into the upcoming 7.0.0 release, scheduled for the 21st of July.

A big thanks to everyone who played a role in this contribution. Your efforts are greatly appreciated!

@wing328
Copy link
Member

wing328 commented Jul 21, 2023

scheduled for the 21st of July.

likely we need postpone that as there are other enhancements (dart, ruby) that we want to include in 7.0.0 release

@barttenbrinke
Copy link
Contributor Author

Hey @mrmstn @wing328 ,

thank you for your work in building and maintaining this openapi-generator. It has been an absolute lifesaver for consuming large scale API's in Elixir like the Zuora.

Thanks for the tooling, your efforts are also greatly appreciated!

@wpiekutowski
Copy link
Contributor

Thanks for working with us on merging this. I think I might contribute some more stuff thanks to your supportive attitude.

@wing328
Copy link
Member

wing328 commented Jul 22, 2023

@barttenbrinke @wpiekutowski

Thank you for your contributions too. This project wouldn't be a success without contributors like you guys. I'm sure the Elixir community appreciates all the efforts you guys have put into the Elixir client generator.

Look forward to more PRs to further improve openapi-generator.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants