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

[BUG] v7.0.0 Elixir Deserialization Regression #16412

Open
5 of 6 tasks
tobbbles opened this issue Aug 25, 2023 · 11 comments
Open
5 of 6 tasks

[BUG] v7.0.0 Elixir Deserialization Regression #16412

tobbbles opened this issue Aug 25, 2023 · 11 comments

Comments

@tobbbles
Copy link
Contributor

tobbbles commented Aug 25, 2023

Bug Report Checklist

  • Have you provided a full/minimal spec to reproduce the issue?
  • Have you validated the input using an OpenAPI validator (example)?
  • Have you tested with the latest master to confirm the issue still exists?
  • Have you searched for related issues/PRs?
  • What's the actual output vs expected output?
  • [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description

#16061 appears to have introduced a bug when attempting to deserialize a string into an enum model.

    ** (FunctionClauseError) no function clause matching in Ory.Deserializer.to_struct/2
        (ory_client 1.1.50) lib/ory/deserializer.ex:97: Ory.Deserializer.to_struct("aal1", Ory.Model.AuthenticatorAssuranceLevel)

Here we see to_struct/2 being called on the value of "aal1", to be decoded into the Ory.Model.AuthenticatorAssuranceLevel model. However there is no implementation from this.

This is a regression from the prior generators, and the expected output is to be able to deserialize into these modules/structs.

openapi-generator version

v7.0.0

OpenAPI declaration file content or url

Any string-typed enum field, for example this from the Ory SDK

      "authenticatorAssuranceLevel": {
        "description": "The authenticator assurance level can be one of \"aal1\", \"aal2\", or \"aal3\". A higher number means that it is harder\nfor an attacker to compromise the account.\n\nGenerally, \"aal1\" implies that one authentication factor was used while AAL2 implies that two factors (e.g.\npassword + TOTP) have been used.\n\nTo learn more about these levels please head over to: https://www.ory.sh/kratos/docs/concepts/credentials",
        "enum": [
          "aal0",
          "aal1",
          "aal2",
          "aal3"
        ],
        "title": "Authenticator Assurance Level (AAL)",
        "type": "string"
      },

Generation Details

The above spec will output the following model module.

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

defmodule Ory.Model.AuthenticatorAssuranceLevel do
  @moduledoc """
  The authenticator assurance level can be one of \"aal1\", \"aal2\", or \"aal3\". A higher number means that it is harder for an attacker to compromise the account.  Generally, \"aal1\" implies that one authentication factor was used while AAL2 implies that two factors (e.g. password + TOTP) have been used.  To learn more about these levels please head over to: https://www.ory.sh/kratos/docs/concepts/credentials
  """

  @derive Jason.Encoder
  defstruct [
    
  ]

  @type t :: %__MODULE__{
    
  }

  def decode(value) do
    value
  end
end
Steps to reproduce

Generate any OpenAPI schema with an enum string model and you will get this. For further testing using the Ory SDK, you can checkout my fork and generate the elixir client by commenting out lines 235-333 in scripts/generate.sh and running $ FORCE_PROJECT=client FORCE_VERSION=(cat spec/client/latest) ./scripts/generate.sh to invoke the openapi-generator-cli to generate the client at clients/client/elixir/.

Related issues/PRs

#16061 Introduced this regression

Suggest a fix.

Implement a way to deserialize enum modules. Ideally this would go further and implement a word list to validate whether a string value is a valid enum when trying to deserialize into the

@tobbbles
Copy link
Contributor Author

@barttenbrinke As the Jason->Poison author and the creator of the type decoding magic, any ideas?

@tobbbles
Copy link
Contributor Author

tobbbles commented Jan 7, 2024

This is still broken on the new implementation and blocking usage of openapi-generator in Elixir for any string enum components.

@barttenbrinke
Copy link
Contributor

@tobbbles Sorry, this only just appeared in my inbox for some reason /cc @wpiekutowski

@barttenbrinke
Copy link
Contributor

barttenbrinke commented Jan 8, 2024

TLDR - Swagger and thus the openapi generator sees the Enum as a authenticatorAssuranceLevel struct, while it basically is just a string. As it is not really doable to handle enums in a struct like this in Elixir, just returning the string might just be the simplest solution here.

We need a simpler example than the whole ORY library

@tobbbles
Copy link
Contributor Author

tobbbles commented Jan 13, 2024

Hi @barttenbrinke, thanks for looking around to this. I've made a couple of attempts to fix or investigate this but I don't get any further. What I find very weird is I'm only seeing this behaviour when the string enum is a component/outer enum

This is currently existing in the petstore sample and the generated Elixir client in the repository.

The pet store 3_0 enum test:

Enum_Test:
type: object
required:
- enum_string_required
properties:
enum_string:
type: string
enum:
- UPPER
- lower
- ''
enum_string_required:
type: string
enum:
- UPPER
- lower
- ''
enum_integer:
type: integer
format: int32
enum:
- 1
- -1
enum_number:
type: number
format: double
enum:
- 1.1
- -1.2
outerEnum:
$ref: '#/components/schemas/OuterEnum'
outerEnumInteger:
$ref: '#/components/schemas/OuterEnumInteger'
outerEnumDefaultValue:
$ref: '#/components/schemas/OuterEnumDefaultValue'
outerEnumIntegerDefaultValue:
$ref: '#/components/schemas/OuterEnumIntegerDefaultValue'

generates the following module:
https://github.com/OpenAPITools/openapi-generator/blob/master/samples/client/petstore/elixir/lib/openapi_petstore/model/enum_test.ex

Which here we can see there is no issue with enum_string being just a type of String.t; however the outer enum is generated into a module as seen with the Ory example https://github.com/OpenAPITools/openapi-generator/blob/master/samples/client/petstore/elixir/lib/openapi_petstore/model/outer_enum.ex

As it is not really doable to handle enums in a struct like this in Elixir

Would we instead aim to infer the type of out enums and use those as the base type for these fields, compared to generating modules for outer enums? On one hand I like that a module could be generated with a word list for valid enum values; however like you say Elixir doesn't lend itself well to this style of enum.

@tobbbles
Copy link
Contributor Author

I believe adding the following test case will highlight this:

defmodule EnumTest do
  use ExUnit.Case, async: true
  alias OpenapiPetstore.Deserializer
  alias OpenapiPetstore.Model.EnumTest

  @valid_json """
  {
    "enum_string": "UPPER",
    "outerEnum": "placed"

  }
  """

  test "jason_decode/2 with valid JSON" do
    assert Deserializer.jason_decode(@valid_json, EnumTest) ==
             {:ok,
              %EnumTest{
                enum_string: "UPPER",
                outerEnum: "placed"
              }}
  end
end

With the output:

$ mix test
...


  1) test jason_decode/2 with valid JSON (EnumTest)
     test/enum_test.exs:14
     ** (FunctionClauseError) no function clause matching in OpenapiPetstore.Deserializer.to_struct/2

     The following arguments were given to OpenapiPetstore.Deserializer.to_struct/2:
     
         # 1
         "placed"
     
         # 2
         OpenapiPetstore.Model.OuterEnum
     
     Attempted function clauses (showing 3 out of 3):
     
         defp to_struct(nil, _)
         defp to_struct(list, module) when is_list(list) and is_atom(module)
         defp to_struct(map, module) when is_map(map) and is_atom(module)
     
     code: assert Deserializer.jason_decode(@valid_json, EnumTest) ==
     stacktrace:
       (openapi_petstore 1.0.0) lib/openapi_petstore/deserializer.ex:97: OpenapiPetstore.Deserializer.to_struct/2
       (elixir 1.15.7) lib/map.ex:916: Map.update!/3
       (openapi_petstore 1.0.0) lib/openapi_petstore/model/enum_test.ex:36: OpenapiPetstore.Model.EnumTest.decode/1
       (openapi_petstore 1.0.0) lib/openapi_petstore/deserializer.ex:19: OpenapiPetstore.Deserializer.jason_decode/2
       test/enum_test.exs:15: (test)


@binajmen
Copy link

binajmen commented Jul 1, 2024

@barttenbrinke @tobbbles, apologies for the ping. Is this bug being tracked and worked on? I'm having issues verifying sessions in my Elixir project, which renders the Ory client unusable.

 Frontend.to_session(Ory.Connection.new(), Cookie: cookies(conn))

 [error] ** (FunctionClauseError) no function clause matching in Ory.Deserializer.to_struct/2

@barttenbrinke
Copy link
Contributor

@binajmen The core issue is that there is no simple test / example that reproduces the issue at the moment. All enum tests work in the OpenAPITools suite and the simple test @tobbbles made also succeeds. I am not working on it atm, if somebody creates a test / example that reproduces the error I would be able to fix it quite quickly I think.

@binajmen
Copy link

binajmen commented Jul 3, 2024

I would love to help, but I'm clueless about where to start. @tobbbles, is there a reason why your test succeeded with the OpenAPITools suite?

@tobbbles
Copy link
Contributor Author

Hi folks, finally got some vacation time to circle back on this. Are we sure the example I posted succeeds? With a fresh checkout I have the following; however in #19435 where I introduce this test case, it looks like the CI is not executing it.

Could someone check to reproduce as I have please?

$ cat >samples/client/petstore/elixir/test/issue.exs<<EOF

