coverghoul

coverghoul

Hey Ash Community,

I found ash and the json api and immediately thought of robust documenting APIs like fastapi in python, or spring in java. I’ve been playing with it, and for resources I’m backing into postgres this makes total sense/works. I’m trying to sent up a “calculation only” api (a bunch of recursive math calls), that I don’t need to store into the DB, this is what I came up with:

The domain –


defmodule App.Math do
  @moduledoc """
  This establishes our ash domain so that we track what is in scope
  """
  use Ash.Domain, extensions: [AshJsonApi.Domain]

  resources do
    resource App.Math.Answer
  end


  json_api do
    routes do
      # in the domain `base_route` acts like a scope
      base_route "/math/fibonacci", App.Math.Answer do
        post :calculate_fibonacci
      end
    end
  end
end

The resource –

defmodule App.Math.Answer do
  @moduledoc """
  This will showcase an API endpoint that does not store into the database,
  but instead does a calculation and returns to the user this value
  """
  use Ash.Resource,
    domain: App.Math,
    extensions: [AshJsonApi.Resource]

  attributes do
    attribute :problem, :string
    attribute :answer, :integer
  end

  resource do
    require_primary_key? false
  end

  defp calc_fibonacci(0) do
    [0 | 0]
  end
  defp calc_fibonacci(1) do
    [1 | 0]
  end
  defp calc_fibonacci(depth) when is_integer(depth) and depth >= 0 do
    [head | tail] = calc_fibonacci(depth - 1)
    [head + tail | head]
  end

  def calculate_fibonacci(depth) when is_integer(depth) and depth >= 0 do
    hd(calc_fibonacci(depth))
  end

  json_api do
    type "answer"
  end

  actions do
    action :calculate_fibonacci, :struct do
      constraints instance_of: __MODULE__
      argument :depth, :integer do
        constraints  [min: 0]
        allow_nil? false
      end

      run fn input, _context ->
        {:ok, %{problem: "fibonacci", answer: calculate_fibonacci(input.arguments.depth)}}
      end
    end
  end


end

When I post to this with -

{
  "data": {
    "attributes": {
      "depth": 3
    }
  }
}

I get -

[error] ** (ArgumentError) Could not determine a resource from the provided input: %{answer: 2, problem: "fibonacci"}

I could be approaching this the wrong way (and would welcome that feedback/a steer in the right direction), but was hopeful to have something that takes advantage of the swagger/redoc generation ash-json has. Thanks for reading this.

Paul

Showing Posts 1 to 8

zachdaniel

zachdaniel

Creator of Ash

When you use post with a generic action it expects the generic action to respond with an instance of the resource. So you’d return struct(__MODULE__, your_data)

coverghoul

coverghoul OP

Hey Zach,

Thanks for taking the time to help.

I did what you suggested and it started insisting i need a primary key despite the resource non-primary key I had above, and also that read must be implemented so I changed it to the following:

  attributes do
    uuid_primary_key :id
    attribute :problem, :string do
      allow_nil? false
    end
    attribute :answer, :integer do
      allow_nil? false
    end
  end

And I added to the function –

  actions do
    defaults [:read]
    action :calculate_fibonacci, :struct do
      constraints instance_of: __MODULE__
      argument :argument, :integer do
        constraints  [min: 0]
        allow_nil? false
      end

      run fn input, _context ->
        answer = calculate_fibonacci(input.arguments.argument)
        answer_struct = struct(__MODULE__, %{id: Ecto.UUID.generate(), problem: "fibonacci", answer: answer})
        IO.puts("The struct is - id: #{answer_struct.id} problem: #{answer_struct.problem} answer: #{answer_struct.answer}")
        {:ok, answer_struct}
      end
    end
  end

When posting with –

{
  "data": {
    "attributes": {
      "argument": 10
    }
  }
}

This now returns a 200 and –

{
  "data": {
    "attributes": {},
    "id": "c86e847f-68ad-4907-a846-75fdad5b8eb2",
    "links": {},
    "meta": {},
    "type": "answer",
    "relationships": {}
  },
  "links": {
    "self": "http://localhost:4000/api/json/math/fibonacci"
  },
  "meta": {},
  "jsonapi": {
    "version": "1.0"
  }
}

But logs –

[debug] Processing with AppWeb.AshJsonApiRouter
    Parameters: %{"data" => %{"attributes" => %{"argument" => 10}}}
    Pipelines: [:api]
    The struct is - id: c86e847f-68ad-4907-a846-75fdad5b8eb2 problem: fibonacci answer: 55

Any thoughts? Or am I doing a non-storage based API completely wrong with ash?

Thanks,
Paul

zachdaniel

zachdaniel

Creator of Ash

Ah, right. So in this case since you’re not using something that has a primary key (i.e non-storage backed as you say), your best bet is not to use routes like post and get, as those are meant to following specific patterns described here https://jsonapi.org

