Skip to content

Instantly share code, notes, and snippets.

@IvanRublev
Last active November 22, 2022 22:04
Show Gist options
  • Save IvanRublev/d6c77c64f7c94976828d606c5fcfe6d8 to your computer and use it in GitHub Desktop.
Save IvanRublev/d6c77c64f7c94976828d606c5fcfe6d8 to your computer and use it in GitHub Desktop.

Nestru example for Lisbon Elixir meetup at 23 Nov 2022

Mix.install(
  [
    {:nestru, "~> 0.3.2"},
    {:jason, "~> 1.3"},
    {:ex_json_schema, "~> 0.9.2"},
    {:ecto_sql, "~> 3.7"},
    {:postgrex, "~> 0.16.1"},
    {:exjsonpath, "~> 0.9.0"}
  ],
  consolidate_protocols: false
)

defmodule Repo do
  use Ecto.Repo, adapter: Ecto.Adapters.Postgres, otp_app: :my_app
end

Application.put_env(:my_app, Repo,
  username: "postgres",
  password: "postgres",
  # Please, create the database if it does not exists
  database: "nestru_example",
  hostname: "localhost",
  port: 5432,
  pool_size: 5
)

pid = Repo.start_link()

Repo.query!("""
CREATE TABLE IF NOT EXISTS squads (
    id SERIAL PRIMARY KEY,
    squad_name text,
    formed integer,
    secret_base text,
    active boolean
)
""")

Repo.query!("CREATE UNIQUE INDEX IF NOT EXISTS squads_pkey ON squads(id int4_ops)")

Repo.query!("""
CREATE TABLE IF NOT EXISTS super_heroes (
    id SERIAL PRIMARY KEY,
    name text,
    age integer,
    secret_identity text,
    powers text[],
    squad_id integer REFERENCES squads(id) ON DELETE CASCADE
)
""")

Repo.query!("CREATE UNIQUE INDEX IF NOT EXISTS super_heroes_pkey ON super_heroes(id int4_ops)")

pid

Library

Run in Livebook

https://github.com/IvanRublev/Nestru

Input JSON

payload =
  """
  {
    "squad_name": "Super hero squad",
    "home_town": "Metro City",
    "formed": 2016,
    "secret_base": "Super tower",
    "active": true,
    "members": [
      {
        "name": "Molecule Man",
        "age": 29,
        "secretIdentity": "Dan Jukes",
        "powers": [
          "Radiation resistance",
          "Turning tiny",
          "Radiation blast"
        ]
      },
      {
        "name": "Madame Uppercut",
        "age": 39,
        "secretIdentity": "Jane Wilson",
        "powers": [
          "Million tonne punch",
          "Damage resistance",
          "Superhuman reflexes"
        ]
      },
      {
        "name": "Eternal Flame",
        "age": 1000000,
        "secretIdentity": "Unknown",
        "powers": [
          "Immortality",
          "Heat Immunity",
          "Inferno",
          "Teleportation",
          "Interdimensional travel"
        ]
      }
    ]
  }
  """
  |> Jason.decode!()
~s({"$schema":"http://json-schema.org/draft-04/schema#","type":"object","properties":{"squad_name":{"type":"string"},"home_town":{"type":"string"},"formed":{"type":"integer"},"secret_base":{"type":"string"},"active":{"type":"boolean"},"members":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"integer"},"secretIdentity":{"type":"string"},"powers":{"type":"array","items":{"type":"string"}}},"required":["name","age","secretIdentity","powers"]}}},"required":["squad_name","home_town","formed","secret_base","active","members"]})
|> Jason.decode!()
|> ExJsonSchema.Schema.resolve()
|> ExJsonSchema.Validator.validate(payload)
payload_beta =
  """
  {
    "heroes": [
      {
        "name": "Rorschach",
        "age": 35,
        "secretIdentity": "Walter Joseph Kovacs",
        "powers": ["curiosity"]
      }
    ]
  }
  """
  |> Jason.decode!()
~s({"$schema":"http://json-schema.org/draft-04/schema#","type":"object","properties":{"heroes":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"integer"},"secretIdentity":{"type":"string"},"powers":{"type":"array","items":{"type":"string"}}},"required":["name","age","secretIdentity","powers"]}}},"required":["heroes"]})
|> Jason.decode!()
|> ExJsonSchema.Schema.resolve()
|> ExJsonSchema.Validator.validate(payload_beta)
payload_alpha =
  """
  {
    "a":{
        "very":{
          "nested":{
              "heroes":[
                {
                    "name":"Rorschach",
                    "age":35,
                    "secretIdentity":"Walter Joseph Kovacs",
                    "powers":[
                      "curiosity"
                    ]
                }
              ]
          }
        }
    }
  }
  """
  |> Jason.decode!()

Application Model

import Ecto.Changeset

defmodule Squad do
  use Ecto.Schema

  schema "squads" do
    field(:squad_name, :string)
    field(:formed, :integer)
    field(:secret_base, :string)
    field(:active, :boolean)
    has_many(:members, SuperHero)
  end

  # def changeset(changeset \\ %__MODULE__{}, params) do
  #   keys = __schema__(:fields) -- __schema__(:primary_key)
  #   changeset
  #   |> cast(params, keys)
  #   |> validate_required(keys)
  #   |> cast_assoc(:members)
  # end
end

defmodule SuperHero do
  use Ecto.Schema

  schema "super_heroes" do
    field(:name, :string)
    field(:age, :integer)
    field(:secret_identity, :string)
    field(:powers, {:array, :string})
    belongs_to(:squad, Squad)
  end

  # def changeset(changeset \\ %__MODULE__{}, params) do
  #   keys = __schema__(:fields) -- ([:squad, :squad_id] ++ __schema__(:primary_key))
  #   changeset
  #   |> cast(params, keys)
  #   |> validate_required(keys)
  #   |> validate_length(:powers, min: 1)
  # end
end
require Protocol

defimpl Nestru.PreDecoder, for: Squad do
  def gather_fields_from_map(_value, :alpha, map) do
    with {:ok, members} <- ExJSONPath.eval(map, "$.a.very.nested.heroes.*") do
      {:ok,
       %{
         squad_name: "A squad (from alpha)",
         formed: 2000,
         secret_base: "Unknown",
         active: true,
         members: members
       }}
    end
  end

  def gather_fields_from_map(_value, :beta, map) do
    {:ok,
     %{
       squad_name: "A squad (from beta)",
       formed: 2000,
       secret_base: "Unknown",
       active: true,
       members: Map.get(map, "heroes")
     }}
  end

  def gather_fields_from_map(_value, _context, map) do
    {:ok, map}
  end
end

Protocol.derive(Nestru.Decoder, Squad, hint: %{members: [SuperHero]})

Protocol.derive(Nestru.PreDecoder, SuperHero, translate: %{"secretIdentity" => :secret_identity})
Protocol.derive(Nestru.Decoder, SuperHero)

Protocol.derive(Nestru.Encoder, Squad, except: [:__meta__])
Protocol.derive(Nestru.Encoder, SuperHero, except: [:__meta__, :squad, :squad_id])
squad = Nestru.decode_from_map!(payload, Squad)
squad_beta = Nestru.decode_from_map!(payload_beta, Squad, :beta)
squad_alpha = Nestru.decode_from_map!(payload_alpha, Squad, :alpha)
Repo.insert!(squad)
Repo.insert!(squad_beta)
squads = Squad |> Repo.all() |> Repo.preload(:members)
map = Nestru.encode_to_list_of_maps!(squads)
map |> Jason.encode!() |> IO.puts()

Links

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