defmodule IssueTest do
  use ExUnit.Case, async: true

  alias OpenapiPetstore.Deserializer
  alias OpenapiPetstore.Model.EnumTest

  @valid_json """
  {
    "enum_string": "UPPER",
    "outerEnum": "placed"
  }
  """

  test "jason_decode/2 with valid JSON" do
    assert Deserializer.jason_decode(@valid_json, EnumTest) ==
             {:ok,
              %EnumTest{
                enum_string: "UPPER",
                outerEnum: "placed"
              }}
  end
end

EOF 

$ cd samples/client/petstore/elixir

$ mix deps.get
...

$ mix test test/issue.exs
Running ExUnit with seed: 24428, max_cases: 64



  1) test jason_decode/2 with valid JSON (IssueTest)
     test/issue.exs:14
     ** (FunctionClauseError) no function clause matching in OpenapiPetstore.Deserializer.to_struct/2

     The following arguments were given to OpenapiPetstore.Deserializer.to_struct/2:

         # 1
         "placed"

         # 2
         OpenapiPetstore.Model.OuterEnum

     Attempted function clauses (showing 3 out of 3):

         defp to_struct(nil, _)
         defp to_struct(list, module) when is_list(list) and is_atom(module)
         defp to_struct(map, module) when is_map(map) and is_atom(module)

     code: assert Deserializer.jason_decode(@valid_json, EnumTest) ==
     stacktrace:
       (openapi_petstore 1.0.0) lib/openapi_petstore/deserializer.ex:97: OpenapiPetstore.Deserializer.to_struct/2
       (elixir 1.17.2) lib/map.ex:916: Map.update!/3
       (openapi_petstore 1.0.0) lib/openapi_petstore/model/enum_test.ex:36: OpenapiPetstore.Model.EnumTest.decode/1
       (openapi_petstore 1.0.0) lib/openapi_petstore/deserializer.ex:19: OpenapiPetstore.Deserializer.jason_decode/2
       test/issue.exs:15: (test)


Finished in 0.05 seconds (0.05s async, 0.00s sync)
1 test, 1 failure

@tobbbles
Copy link
Contributor Author

I think the main root issue here is the generated enum modules are just empty struct,

iex(3)> OpenapiPetstore.Model.OuterEnum.__struct__
%OpenapiPetstore.Model.OuterEnum{}

Which is more or less unusable.

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

No branches or pull requests

3 participants