Spec compliant JSON:API requires ids. However, we provide a facility for doing generic routes that don’t have to strictly follow those formats, which looks like this:

  defmodule Bar do
    use Ash.Resource, domain: nil, extensions: AshJsonApi.Resource

    json_api do
      type "bar"

      routes do
        post :say_hello
      end
    end

    resource do
      require_primary_key?(false)
    end

    attributes do
      attribute(:message, :string, allow_nil?: false)
    end

    actions do
      action :say_hello, :struct do
        constraints(instance_of: __MODULE__)
        argument(:name, :string, allow_nil?: false)

        run(fn input, _ ->
          {:ok, "Hello, #{input.arguments.name}!"}
        end)
      end
    end
  end

What you can additionally do to simplify is something like this:

defmodule MathAnswer do
  destruct [:question, :answer]

  use Ash.Type.NewType, subtype_of: :struct, 
    constraints: [
      instance_of: __MODULE__,
      fields: [
        question: [type: :string, allow_nil?: false],
        answer: [type: :integer, allow_nil?: false]
      ]
    ]
end

Then you can simplify the resource to require no state/attributes.

defmodule App.Math.Answer do
  @moduledoc """
  This will showcase an API endpoint that does not store into the database,
  but instead does a calculation and returns to the user this value
  """
  use Ash.Resource,
    domain: App.Math,
    extensions: [AshJsonApi.Resource]

  actions do
    action :calculate_fibonacci, MathAnswerdo
      argument :depth, :integer do
        constraints  [min: 0]
        allow_nil? false
      end

      run fn input, _context ->
        {:ok, %MathAnswer{problem: "fibonacci", answer: calculate_fibonacci(input.arguments.depth)}}
      end
    end
  end

  defp calc_fibonacci(0) do
    [0 | 0]
  end
  defp calc_fibonacci(1) do
    [1 | 0]
  end
  defp calc_fibonacci(depth) when is_integer(depth) and depth >= 0 do
    [head | tail] = calc_fibonacci(depth - 1)
    [head + tail | head]
  end

  def calculate_fibonacci(depth) when is_integer(depth) and depth >= 0 do
    hd(calc_fibonacci(depth))
  end
end

And you can then use route/3 to define the route:

        route :post, "/", :calculate_fibonacci
coverghoul

coverghoul OP

Thanks for the reply, due to my new born haven’t had the time to try implementing. Hopefully he settles down tonight and I’ll be able to let you know how it goes. Thanks you again for all the help.

coverghoul

coverghoul OP

Hey Zach,

So this worked, I get a return with the right depth, the arguments well defined etc. Thank you for your help! I was debating about typing this up as a tutorial for your docs if you’d accept that PR.

While debating about steps to present it, I found that the return type isn’t displayed it just says “string” and the mathanswer isn’t a type in the swagger definitions. Is there a way to override openapi types on a specific route without doing it at the router level?

Another question would be, if we wanted to host both open_api_spex and ash routers, is there a clean way to combine their auto-generated api docs? I think that would be useful to present to people as an “escape hatch/advanced use cases” thing.

Thanks,
Paul

zachdaniel

zachdaniel

Creator of Ash

In your AshJsonApi router, you can do:

    modify_open_api: {__MODULE__, :modify_open_api, []}

and then you could add your custom open api spex code to our generated one, and that would effectively be merging them.

As for showing string, I’ve pushed a fix to main of ash_json_api which should address this and show proper type information.

Bumbus

Bumbus

Actually I got stuck with this:

defmodule My.Guests do
  use Ash.Domain,
    otp_app: :my,
    extensions: [AshJsonApi.Domain]

  json_api do
    routes do
      base_route "/cards", My.Guests.Card do
        route :get, "/test", :test
      end
    end
  end

  resources do
    resource My.Guests.Card do
      define :test, action: :test
    end
  end
end


defmodule My.Guests.Card do
  use Ash.Resource,
    otp_app: :my,
    domain: My.Guests,
    data_layer: Ash.DataLayer.Simple,
    extensions: [
      AshJsonApi.Resource
    ]

  json_api do
    type "guests_card"
    derive_filter? false
  end

  # resource do
  #   require_primary_key? false
  # end

  actions do
    defaults [:create, :read, :destroy]
    action :test, {:array, :struct} do
      constraints items: [instance_of: __MODULE__]

      run fn input, _ ->
        {:ok, [struct(__MODULE__, %{id: Ecto.UUID.generate(), name: "WTF"})]}
      end
    end
  end

  attributes do
    uuid_primary_key :id
    attribute :name, :string, default: "HELLO"
  end
end

GET requests to `http://localhost:4000/api/json/cards/test\` keep returning 404 - no_route_found.

Help please!

Bumbus

Bumbus

Hmm, damn I forgot to add the domain in `AshJsonApiRouter` :grimacing:

— All posts loaded —

Where Next? Top

Trending in Questions Top

Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews