Skip to content

Commit b1d8bbc

Browse files
authored
Release v1.0.0 (#1)
1 parent 5b09e5f commit b1d8bbc

17 files changed

Lines changed: 1395 additions & 19 deletions

.tool-versions

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
local elixir 1.12.0-otp-24
1+
local elixir 1.18.4-otp-28

CHANGELOG.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [v0.1.0] - 2025-08-02
8+
## [v1.0.0] - 2025-08-02
99

10-
- First release
10+
### Added
11+
12+
- Initial release
13+
- Support for all MessagePack types, including `Integer`, `Float`, `String`,
14+
`Binary`, `Array`, `Map`, `Ext`, and the `Timestamp` extension
15+
- Encoding for the full 64-bit unsigned integer range
16+
- Native encoding and decoding for Elixir's `DateTime` and `NaiveDateTime`
17+
structs
18+
- Protection against maliciously crafted decoding inputs via `:max_depth` and
19+
`:max_byte_size` options
20+
- Added a `:string_validation` option to `encode/2` to bypass UTF-8 validation
21+
for performance gains
22+
- Emits `:telemetry` events for all encode and decode operations
23+
- Includes `encode!/2` and `decode!/2` for raising exceptions on errors

README.md

Lines changed: 185 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,194 @@
11
# msgpack_elixir
22

3+
[![Hex.pm](https://img.shields.io/hexpm/v/msgpack_elixir.svg)](https://hex.pm/packages/msgpack_elixir)
4+
5+
A [MessagePack](https://msgpack.org/) serialization library for Elixir.
6+
7+
## Features
8+
9+
- **Full Specification Compliance:** Adheres to the MessagePack specification to
10+
ensure compatibility with other MessagePack implementations
11+
- Includes support for types such as `Booleans`, `Integers`, `Floats`, `Tuples`,
12+
`Lists`, `Maps`, `Strings`, `Binaries`, `Extensions`, and `Timestamps`
13+
- Encodes and decodes Elixir's `DateTime` and `NaiveDateTime` structs using
14+
the MessagePack `Timestamp` extension
15+
- **Performant Encoding:** Implements efficient encoding for collections and
16+
provides a `:string_validation` option to bypass UTF-8 validation in
17+
performance-sensitive applications
18+
- **Exception-raising Variants:** Includes bang (`!`) variants like `encode!/2`
19+
and `decode!/2` for contexts where raising an exception is preferred over
20+
error tuples
21+
- **Telemetry Integration:** Emits standard `:telemetry` events for all encode
22+
and decode operations, allowing for easy integration into monitoring and
23+
observability tools
24+
325
## Installation
426

5-
The package can be installed by adding `msgpack_elixir` to your list of
6-
dependencies in `mix.exs`:
27+
Add `msgpack_elixir` to your list of dependencies in `mix.exs`:
728

829
```elixir
930
def deps do
10-
[
11-
{:msgpack_elixir, "~> 0.1.0"}
12-
]
31+
[{:msgpack_elixir, "~> 1.0.0"}]
1332
end
1433
```
34+
35+
Then, run `mix deps.get`.
36+
37+
## Usage
38+
39+
### Basic Operations
40+
41+
The library returns `{:ok, value}` tuples for successful operations and
42+
`{:error, reason}` tuples for failures.
43+
44+
```elixir
45+
# Encode a map
46+
iex> data = %{"id" => 1, "name" => "Elixir"}
47+
iex> {:ok, encoded} = Msgpack.encode(data)
48+
<<130, 162, 105, 100, 1, 164, 110, 97, 109, 101, 166, 69, 108, 105, 120, 105, 114>>
49+
50+
# Decode a binary
51+
iex> Msgpack.decode(encoded)
52+
{:ok, %{"id" => 1, "name" => "Elixir"}}
53+
```
54+
55+
### Exception-raising Operations
56+
57+
If you prefer an exception to be raised on failure, use the bang (`!`) variants.
58+
59+
```elixir
60+
iex> encoded = Msgpack.encode!(%{id: 1})
61+
<<129, 162, 105, 100, 1>>
62+
63+
iex> Msgpack.decode!(<<192, 42>>)
64+
** (Msgpack.DecodeError) Failed to decode MessagePack binary. Reason = {:trailing_bytes, <<42>>}
65+
```
66+
67+
## Options
68+
69+
The following options can be passed as a second argument to the `encode` and
70+
`decode` functions.
71+
72+
### For `encode/2`
73+
74+
- `:atoms`
75+
- Controls how atoms are encoded.
76+
- `:string` (default) - Encodes atoms as MessagePack strings
77+
- `:error` - Returns an `{:error, {:unsupported_atom, atom}}` tuple if an atom
78+
is encountered
79+
- `:string_validation`
80+
- Controls whether to perform UTF-8 validation on binaries
81+
- `true` (default) - Validates binaries; encodes as `str` type if valid UTF-8,
82+
`bin` type otherwise
83+
- `false` - Skips validation and encodes all binaries as the `str` type.
84+
Improves performance but should only be used if you are certain your data is
85+
valid
86+
87+
### For `decode/2`
88+
89+
- `:max_depth`
90+
- Sets a limit on the nesting level of arrays and maps to prevent stack
91+
exhaustion attacks
92+
- Defaults to `100`
93+
- `:max_byte_size`
94+
- Sets a limit on the declared byte size of any single string, binary, array,
95+
or map to prevent memory exhaustion attacks
96+
- Defaults to `10_000_000` (10MB)
97+
98+
## Telemetry
99+
100+
The library emits `:telemetry` events which can be used for monitoring or
101+
logging.
102+
103+
- `[:msgpack, :encode]` - Dispatched when `Msgpack.encode/2` is called
104+
- `[:msgpack, :decode]` - Dispatched when `Msgpack.decode/2` is called
105+
106+
Example of attaching a logger:
107+
108+
```elixir
109+
defmodule MyTelemetryHandler do
110+
require Logger
111+
112+
def attach do
113+
:telemetry.attach(
114+
"msgpack-logger",
115+
[:msgpack, :encode],
116+
&__MODULE__.handle_event/4,
117+
nil
118+
)
119+
end
120+
121+
def handle_event(event_name, measurements, metadata, _config) do
122+
Logger.info("Telemetry Event: #{inspect(event_name)}",
123+
measurements: measurements,
124+
metadata: metadata
125+
)
126+
end
127+
end
128+
```
129+
130+
## Development
131+
132+
This section explains how to setup the project locally for development.
133+
134+
### Dependencies
135+
136+
- Elixir `~> 1.7` (OTP 21+)
137+
138+
### Get the Source
139+
140+
Clone the project locally:
141+
142+
```bash
143+
# via HTTPS
144+
git clone https://github.com/nrednav/msgpack_elixir.git
145+
146+
# via SSH
147+
git clone git@github.com:nrednav/msgpack_elixir.git
148+
```
149+
150+
### Install
151+
152+
Install the project's dependencies:
153+
154+
```bash
155+
cd msgpack_elixir/
156+
mix deps.get
157+
```
158+
159+
### Test
160+
161+
Run the test suite:
162+
163+
```bash
164+
mix test
165+
```
166+
167+
### Benchmark
168+
169+
Run the benchmarks:
170+
171+
```bash
172+
mix run bench/run.exs
173+
```
174+
175+
## Versioning
176+
177+
This project uses [Semantic Versioning](https://semver.org/).
178+
For a list of available versions, see the [repository tag list](https://github.com/nrednav/msgpack_elixir/tags).
179+
180+
## Issues & Requests
181+
182+
If you encounter a bug or have a feature request, please [open an
183+
issue](https://github.com/nrednav/msgpack_elixir/issues) on the GitHub
184+
repository.
185+
186+
## Contributing
187+
188+
Public contributions are welcome! If you would like to contribute, please fork
189+
the repository and create a pull request.
190+
191+
## License
192+
193+
This project is licensed under the MIT License - see the [LICENSE](./LICENSE)
194+
file for details.

bench/decode_bench.exs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
defmodule Bench.Decode do
2+
def run do
3+
IO.puts("\n--- Benchmarking Msgpack.decode!/2 ---")
4+
5+
encoded_inputs = Bench.Encode.prepare_decode_inputs()
6+
7+
Benchee.run(
8+
%{
9+
"Decode" => fn encoded_binary ->
10+
Msgpack.decode!(encoded_binary)
11+
end
12+
},
13+
inputs: encoded_inputs,
14+
time: 5,
15+
memory_time: 2,
16+
warmup: 2,
17+
formatters: [Benchee.Formatters.Console]
18+
)
19+
end
20+
end
21+
22+
Bench.Decode.run()

bench/encode_bench.exs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
defmodule Bench.Encode do
2+
@inputs %{
3+
"Small Map" => %{
4+
"id" => 123,
5+
"name" => "Msgpack Elixir",
6+
"is_awesome" => true,
7+
"version" => 1.0
8+
},
9+
"Large List" => Enum.to_list(1..1000),
10+
"Large Map" => Map.new(1..1000, &{&1, &1 * 2}),
11+
"Nested Data" => %{
12+
"user_id" => 456,
13+
"data" => [
14+
%{
15+
"event" => "login",
16+
"timestamp" => NaiveDateTime.utc_now(),
17+
"details" => %{"ip" => "127.0.0.1"}
18+
}
19+
]
20+
},
21+
"Large Binary" => %{
22+
"key" => "data",
23+
"payload" => :crypto.strong_rand_bytes(10_000)
24+
}
25+
}
26+
27+
def run do
28+
IO.puts("\n--- Benchmarking Msgpack.encode!/2 ---")
29+
30+
Benchee.run(
31+
%{
32+
"Encode (Default)" => fn input ->
33+
Msgpack.encode!(input)
34+
end,
35+
"Encode (Optimized)" => fn input ->
36+
Msgpack.encode!(input, string_validation: false)
37+
end
38+
},
39+
inputs: @inputs,
40+
time: 5,
41+
memory_time: 2,
42+
warmup: 2,
43+
formatters: [Benchee.Formatters.Console]
44+
)
45+
end
46+
47+
def prepare_decode_inputs do
48+
for {name, term} <- @inputs, into: %{} do
49+
{name, Msgpack.encode!(term)}
50+
end
51+
end
52+
end
53+
54+
Bench.Encode.run()

bench/run.exs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Mix.Task.run("compile")
2+
3+
IO.puts("--- Running MessagePack Benchmark Suite ---")
4+
IO.puts("Loading benchmark files...")
5+
6+
bench_path = __DIR__
7+
8+
Path.join(bench_path, "encode_bench.exs") |> Code.require_file()
9+
Path.join(bench_path, "decode_bench.exs") |> Code.require_file()
10+
Path.join(bench_path, "string_validation_bench.exs") |> Code.require_file()

bench/string_validation_bench.exs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
defmodule Bench.StringValidation do
2+
@large_string String.duplicate("a", 10_000)
3+
4+
def run do
5+
IO.puts("==== Benchmarking String Validation ====")
6+
IO.puts("Compares default encoding vs. encoding with string_validation: false")
7+
8+
Benchee.run(
9+
%{
10+
"Default (with validation)" => fn ->
11+
Msgpack.encode!(@large_string, string_validation: true)
12+
end,
13+
"Fast Path (without validation)" => fn ->
14+
Msgpack.encode!(@large_string, string_validation: false)
15+
end,
16+
},
17+
time: 5,
18+
memory_time: 2
19+
)
20+
end
21+
end
22+
23+
Bench.StringValidation.run()

0 commit comments

Comments
 (0)