diff options
author | Michael Schaefermeyer <michael.schaefermeyer@gmail.com> | 2016-07-19 01:01:18 +0200 |
---|---|---|
committer | Michael Schaefermeyer <michael.schaefermeyer@gmail.com> | 2016-07-19 01:01:18 +0200 |
commit | fe62ea8ee39e5460b663359daf4759e9a379e538 (patch) | |
tree | d7cab1c9abfdd26b56869f75d355800ad2ba51ab | |
download | boltex-fe62ea8ee39e5460b663359daf4759e9a379e538.tar.gz boltex-fe62ea8ee39e5460b663359daf4759e9a379e538.tar.xz |
Initial commit
-rw-r--r-- | .gitignore | 5 | ||||
-rw-r--r-- | README.md | 30 | ||||
-rw-r--r-- | config/config.exs | 30 | ||||
-rw-r--r-- | lib/boltex.ex | 13 | ||||
-rw-r--r-- | lib/boltex/bolt.ex | 209 | ||||
-rw-r--r-- | lib/boltex/pack_stream.ex | 113 | ||||
-rw-r--r-- | lib/boltex/pack_stream/encoder.ex | 110 | ||||
-rw-r--r-- | lib/boltex/utils.ex | 14 | ||||
-rw-r--r-- | mix.exs | 37 | ||||
-rw-r--r-- | mix.lock | 4 | ||||
-rw-r--r-- | test/boltex/pack_stream_test.exs | 119 | ||||
-rw-r--r-- | test/boltex_test.exs | 8 | ||||
-rw-r--r-- | test/test_helper.exs | 1 |
13 files changed, 693 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..755b605 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/_build +/cover +/deps +erl_crash.dump +*.ez diff --git a/README.md b/README.md new file mode 100644 index 0000000..f098ff6 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# Boltex + +Elixir implementation of the Bolt protocol and corresponding PackStream +protocol. Both is being used by Neo4J. + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed as: + + 1. Add boltex to your list of dependencies in `mix.exs`: + + def deps do + [{:boltex, "~> 0.0.1"}] + end + + 2. Ensure boltex is started before your application: + + def application do + [applications: [:boltex]] + end + +## Todo + +- [x] PackStream decoding +- [x] PackStream encoding +- [x] Bolt message receiving +- [x] Bolt message sending +- [ ] Auth +- [ ] Transport adapter (e.g. plain `:gen_tcp`, `DBConnection`, ...) +- [ ] Handle failures gracefully diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..e698bfe --- /dev/null +++ b/config/config.exs @@ -0,0 +1,30 @@ +# This file is responsible for configuring your application +# and its dependencies with the aid of the Mix.Config module. +use Mix.Config + +# This configuration is loaded before any dependency and is restricted +# to this project. If another project depends on this project, this +# file won't be loaded nor affect the parent project. For this reason, +# if you want to provide default values for your application for +# 3rd-party users, it should be done in your "mix.exs" file. + +# You can configure for your application as: +# +# config :boltex, key: :value +# +# And access this configuration in your application as: +# +# Application.get_env(:boltex, :key) +# +# Or configure a 3rd-party app: +# +# config :logger, level: :info +# + +# It is also possible to import configuration files, relative to this +# directory. For example, you can emulate configuration per environment +# by uncommenting the line below and defining dev.exs, test.exs and such. +# Configuration from the imported file will override the ones defined +# here (which is why it is important to import them last). +# +# import_config "#{Mix.env}.exs" diff --git a/lib/boltex.ex b/lib/boltex.ex new file mode 100644 index 0000000..9de7d3b --- /dev/null +++ b/lib/boltex.ex @@ -0,0 +1,13 @@ +defmodule Boltex do + alias Boltex.Bolt + + def test(host, port, query) do + {:ok, p} = :gen_tcp.connect host, port, [active: false, mode: :binary, packet: :raw] + + :ok = Bolt.handshake :gen_tcp, p + :ok = Bolt.init :gen_tcp, p + + Enum.map Bolt.run_statement(:gen_tcp, p, query), &IO.inspect/1 + end + +end diff --git a/lib/boltex/bolt.ex b/lib/boltex/bolt.ex new file mode 100644 index 0000000..dc0c04d --- /dev/null +++ b/lib/boltex/bolt.ex @@ -0,0 +1,209 @@ +defmodule Boltex.Bolt do + alias Boltex.{Utils, PackStream} + require Logger + + @recv_timeout 1_000 + @max_chunk_size 65_535 + + @user_agent "Boltex/1.0" + @hs_magic << 0x60, 0x60, 0xB0, 0x17 >> + @hs_version << 1 :: 32, 0 :: 32, 0 :: 32, 0 :: 32 >> + + @zero_chunk << 0, 0 >> + + @sig_init 0x01 + @sig_ack_failure 0x0E + @sig_reset 0x0F + @sig_run 0x10 + @sig_discard_all 0x2F + @sig_pull_all 0x3F + @sig_success 0x70 + @sig_record 0x71 + @sig_ignored 0x7E + @sig_failure 0x7F + + @summary ~w(success ignored failure)a + + @moduledoc """ + The Boltex.Bolt module handles the Bolt protocol specific steps (i.e. + handshake, init) as well as sending and receiving messages and wrapping + them in chunks. + + It abstracts transportation, expecing the transport layer to define + send/2 and recv/3 analogous to :gen_tcp. + """ + + @doc "Does the handshake" + def handshake(transport, port) do + transport.send port, @hs_magic <> @hs_version + case transport.recv(port, 4, @recv_timeout) do + {:ok, << 1 :: 32 >>} -> + :ok + + response -> + Logger.error "Handshake failed. Received: #{Utils.hex_encode response})" + {:error, :handshake_failed} + end + end + + @doc """ + Initialises the connection. + """ + def init(transport, port, params \\ %{}) do + send_messages transport, port, [{[@user_agent, params], @sig_init}] + + case receive_data(transport, port) do + {:success, %{}} -> + :ok + + response -> + Logger.error "Init failed. Received: #{Utils.hex_encode response})" + {:error, :init_failed} + end + end + + @doc """ + Sends a list of messages using the Bolt protocol and PackStream encoding. + + Messages have to be in the form of {[messages], signature}. + """ + def send_messages(transport, port, messages) do + Enum.map(messages, &generate_binary_message/1) + |> generate_chunks + |> Enum.each(&(transport.send(port, &1))) + end + + defp generate_binary_message({messages, signature}) do + messages = List.wrap messages + struct_size = length messages + + << 0xB :: 4, struct_size :: 4, signature >> <> + Utils.reduce_to_binary(messages, &PackStream.encode/1) + end + + defp generate_chunks(messages, chunks \\ [], current_chunk \\ <<>>) + defp generate_chunks([], chunks, current_chunk) do + [current_chunk | chunks] + |> Enum.reverse + end + defp generate_chunks([message | messages], chunks, current_chunk) + when byte_size(current_chunk <> message) <= @max_chunk_size do + message_size = byte_size message + current_chunk = + current_chunk <> + << message_size :: 16 >> <> + message <> + @zero_chunk + + generate_chunks messages, chunks, current_chunk + end + defp generate_chunks([chunk | chunks], chunks, current_chunk) do + oversized_chunk = current_chunk <> chunk + {first, rest} = binary_part oversized_chunk, 0, @max_chunk_size + first_size = byte_size first + rest_size = byte_size rest + current_chunk = current_chunk <> << first_size :: 16 >> <> first + new_chunk = << rest_size :: 16 >> <> rest + + generate_chunks chunks, [current_chunk | chunks], new_chunk + end + + @doc """ + Runs a statement (most like Cypher statement) and returns a list of the + records and a summary. + + Records are represented using PackStream's record data type. Their Elixir + representation is a Keyword with the indexse `:sig` and `:fields`. + + ## Examples + + iex> Boltex.Bolt.run_statement("MATCH (n) RETURN n") + [ + {:record, [sig: 1, fields: [1, "Exmaple", "Labels", %{"some_attribute" => "some_value"}, + {:success, %{"type" => "r"}} + ] + """ + def run_statement(transport, port, statement, params \\ %{}) do + send_messages transport, port, [ + {[statement, params], @sig_run}, + {[nil], @sig_pull_all} + ] + + with {:success, %{}} <- receive_data(transport, port), + do: receive_data transport, port + end + + @doc """ + Receives data. + + This function is supposed to be called after a request to the server has been + made. It receives data chunks, mends them (if they were split between frames) + and decodes them using PackStream. + + When just a single message is received (i.e. to acknowledge a command), this + function returns a tuple with two items, the first being the signature and the + second being the message(s) itself. If a list of messages is received it will + return a list of the former. + + The same goes for the messages: If there was a single data point in a message + said data point will be returned by itself. If there were multiple data points, + the list will be returned. + + The signature is represented as one of the following: + + * `:success` + * `:record` + * `:ignored` + * `:failure` + """ + def receive_data(transport, port, previous \\ []) do + case do_receive_data(transport, port) |> unpack do + {:record, _} = data -> + receive_data transport, port, [data | previous] + + {status, _} = data when status in @summary and previous == [] -> + data + + {status, _} = data when status in @summary -> + Enum.reverse [data | previous] + end + end + + defp do_receive_data(transport, port) do + with {:ok, <<chunk_size :: 16>>} <- transport.recv(port, 2, @recv_timeout), + do: do_receive_data(transport, port, chunk_size) + end + defp do_receive_data(transport, port, chunk_size) do + with {:ok, data} <- transport.recv(port, chunk_size, @recv_timeout) + do + case transport.recv(port, 2, @recv_timeout) do + {:ok, @zero_chunk} -> + data + {:ok, <<chunk_size :: 16>>} -> + data <> do_receive_data(transport, port, chunk_size) + end + else + {:error, :timeout} -> + {:error, :no_more_data_received} + other -> + IO.inspect Utils.hex_encode other + raise "receive failed" + end + end + + @doc """ + Unpacks (or in other words parses) a message. + """ + def unpack(<< 0x0B :: 4, packages :: 4, status, message :: binary >>) do + response = PackStream.decode(message) + response = if packages == 1, do: List.first(response), else: response + + case status do + @sig_success -> {:success, response} + @sig_record -> {:record, response} + @sig_ignored -> {:ignored, response} + @sig_failure -> {:failure, response} + other -> raise "Couldn't decode #{Utils.hex_encode << other >>}" + end + end +end diff --git a/lib/boltex/pack_stream.ex b/lib/boltex/pack_stream.ex new file mode 100644 index 0000000..ada9667 --- /dev/null +++ b/lib/boltex/pack_stream.ex @@ -0,0 +1,113 @@ +defmodule Boltex.PackStream do + @moduledoc """ + The PackStream implementation for Bolt. + + This module defines a decode function, that will take a binary stream of data + and recursively turn it into a list of Elixir data types. + + It further defines a function for encoding Elixir data types into a binary + stream, using the Boltex.PackStream.Encoder protocol. + """ + + @doc """ + Encodes a list of items into their binary representation. + + As developers tend to be lazy, single objects may be passed. + + ## Examples + + iex> Boltex.PackStream.encode "hello world" + <<0x8B, 0x68, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x77, 0x6F, 0x72, 0x6C, 0x64>> + """ + def encode(item), do: Boltex.PackStream.Encoder.encode(item) + + ## + # Decode + @doc "Decodes a binary stream recursively into Elixir data types" + # Null + def decode(<< 0xC0, rest :: binary >>), do: [nil| decode(rest)] + + # Boolean + def decode(<< 0xC3, rest :: binary >>), do: [true | decode(rest)] + def decode(<< 0xC2, rest :: binary >>), do: [false | decode(rest)] + + # Float + def decode(<< 0xC1, number :: float, rest :: binary >>) do + [number | decode(rest)] + end + + # Strings + def decode(<< 0x8 :: 4, str_length :: 4, rest :: bytes >>) do + decode_text rest, str_length + end + def decode(<< 0xD0, str_length, rest :: bytes >>) do + decode_text rest, str_length + end + def decode(<< 0xD1, str_length :: 16, rest :: bytes >>) do + decode_text rest, str_length + end + def decode(<< 0xD2, str_length :: 32, rest :: binary >>) do + decode_text rest, str_length + end + + # Lists + # FIXME: Make sure list size is correct, only collect items within list-size. + def decode(<< 0x9 :: 4, _list_size :: 4, list :: binary >>), do: [decode list] + def decode(<< 0xD4, _list_size :: 8, list :: binary >>), do: [decode list] + def decode(<< 0xD5, _list_size :: 16, list :: binary >>), do: [decode list] + def decode(<< 0xD6, _list_size :: 32, list :: binary >>), do: [decode list] + def decode(<< 0xD7, list :: binary >>) do + position = + for(<<byte <- list>>, do: byte) + |> Enum.find_index(&(&1 == 0xDF)) + + << list :: binary-size(position), 0xDF, rest :: binary >> = list + + [decode(list) | decode(rest)] + end + + # Maps + # FIXME: Make sure map size is correct, only collect items within map-size. + def decode(<< 0xA :: 4, _entries :: 4, map :: binary>>), do: decode_map(map) + def decode(<< 0xD8, _entries, map :: binary >>), do: decode_map(map) + def decode(<< 0xD9, _entries :: 16, map :: binary >>), do: decode_map(map) + def decode(<< 0xDA, _entries :: 32, map :: binary >>), do: decode_map(map) + def decode(<< 0xDB, map :: binary >>) do + position = + for(<<byte <- map>>, do: byte) + |> Enum.find_index(&(&1 == 0xDF)) + + << map:: binary-size(position), 0xDF, rest :: binary >> = map + + decode_map(map) ++ decode(rest) + end + + # Struct + def decode(<< 0xB :: 4, _struct_size :: 4, sig :: 8>> <> struct) do + [sig: sig, fields: decode(struct)] + end + + def decode(<<0, 0>>), do: [] + def decode(""), do: [] + + # Integers + def decode(<< 0xC8, int, rest :: binary >>), do: [int | decode(rest)] + def decode(<< 0xC9, int :: 16, rest :: binary >>), do: [int | decode(rest)] + def decode(<< 0xCA, int :: 32, rest :: binary >>), do: [int | decode(rest)] + def decode(<< 0xCB, int :: 64, rest :: binary >>), do: [int | decode(rest)] + def decode(<< int, rest :: binary >>), do: [int | decode(rest)] + + defp decode_text(bytes, str_length) do + << string :: binary-size(str_length), rest :: binary >> = bytes + + [string | decode(rest)] + end + + defp decode_map(map) do + decode(map) + |> Enum.chunk(2) + |> Enum.map(&List.to_tuple/1) + |> Enum.into(%{}) + |> List.wrap + end +end diff --git a/lib/boltex/pack_stream/encoder.ex b/lib/boltex/pack_stream/encoder.ex new file mode 100644 index 0000000..bd7dc6e --- /dev/null +++ b/lib/boltex/pack_stream/encoder.ex @@ -0,0 +1,110 @@ +defprotocol Boltex.PackStream.Encoder do + @doc "Encodes an item to its binary PackStream Representation" + def encode(entitiy) +end + +defimpl Boltex.PackStream.Encoder, for: Atom do + def encode(nil), do: << 0xC0 >> + def encode(true), do: << 0xC3 >> + def encode(false), do: << 0xC2 >> + def encode(other) when is_atom(other) do + other + |> Atom.to_string + |> Boltex.PackStream.Encoder.encode + end +end + +defimpl Boltex.PackStream.Encoder, for: Integer do + @int8 -127..-17 + @int16_low -32_768..-129 + @int16_high 128..32_767 + @int32_low -2_147_483_648..-32_769 + @int32_high 32_768..2_147_483_647 + @int64_low -9_223_372_036_854_775_808..-2_147_483_649 + @int64_high 2_147_483_648..9_223_372_036_854_775_807 + + def encode(integer) when integer in -16..127 do + <<integer>> + end + def encode(integer) when integer in @int8 do + << 0xC8, integer >> + end + def encode(integer) when integer in @int16_low or integer in @int16_high do + << 0xC9, integer >> + end + def encode(integer) when integer in @int32_low or integer in @int32_high do + << 0xCA, integer >> + end + def encode(integer) when integer in @int64_low or integer in @int64_high do + << 0xCB, integer >> + end +end + +defimpl Boltex.PackStream.Encoder, for: BitString do + def encode(string), do: do_encode(string, byte_size(string)) + + defp do_encode(string, size) when size <= 15 do + << 0x8 :: 4, size :: 4 >> <> string + end + defp do_encode(string, size) when size <= 255 do + << 0xD0, size :: 8 >> <> string + end + defp do_encode(string, size) when size <= 65_535 do + << 0xD1, size :: 16 >> <> string + end + defp do_encode(string, size) when size <= 4_294_967_295 do + << 0xD2, size :: 32 >> <> string + end +end + +defimpl Boltex.PackStream.Encoder, for: List do + def encode(list) do + binary = Enum.map_join list, &Boltex.PackStream.Encoder.encode/1 + + do_encode binary, byte_size(binary) + end + + defp do_encode(binary, list_size) when list_size <= 15 do + << 0x9 :: 4, list_size :: 4 >> <> binary + end + defp do_encode(binary, list_size) when list_size <= 255 do + << 0xD4, list_size :: 8 >> <> binary + end + defp do_encode(binary, list_size) when list_size <= 65_535 do + << 0xD5, list_size :: 16 >> <> binary + end + defp do_encode(binary, list_size) when list_size <= 4_294_967_295 do + << 0xD6, list_size :: 32 >> <> binary + end + defp do_encode(binary, _size) do + << 0xD7 >> <> binary <> <<0xDF>> + end +end + +defimpl Boltex.PackStream.Encoder, for: Map do + def encode(map) do + do_encode map, map_size(map) + end + + defp do_encode(map, size) when size <= 15 do + << 0xA :: 4, size :: 4 >> <> encode_kv(map) + end + defp do_encode(map, size) when size <= 255 do + << 0xD8, size :: 8 >> <> encode_kv(map) + end + defp do_encode(map, size) when size <= 65_535 do + << 0xD9, size :: 16 >> <> encode_kv(map) + end + defp do_encode(map, size) when size <= 4_294_967_295 do + << 0xDA, size :: 32 >> <> encode_kv(map) + end + + defp encode_kv(map) do + Boltex.Utils.reduce_to_binary map, &do_reduce_kv/1 + end + + defp do_reduce_kv({key, value}) do + Boltex.PackStream.Encoder.encode(key) <> + Boltex.PackStream.Encoder.encode(value) + end +end diff --git a/lib/boltex/utils.ex b/lib/boltex/utils.ex new file mode 100644 index 0000000..333217e --- /dev/null +++ b/lib/boltex/utils.ex @@ -0,0 +1,14 @@ +defmodule Boltex.Utils do + def reduce_to_binary(enumerable, transform) do + Enum.reduce enumerable, <<>>, fn(data, acc) -> acc <> transform.(data) end + end + + def hex_encode(bytes) do + for << i <- bytes >>, do: Integer.to_string(i, 16) + end + + def hex_decode(hex_list) do + for(hex <- hex_list, do: Integer.parse(hex, 16) |> elem(0)) + |> reduce_to_binary(&<<&1>>) + end +end @@ -0,0 +1,37 @@ +defmodule Boltex.Mixfile do + use Mix.Project + + def project do + [app: :boltex, + version: "0.0.1", + elixir: "~> 1.2", + build_embedded: Mix.env == :prod, + start_permanent: Mix.env == :prod, + deps: deps] + end + + # Configuration for the OTP application + # + # Type "mix help compile.app" for more information + def application do + [ + #mod: {Boltex, []}, + applications: [:logger] + ] + end + + # Dependencies can be Hex packages: + # + # {:mydep, "~> 0.3.0"} + # + # Or git/path repositories: + # + # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} + # + # Type "mix help deps" for more examples and options + defp deps do + [ + {:mix_test_watch, "~> 0.2.6", only: [:dev, :test]} + ] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..fc3132e --- /dev/null +++ b/mix.lock @@ -0,0 +1,4 @@ +%{"connection": {:hex, :connection, "1.0.3", "3145f7416be3df248a4935f24e3221dc467c1e3a158d62015b35bd54da365786", [:mix], []}, + "db_connection": {:hex, :db_connection, "1.0.0-rc.3", "d9ceb670fe300271140af46d357b669983cd16bc0d01206d7d3222dde56cf038", [:mix], [{:sbroker, "~> 1.0.0-beta.3", [hex: :sbroker, optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: true]}, {:connection, "~> 1.0.2", [hex: :connection, optional: false]}]}, + "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []}, + "mix_test_watch": {:hex, :mix_test_watch, "0.2.6", "9fcc2b1b89d1594c4a8300959c19d50da2f0ff13642c8f681692a6e507f92cab", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, optional: false]}]}} diff --git a/test/boltex/pack_stream_test.exs b/test/boltex/pack_stream_test.exs new file mode 100644 index 0000000..1689aa5 --- /dev/null +++ b/test/boltex/pack_stream_test.exs @@ -0,0 +1,119 @@ +defmodule Boltex.PackStreamTest do + use ExUnit.Case + + alias Boltex.{Utils, PackStream} + + # A lot of the examples have been taken from + # https://github.com/neo4j/neo4j-python-driver/blob/1.1/neo4j/v1/packstream.py + # """ + + doctest Boltex.PackStream + + test "encodes null" do + assert PackStream.encode(nil) == <<0xC0>> + end + + test "encodes boolean" do + assert PackStream.encode(true) == <<0xC3>> + assert PackStream.encode(false) == <<0xC2>> + end + + test "encodes atom" do + assert PackStream.encode(:hello) == <<0x85, 0x68, 0x65, 0x6C, 0x6C, 0x6F>> + end + + test "encodes integer" do + assert PackStream.encode(0) == << 0x00 >> + assert PackStream.encode(42) == << 0x2A >> + assert PackStream.encode(-42) == << 0xC8, 0xD6 >> + assert PackStream.encode(420) == << 0xC9, 0xA4 >> + end + + test "encodes string" do + assert PackStream.encode("") == << 0x80 >> + assert PackStream.encode("Short") == <<0x85, 0x53, 0x68, 0x6F, 0x72, 0x74>> + + long_8 = "This is a räther löng string" # 30 bytes due to umlauts + assert <<0xD0, 0x1E, _ :: binary-size(30)>> = PackStream.encode(long_8) + + long_16 = + """ + For encoded string containing fewer than 16 bytes, including empty strings, + the marker byte should contain the high-order nibble `1000` followed by a + low-order nibble containing the size. The encoded data then immediately + follows the marker. + + For encoded string containing 16 bytes or more, the marker 0xD0, 0xD1 or + 0xD2 should be used, depending on scale. This marker is followed by the + size and the UTF-8 encoded data. + """ + assert <<0xD1, 0x01, 0xA5, _ :: binary-size(421)>> = PackStream.encode(long_16) + end + + test "encodes list" do + assert PackStream.encode([]) == <<0x90>> + end + + test "encodes map" do + assert PackStream.encode(%{}) == <<0xA0>> + end + + test "decodes null" do + assert PackStream.decode(<<0xC0>>) == [nil] + end + + test "decodes boolean" do + assert PackStream.decode(<<0xC3>>) == [true] + assert PackStream.decode(<<0xC2>>) == [false] + end + + test "decodes floats" do + positive = ~w(C1 3F F1 99 99 99 99 99 9A) |> Utils.hex_decode + negative = ~w(C1 BF F1 99 99 99 99 99 9A) |> Utils.hex_decode + + assert PackStream.decode(positive) == [1.1] + assert PackStream.decode(negative) == [-1.1] + end + + test "decodes integers" do + assert PackStream.decode(<<0x2A>>) == [42] + assert PackStream.decode(<<0xC8, 0x2A>>) == [42] + assert PackStream.decode(<<0xC9, 0, 0x2A>>) == [42] + assert PackStream.decode(<<0xCA, 0, 0, 0, 0x2A>>) == [42] + assert PackStream.decode(<<0xCB, 0, 0, 0, 0, 0, 0, 0, 0x2A>>) == [42] + end + + test "decodes strings" do + longstr = + ~w(D0 1A 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 75 76 77 78 79 7A) + |> Utils.hex_decode + + specialcharstr = + ~w(D0 18 45 6E 20 C3 A5 20 66 6C C3 B6 74 20 C3 B6 76 65 72 20 C3 A4 6E 67 65 6E) + |> Utils.hex_decode + + + assert PackStream.decode(<<0x80>>) == [""] + assert PackStream.decode(<<0x81, 0x61>>) == ["a"] + assert PackStream.decode(longstr) == ["abcdefghijklmnopqrstuvwxyz"] + assert PackStream.decode(specialcharstr) == ["En å flöt över ängen"] + end + + test "decodes lists" do + longlist = + ~w(D4 14 01 02 03 04 05 06 07 08 09 00) + |> Utils.hex_decode + + assert PackStream.decode(<<0x90>>) == [[]] + assert PackStream.decode(<<0x93, 0x01, 0x02, 0x03>>) == [[1, 2, 3]] + assert PackStream.decode(longlist) == [[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]] + assert PackStream.decode(<<0xD7, 0x01, 0x02, 0xDF>>) == [[1, 2]] + end + + test "decodes maps" do + assert PackStream.decode(<<0xA0>>) == [%{}] + assert PackStream.decode(<<0xA1, 0x81, 0x61, 0x01>>) == [%{"a" => 1}] + assert PackStream.decode(<<0xAB, 0x81, 0x61, 0x01, 0xDF>>) == [%{"a" => 1}] + end + +end diff --git a/test/boltex_test.exs b/test/boltex_test.exs new file mode 100644 index 0000000..c091178 --- /dev/null +++ b/test/boltex_test.exs @@ -0,0 +1,8 @@ +defmodule BoltexTest do + use ExUnit.Case + doctest Boltex + + test "the truth" do + assert 1 + 1 == 2 + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() |