npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

skir-elixir-gen

v0.1.3

Published

Native Elixir code generation for Skir schemas

Readme

npm Hex HexDocs build

skir-elixir-gen — native Elixir code generation for Skir

An unofficial Skir compiler plugin that emits native Elixir structs, enums, codecs, constants, reflection and RPC helpers. Generated code uses the separate skir_elixir_client Hex package.

Set up

Requires Node 20+ for generation, and Elixir 1.18+ with Erlang/OTP 27+ for the runtime. The runtime uses built-in JSON and has no production dependencies.

Add the runtime dependency to your application's mix.exs:

{:skir_elixir_client, "~> 0.2.1"}

The Hex package is skir_elixir_client; its Mix application is :skir_elixir_client, and its public modules use Skir.*. Run mix deps.get.

Install the generator alongside the Skir compiler:

npm install --save-dev skir skir-elixir-gen

Add this entry under generators in your skir.yml:

generators:
  - mod: skir-elixir-gen
    outDir: ./lib/skirout
    config:
      namespace: MyApp.Protocol

Run npx skir gen before mix compile. Node is required for generation, not for running the generated Elixir code.

namespace is the only generator option. It defaults to Skir.Generated and must be an Elixir module namespace.

Elixir generated-code guide

The examples below use the example schema with namespace: Example.Protocol. Follow the same pattern with your own namespace.

Referring to generated symbols

Each source file has a module for its constants and RPC helpers, with nested modules for its records:

alias Example.Protocol.UserSkir
alias Example.Protocol.UserSkir.{User, Event}

# A nested User.Pet record has its own module.
alias Example.Protocol.UserSkir.User.Pet

GitHub schema dependencies use Skir's normal dependencies configuration, including transitive imports. An import from @acme/shared-models/accounts/user.skir generates MyApp.Protocol.External.Acme.SharedModels.Accounts.UserSkir.User in external/acme/shared_models/accounts/user_skir.ex. Numeric or punctuation-only owner/repository names gain an N prefix in Elixir module names. The original import path remains in reflection IDs. Names that normalize to the same module or output path are rejected, including collisions with local external/... schemas.

Struct types

Skir structs become native Elixir structs. new/1 accepts a map or keyword list; omitted fields use schema defaults.

user = User.new(id: 42, name: "Alice")
%User{id: 42, name: "Alice"} = user

%User{id: 0, name: ""} = User.default()
from_map = User.new(%{id: 43, name: "Bob"})

new/1 rejects unknown attributes. Field values are validated when serializing; typespecs do not enforce them at construction time.

Creating modified copies

Structs are immutable. Use Elixir's update syntax to copy a value with changes:

updated = %{user | name: "Bob"}
"Alice" = user.name
"Bob" = updated.name

Optional and array fields

Optional fields use nil or the underlying value. Arrays use lists, including lists of nested generated records:

nil = User.default().nickname
family = User.new(nickname: "Al", pets: [Pet.new(name: "Mo")])
"Al" = family.nickname
[%Pet{name: "Mo"}] = family.pets

Keyed arrays also generate index_<field>/1 helpers. These return a map and raise ArgumentError on duplicate keys. See native API examples for nested key paths and enum-kind indexes.

Enum types

Constant variants are atoms; variants carrying a value are tuples. Every enum also has an implicit :unknown default:

connected = :connected
message = {:message, "Hello"}
event = {:user_created, user}
:unknown = Event.default()

Use ordinary Elixir pattern matching:

case event do
  :connected -> "Connected"
  {:user_created, %User{name: name}} -> name
  {:message, text} -> text
  :unknown -> "Unknown event"
  {:unknown, _metadata} -> "Preserved future event"
end

The tuple form of an unknown variant is used when explicitly preserving future values. See the runtime codec guide for schema evolution and unknown-field policies.

Serialization

# JSON terms and JSON strings are separate APIs.
[42, 0, "Alice"] = User.to_json!(user)
{:ok, json} = User.encode_json(user)
{:ok, ^user} = User.decode_json(json)

# Framed Skir binary, not Erlang external-term format.
{:ok, binary} = User.encode(user)
{:ok, ^user} = User.decode(binary)

# Pure Elixir sum types: atoms for constants, tuples for payloads.
{:ok, event_binary} = Event.encode(event)
{:ok, {:user_created, %User{id: 42}}} = Event.decode(event_binary)

# Debug representation; do not use readable JSON as a rename-safe contract.
%{"id" => 42, "name" => "Alice"} = User.to_json!(user, format: :readable)

The zero between id and name is the example schema's permanently removed field 1. It is not an inferred field position.

Every generated record module has default/0, type/0, and these function pairs, each accepting keyword options:

User.to_json(value)       # {:ok, JSON-compatible Elixir term}
User.from_json(term)      # {:ok, %User{}}
User.encode_json(value)   # {:ok, UTF-8 JSON binary}
User.decode_json(json)    # {:ok, %User{}}
User.encode(value)        # {:ok, framed Skir binary}
User.decode(binary)      # {:ok, %User{}}; JSON bytes are also accepted
# The same names ending in ! return the value or raise Skir.Error.

Non-bang serialization functions return {:error, %Skir.Error{reason: atom, path: [...], message: string}} on validated input failures. Unexpected programming errors are not swallowed. Struct modules additionally expose new/1.

See the runtime codec guide for primitive mappings, recursive defaults, encoding rules and resource limits, and the runtime API reference for serializing primitive and composite type handles directly.

Constants

Constants are zero-arity functions on the source-file module:

alias Example.Protocol.UserSkir
alice = UserSkir.alice_const()

SkirRPC services

The same source module also exposes native SkirRPC helpers. For the example GetUser method:

alias Example.Protocol.UserSkir
alias Example.Protocol.UserSkir.User
alias Skir.RPC.{Service, ServiceClient}

service =
  Service.new()
  |> UserSkir.add_get_user(fn id, _metadata ->
    {:ok, %User{id: id, name: "Alice"}}
  end)

client = ServiceClient.new!("https://api.example.com/rpc")
{:ok, %User{}} = UserSkir.get_user(client, 42)
%User{} = UserSkir.get_user!(client, 42)

Phoenix/Plug applications can mount Skir.RPC.Plug with forward "/rpc", Skir.RPC.Plug, service: &MyApp.RPC.service/0. Studio (?studio), method reflection (?list), controlled/unknown errors, request metadata, the compact SkirRPC HTTP wire format, and a pluggable client transport are implemented. See the client RPC guide for the complete API and the client parity audit for the official-runtime parity audit.

Reflection

Inspect any generated type with Skir.RPC.TypeDescriptor:

descriptor = Skir.RPC.TypeDescriptor.to_map(User.type())
json = Skir.RPC.TypeDescriptor.to_json(User.type())

Descriptors include nested types, field numbers, enum variants, documentation, and removed slots. User.type/0 is the public type handle; schema/0 is internal codec metadata. See the runtime API reference.

Tests and benchmarks

npm ci
mix local.hex
mix local.rebar
npm run test:all

test:all runs syntax, lint, formatting, coverage, compiler/runtime integration, malformed-input and npm package checks. Use npm test for just the Node generator tests.

Run codec benchmarks separately:

npm run test:benchmark

This builds an OTP release and measures binary and JSON encoding/decoding. Five-sample median throughput and environment details are saved to .artifacts/release-benchmark.json.

See also