├── .formatter.exs ├── .github └── workflows │ ├── elixir.yml │ └── release.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── bench └── compare.exs ├── guides ├── migration_from_elixir_uuid.md └── using_with_ecto.md ├── lib ├── app.ex ├── generator.ex ├── macros.ex └── uuid.ex ├── mix.exs ├── mix.lock └── test ├── ecto_test.exs ├── generator_test.exs ├── support ├── generators.ex └── test_repo.exs ├── test_helper.exs └── uniq_test.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | name: elixir 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | otp: ['24.3', '25.3'] 13 | elixir: ['1.13.4', '1.14.5', '1.15.0'] 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: erlef/setup-beam@v1 17 | with: 18 | otp-version: ${{matrix.otp}} 19 | elixir-version: ${{matrix.elixir}} 20 | - run: mix deps.get 21 | - run: mix test 22 | 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | name: Publish 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: erlef/setup-beam@v1 14 | with: 15 | elixir-version: '1.15' 16 | otp-version: '25.3' 17 | - run: mix deps.get 18 | - run: mix hex.publish --yes 19 | env: 20 | HEX_API_KEY: ${{ secrets.HEX_API_KEY }} 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | uniq-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Paul Schoenfelder 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Uniq 2 | 3 | [![Master](https://github.com/bitwalker/uniq/workflows/elixir/badge.svg?branch=main)](https://github.com/bitwalker/uniq/actions?query=workflow%3A%22elixir%22+branch%3Amain) 4 | [![Hex.pm Version](http://img.shields.io/hexpm/v/uniq.svg?style=flat)](https://hex.pm/packages/uniq) 5 | 6 | Uniq provides generation, formatting, parsing, and analysis of RFC 4122 UUIDs, with 7 | support for the draft UUIDv6 extension. It is a package for Elixir projects, and can 8 | be found on Hex as `:uniq`. 9 | 10 | ## Features 11 | 12 | * Follows the RFC 4122 specification, i.e. supports UUID versions 1, 3, 4, and 5 as described in the RFC 13 | * Supports UUIDv6 and UUIDv7, which are described in a proposed extension for RFC 4122, and improve upon desirable traits 14 | of both UUIDv1 and UUIDv4 to provide the best of both, while removing their downsides. See 15 | [here](https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format) for more information on how it does so. 16 | * Supports formatting UUIDs as canonical strings (e.g. `6ba7b810-9dad-11d1-80b4-00c04fd430c8`, with or without the dashes), 17 | as URNs, e.g. `urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8`, as well as a compact, 22-character, base64-encoded format using 18 | a URI-safe alphabet (e.g. `a6e4EJ2tEdGAtADAT9QwyA`). 19 | * Case-insensitive, i.e. `6ba7b810-9dad-11d1-80b4-00c04fd430c8` and `6BA7B810-9DAD-11D1-80B4-00C04FD430C8` have the same encoding 20 | * Supports Ecto out of the box, just use `Uniq.UUID` as the type of a field where you would use `Ecto.UUID`. See the docs for more info. 21 | * Can be used as a drop-in replacement for `elixir_uuid`, see the docs for details on migrating 22 | 23 | ## Installation 24 | 25 | ```elixir 26 | def deps do 27 | [ 28 | {:uniq, "~> 0.1"} 29 | ] 30 | end 31 | ``` 32 | 33 | ## Usage 34 | 35 | The primary API is provided by the `Uniq.UUID` module. 36 | 37 | To generate UUIDs, pick the version you want, and call the appropriate generator. For example: 38 | 39 | * `uuid1/0`, generates UUIDv1 and formats it as a human-readable string, i.e. `6ba7b810-9dad-11d1-80b4-00c04fd430c8` 40 | * `uuid1/1`, generates UUIDv1 in the specified format 41 | * `uuid3/2`, generates UUIDv3 using the provided namespace and name, and formats it as a human-readable string 42 | * `uuid3/3`, generates UUIDv3 using the provided namespace and name, in the specified format 43 | 44 | See the [docs](https://hexdocs.pm/uniq) for the full set of functions available. 45 | 46 | You can also convert UUID strings to/from the human-readable and binary formats; parse UUID strings/binaries; and determine their validity. 47 | -------------------------------------------------------------------------------- /bench/compare.exs: -------------------------------------------------------------------------------- 1 | uuid_string = "716c654f-d2b7-436b-9751-2440a9cb079d" 2 | uuid_binary = <<113, 108, 101, 79, 210, 183, 67, 107, 151, 81, 36, 64, 169, 203, 7, 157>> 3 | 4 | Benchee.run( 5 | %{ 6 | "elixir_uuid" => fn 7 | {"to_string", bin} -> 8 | UUID.binary_to_string!(bin) 9 | 10 | {"to_binary", s} -> 11 | UUID.string_to_binary!(s) 12 | 13 | {gen, ns, name} -> 14 | apply(UUID, gen, [ns, name]) 15 | 16 | gen -> 17 | apply(UUID, gen, []) 18 | end, 19 | "uniq" => fn 20 | {"to_string", bin} -> 21 | Uniq.UUID.to_string(bin) 22 | 23 | {"to_binary", s} -> 24 | Uniq.UUID.string_to_binary!(s) 25 | 26 | {gen, ns, name} -> 27 | apply(Uniq.UUID, gen, [ns, name]) 28 | 29 | gen -> 30 | apply(Uniq.UUID, gen, []) 31 | end 32 | }, 33 | inputs: %{ 34 | "to_string" => {"to_string", uuid_binary}, 35 | "to_binary" => {"to_binary", uuid_string}, 36 | "uuid1" => :uuid1, 37 | "uuid3" => {:uuid3, :dns, "my.example.com"}, 38 | "uuid4" => :uuid4, 39 | "uuid5" => {:uuid5, :dns, "my.example.com"}, 40 | } 41 | ) 42 | -------------------------------------------------------------------------------- /guides/migration_from_elixir_uuid.md: -------------------------------------------------------------------------------- 1 | # Migrating From elixir_uuid 2 | 3 | Migration from `elixir_uuid` is very simple, and you have 2 paths depending on how it is used 4 | in your project today. 5 | 6 | 1. You no longer depend on `elixir_uuid` directly or indirectly 7 | 3. You no longer depend on `elixir_uuid` directly, but it is still present in your dependency tree 8 | 9 | In the first scenario, all you need to do is replace any uses of `UUID` in your project with `Uniq.UUID`, 10 | or simply alias `Uniq.UUID` in those modules. 11 | 12 | NOTE: The `info/1` and `info!/1` functions return a struct by default, so if you use those functions and 13 | you aren't planning to add the compatibility shim, you'll want to update those uses. See the function docs 14 | for information on the structure. 15 | 16 | In the second scenario - which also applies if you just want to add the dependency without making any code 17 | changes - you must add an override dependency for `:elixir_uuid`, like so: 18 | 19 | ```elixir 20 | defp deps do 21 | [ 22 | {:uniq, "~> x.x"}, 23 | {:elixir_uuid, "~> 0.1", hex: :uniq_compat, override: true}, 24 | ] 25 | end 26 | ``` 27 | 28 | This replaces the `:elixir_uuid` dependency with a shim that delegates to `:uniq` internally. With this 29 | in place, `elixir_uuid` is removed from your dependency tree entirely. 30 | -------------------------------------------------------------------------------- /guides/using_with_ecto.md: -------------------------------------------------------------------------------- 1 | # Using With Ecto 2 | 3 | You can use the type provided by this library in lieu of `Ecto.UUID`, simply use `Uniq.UUID` 4 | where you would use `Ecto.UUID`, and specify `:binary` or `:string` as the type of the column 5 | in your migrations, depending on what parameters you pass to the type. By default, with no 6 | parameters, the UUID will be stored as `:binary`, and loaded in `:default` format. You can control 7 | this behaviour by using the `:dump` and `:format` options to control what format is used for persistence 8 | and in-memory, respectively. The format atoms are the same as you can pass elsewhere, i.e. `:raw`, `:default`, 9 | `:hex`, `:urn`, and `:slug`. All of them but `:raw` are printable strings, while `:raw` is a binary-encoded 10 | format. 11 | 12 | If you wish to use autogenerated UUIDs with Ecto, you have a couple of options: 13 | 14 | ```elixir 15 | # Generate UUIDv4 primary keys 16 | @primary_key {:id, Uniq.UUID, autogenerate: true} 17 | schema "foo" do 18 | ... 19 | end 20 | 21 | # Generate primary keys using UUIDs of a specific version 22 | # NOTE: To autogenerate UUIDs using version 3 or 5, see below 23 | @primary_key {:id, Uniq.UUID, version: 1, autogenerate: true} 24 | schema "foo" do 25 | ... 26 | end 27 | 28 | # Generate primary keys using, version 3 or 5, which are namespaced 29 | # NOTE: Uniq generates 8 bytes of cryptographically strong random data for the name, but 30 | # you must provide a custom namespace in which these names are allocated, as the predefined 31 | # namespaces are not a good fit for random generated ids. 32 | @namespace Uniq.UUID.uuid5(:dns, "foo.example.com", :raw) 33 | @primary_key {:id, Uniq.UUID, version: 5, namespace: @namespace, autogenerate: true} 34 | schema "foo" do 35 | ... 36 | end 37 | 38 | # The same rules as above can be used to autogenerate UUIDs for any field, not just primary keys 39 | schema "foo" do 40 | field :uuidv4, Uniq.UUID, autogenerate: true 41 | field :uuidv1, Uniq.UUID, version: 1, autogenerate: true 42 | # This field will be dumped to a 36-byte printable string format, and loaded into a 22-byte base64-encoded string 43 | field :uuidv3, Uniq.UUID, version: 3, namespace: @namespace, format: :slug, dump: :default, autogenerate: true 44 | end 45 | ``` 46 | 47 | To use UUIDs for all keys, you can do something like this: 48 | 49 | ```elixir 50 | # Define your global schema defaults in a module 51 | defmodule MyApp.Schema do 52 | defmacro __using__(_) do 53 | quote do 54 | use Ecto.Schema 55 | @primary_key {:id, Uniq.UUID, autogenerate: true} 56 | @foreign_key_type Uniq.UUID 57 | end 58 | end 59 | end 60 | 61 | # Then use that module anywhere that you would use `Ecto.Schema` to apply those defaults 62 | defmodule MyApp.Comment do 63 | use MyApp.Schema 64 | 65 | schema "comments" do 66 | belongs_to :post, MyApp.Post 67 | end 68 | end 69 | ``` 70 | 71 | For many_to_many association you should add `type: :uuid`: 72 | 73 | ```elixir 74 | @primary_key {:id, Uniq.UUID, version: 7, autogenerate: true, type: :uuid} 75 | ``` 76 | -------------------------------------------------------------------------------- /lib/app.ex: -------------------------------------------------------------------------------- 1 | defmodule Uniq.App do 2 | @moduledoc false 3 | use Application 4 | 5 | def start(_type, _args) do 6 | children = [ 7 | Uniq.Generator 8 | ] 9 | 10 | Supervisor.start_link(children, strategy: :rest_for_one) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/generator.ex: -------------------------------------------------------------------------------- 1 | defmodule Uniq.Generator do 2 | @moduledoc """ 3 | This module is used to interact with the global state 4 | needed to correctly generate version 1 and version 6 UUIDs. 5 | 6 | This state consists of two atomic values: the last timestamp at 7 | which a UUID was generated; and the current clock sequence value. 8 | 9 | On init, the clock sequence is seeded with a random value provided 10 | by interpreting bits from the cryptographic random number generator 11 | as an unsigned 14 bit integer. 12 | 13 | On subsequent generations, the clock sequence is incremented by 1 14 | if a UUID has already been generated with the same last timestamp. 15 | If the timestamp has changed, the clock sequence remains unchanged. 16 | """ 17 | use GenServer 18 | 19 | @compile {:inline, [get_atomic_ref: 0, now: 0]} 20 | 21 | @timestamp_index 1 22 | @clock_index 2 23 | 24 | @doc """ 25 | Generates the next initial state for UUID creation. 26 | """ 27 | def next do 28 | ref = get_atomic_ref() 29 | last_ts = :atomics.get(ref, @timestamp_index) 30 | current_ts = now() 31 | 32 | # Increment clock sequence if the timestamp has not changed 33 | if last_ts == current_ts do 34 | # Always increment the clock to ensure concurrent accesses 35 | # are unique for the same timestamp 36 | clock = :atomics.add_get(ref, @clock_index, 1) 37 | 38 | {current_ts, clock} 39 | else 40 | clock = :atomics.add_get(ref, @clock_index, 1) 41 | :atomics.put(ref, @timestamp_index, current_ts) 42 | 43 | {current_ts, clock} 44 | end 45 | end 46 | 47 | ## Private 48 | 49 | defp get_atomic_ref do 50 | # By storing the atomic ref in the process state, we avoid all the 51 | # overhead of looking it up in an ETS table, or asking a process for it, 52 | # and it becomes almost as efficient as a thread local 53 | case Process.get(__MODULE__) do 54 | nil -> 55 | # The slow path, in this case we have to fetch the ref from the public ETS table 56 | [{_, ref}] = :ets.lookup(__MODULE__, :counters) 57 | Process.put(__MODULE__, ref) 58 | ref 59 | 60 | ref -> 61 | # The fast path 62 | ref 63 | end 64 | end 65 | 66 | # Obtain the current time in UTC as the number of microseconds since 67 | # the start of the gregorian calendar 68 | defp now do 69 | system_time = System.system_time(:microsecond) 70 | 71 | us = rem(system_time, 100_000) 72 | 73 | system_time 74 | |> :calendar.system_time_to_universal_time(:microsecond) 75 | |> :calendar.datetime_to_gregorian_seconds() 76 | |> Kernel.*(100_000) 77 | |> Kernel.+(us) 78 | end 79 | 80 | ## GenServer Implementation 81 | 82 | def start_link(_args), do: GenServer.start_link(__MODULE__, [], name: __MODULE__) 83 | 84 | def init(_) do 85 | ref = :atomics.new(2, signed: false) 86 | 87 | # Seed the clock index 88 | <> = :crypto.strong_rand_bytes(2) 89 | :atomics.put(ref, @clock_index, clock) 90 | 91 | # Store the atomic ref in a public ETS table to avoid bottlenecks 92 | tab = :ets.new(__MODULE__, [:public, :named_table, {:read_concurrency, true}]) 93 | 94 | :ets.insert(tab, {:counters, ref}) 95 | 96 | {:ok, tab} 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/macros.ex: -------------------------------------------------------------------------------- 1 | defmodule Uniq.Macros do 2 | otp_version = 3 | case String.split(to_string(:erlang.system_info(:otp_release)), ".", trim: true) do 4 | [maj] -> 5 | Version.parse!("#{maj}.0.0") 6 | 7 | [maj, min] -> 8 | Version.parse!("#{maj}.#{min}.0") 9 | 10 | [maj, min, patch] -> 11 | Version.parse!("#{maj}.#{min}.#{patch}") 12 | 13 | [maj, min, patch | _] -> 14 | Version.parse!("#{maj}.#{min}.#{patch}") 15 | end 16 | 17 | hex_encoding = Version.match?(otp_version, ">= 24.0.0", allow_pre: true) 18 | 19 | @builtins [ 20 | binary: [ 21 | encode_hex: hex_encoding, 22 | decode_hex: hex_encoding, 23 | ] 24 | ] 25 | 26 | defmacro defextension(module, do: body) do 27 | module = Macro.expand(module, __ENV__) 28 | 29 | if Code.ensure_loaded?(module) do 30 | quote do 31 | unquote(body) 32 | end 33 | end 34 | end 35 | 36 | defmacro defshim({_, meta, _} = function, [to: module], do: fallback) do 37 | {name, args} = 38 | case function do 39 | {:when, _, _} -> raise ArgumentError, "guards are not allowed in defshim/3" 40 | _ -> 41 | case Macro.decompose_call(function) do 42 | {_, _} = pair -> pair 43 | _ -> raise ArgumentError, "invalid syntax in defshim/3 #{Macro.to_string(function)}" 44 | end 45 | end 46 | 47 | if get_in(@builtins, [module, name]) == true do 48 | {:defdelegate, meta, [{name, meta, args}, [to: module]]} 49 | else 50 | quote do 51 | unquote(fallback) 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/uuid.ex: -------------------------------------------------------------------------------- 1 | defmodule Uniq.UUID do 2 | @moduledoc """ 3 | This module provides RFC 4122 compliant universally unique identifiers (UUIDs). 4 | 5 | See the [README](README.md) for general usage information. 6 | """ 7 | import Bitwise, except: ["~~~": 1, &&&: 2, |||: 2, "^^^": 2, <<<: 2, >>>: 2] 8 | import Kernel, except: [to_string: 1] 9 | 10 | defstruct [:format, :version, :variant, :time, :seq, :node, :bytes] 11 | 12 | @type t :: <<_::128>> 13 | @type formatted :: 14 | t 15 | | <<_::360>> 16 | | <<_::288>> 17 | | <<_::256>> 18 | | <<_::176>> 19 | @type format :: :default | :raw | :hex | :urn | :slug 20 | @type namespace :: :dns | :url | :oid | :x500 | nil | formatted 21 | 22 | @type info :: %__MODULE__{ 23 | format: :raw | :hex | :default | :urn | :slug, 24 | version: 1..8, 25 | variant: bitstring, 26 | time: non_neg_integer, 27 | seq: non_neg_integer, 28 | node: <<_::48>>, 29 | bytes: t 30 | } 31 | 32 | @formats [:default, :raw, :hex, :urn, :slug] 33 | @namespaces [:dns, :url, :oid, :x500, nil] 34 | 35 | # Namespaces 36 | @dns_namespace_id Base.decode16!("6ba7b8109dad11d180b400c04fd430c8", case: :lower) 37 | @url_namespace_id Base.decode16!("6ba7b8119dad11d180b400c04fd430c8", case: :lower) 38 | @oid_namespace_id Base.decode16!("6ba7b8129dad11d180b400c04fd430c8", case: :lower) 39 | @x500_namespace_id Base.decode16!("6ba7b8149dad11d180b400c04fd430c8", case: :lower) 40 | @nil_id <<0::128>> 41 | 42 | # Variants 43 | @reserved_ncs <<0::1>> 44 | @rfc_variant <<2::2>> 45 | @reserved_ms <<6::3>> 46 | @reserved_future <<7::3>> 47 | 48 | defmacrop bits(n), do: quote(do: bitstring - size(unquote(n))) 49 | defmacrop bytes(n), do: quote(do: binary - size(unquote(n))) 50 | defmacrop uint(n), do: quote(do: unsigned - integer - size(unquote(n))) 51 | defmacrop biguint(n), do: quote(do: big - unsigned - integer - size(unquote(n))) 52 | 53 | @doc """ 54 | Generates a UUID using the version 1 scheme, as described in RFC 4122 55 | 56 | This scheme is based on a few key properties: 57 | 58 | * A timestamp, based on the count of 100-nanosecond intervals since the start of 59 | the Gregorian calendar, i.e. October 15th, 1582, in Coordinated Universal Time (UTC). 60 | * A clock sequence number, used to ensure that UUIDs generated with the same timestamp 61 | are still unique, by incrementing the sequence each time a UUID is generated with the 62 | same timestamp as the last UUID that was generated. This sequence is initialized with 63 | random bytes at startup, to protect against conflicts. 64 | * A node identifier, which is based on the MAC address of one of the network interfaces 65 | on the system, or if unavailable, using random bytes. In our case, we specifically look 66 | for the first network interface returned by `:inet.getifaddrs/0` that is up, broadcastable, 67 | and has a hardware address, otherwise falling back to cryptographically strong random bytes. 68 | """ 69 | @spec uuid1() :: formatted 70 | @spec uuid1(format) :: formatted 71 | def uuid1(format \\ :default) do 72 | {time, clock} = Uniq.Generator.next() 73 | 74 | uuid1(time, clock, mac_address(), format) 75 | end 76 | 77 | @doc """ 78 | This function is the same as `uuid/1`, except the caller provides the clock sequence 79 | value and the node identifier (which must be a 6-byte binary). 80 | 81 | See `uuid/1` for details. 82 | """ 83 | @spec uuid1(clock_seq :: non_neg_integer, node :: <<_::48>>, format) :: formatted 84 | def uuid1(clock_seq, <>, format \\ :default) 85 | when is_integer(clock_seq) and format in @formats do 86 | {time, _} = Uniq.Generator.next() 87 | 88 | uuid1(time, clock_seq, node, format) 89 | end 90 | 91 | defp uuid1(time, clock_seq, node, format) do 92 | <> = <> 93 | 94 | # Encode version into high bits of timestamp 95 | thi = bor(thi, bsl(1, 12)) 96 | 97 | # Encode variant into high bits of clock sequence 98 | clock_hi = bsr(band(clock_seq, 0x3F00), 8) 99 | clock_hi = bor(clock_hi, 0x80) 100 | clock_lo = band(clock_seq, 0xFF) 101 | 102 | raw = <> 103 | 104 | format(raw, format) 105 | end 106 | 107 | @doc """ 108 | Generates a UUID using the version 3 scheme, as described in RFC 4122 109 | 110 | This scheme provides the means for generating UUIDs deterministically, 111 | given a namespace and a name. This means that with the same inputs, you 112 | get the same UUID as output. 113 | 114 | The main difference between this and the version 5 scheme, is that version 3 115 | uses MD5 for hashing, and version 5 uses SHA1. Both hashes are deprecated these 116 | days, but you should prefer version 5 unless otherwise required. 117 | 118 | In this scheme, the timestamp, clock sequence and node value are constructed 119 | from the namespace and name, as described in RFC 4122, Section 4.3. 120 | 121 | ## Namespaces 122 | 123 | You may choose one of several options for namespacing your UUIDs: 124 | 125 | 1. Use a predefined namespace. These are provided by RFC 4122 in order to provide 126 | namespacing for common types of names. See below. 127 | 2. Use your own namespace. For this, simply generate a UUID to represent the namespace. 128 | You may provide this UUID in whatever format is supported by `parse/1`. 129 | 3. Use `nil`. This is bound to a special-case UUID that has no intrinsic meaning, but is 130 | valid for use as a namespace. 131 | 132 | The set of predefined namespaces consist of the following: 133 | 134 | * `:dns`, intended for namespacing fully-qualified domain names 135 | * `:url`, intended for namespacing URLs 136 | * `:oid`, intended for namespacing ISO OIDs 137 | * `:x500`, intended for namespacing X.500 DNs (in DER or text output format) 138 | 139 | ## Notes 140 | 141 | One thing to be aware of with version 3 and 5 UUIDs, is that unlike version 1 and 6, 142 | the lexicographical ordering of UUIDs of generated one after the other, is entirely 143 | random, as the most significant bits are dependent upon the hash of the namespace and 144 | name, and thus not based on time or even the lexicographical ordering of the name. 145 | 146 | This is generally worth the tradeoff in favor of determinism, but it is something to 147 | be aware of. 148 | 149 | Likewise, since the generation is deterministic, care must be taken to ensure that you 150 | do not try to use the same name for two different objects within the same namespace. This 151 | should be obvious, but since the other schemes are _not_ sensitive in this way, it is worth 152 | calling out. 153 | """ 154 | @spec uuid3(namespace, name :: binary) :: formatted 155 | @spec uuid3(namespace, name :: binary, format) :: formatted 156 | def uuid3(namespace, name, format \\ :default) 157 | when (namespace in @namespaces or is_binary(namespace)) and is_binary(name) and 158 | format in @formats do 159 | namespaced_uuid(3, :md5, namespace, name, format) 160 | end 161 | 162 | @doc """ 163 | Generates a UUID using the version 4 scheme, as described in RFC 4122 164 | 165 | This scheme is like the version 1 scheme, except it uses randomly generated data 166 | for the timestamp, clock sequence, and node fields. 167 | 168 | This scheme is the closest you can get to truly unique identifiers, as they are based 169 | on truly random (or pseudo-random) data, so the chances of generating the same UUID 170 | twice is astronomically small. 171 | 172 | ## Notes 173 | 174 | The version 4 scheme does have some deficiencies. Namely, since they are based on random 175 | data, the lexicographical ordering of the resulting UUID is itself random, which can play havoc 176 | with database indices should you choose to use UUIDs for primary keys. 177 | 178 | It is strongly recommended to consider the version 6 scheme instead. They are almost the 179 | same as a version 1 UUID, but with improved semantics that combine some of the beneficial 180 | traits of version 4 UUIDs without the lexicographical ordering downsides. The only caveat 181 | to that recommendation is if you need to pass them through a system that inspects the UUID 182 | encoding itself and doesn't have preliminary support for version 6. 183 | """ 184 | @spec uuid4() :: formatted 185 | @spec uuid4(format) :: formatted 186 | def uuid4(format \\ :default) when format in @formats do 187 | <> = :crypto.strong_rand_bytes(16) 188 | 189 | raw = <> 190 | 191 | format(raw, format) 192 | end 193 | 194 | @doc """ 195 | Generates a UUID using the version 5 scheme, as described in RFC 4122 196 | 197 | This scheme provides the means for generating UUIDs deterministically, 198 | given a namespace and a name. This means that with the same inputs, you 199 | get the same UUID as output. 200 | 201 | The main difference between this and the version 5 scheme, is that version 3 202 | uses MD5 for hashing, and version 5 uses SHA1. Both hashes are deprecated these 203 | days, but you should prefer version 5 unless otherwise required. 204 | 205 | In this scheme, the timestamp, clock sequence and node value are constructed 206 | from the namespace and name, as described in RFC 4122, Section 4.3. 207 | 208 | ## Namespaces 209 | 210 | You may choose one of several options for namespacing your UUIDs: 211 | 212 | 1. Use a predefined namespace. These are provided by RFC 4122 in order to provide 213 | namespacing for common types of names. See below. 214 | 2. Use your own namespace. For this, simply generate a UUID to represent the namespace. 215 | You may provide this UUID in whatever format is supported by `parse/1`. 216 | 3. Use `nil`. This is bound to a special-case UUID that has no intrinsic meaning, but is 217 | valid for use as a namespace. 218 | 219 | The set of predefined namespaces consist of the following: 220 | 221 | * `:dns`, intended for namespacing fully-qualified domain names 222 | * `:url`, intended for namespacing URLs 223 | * `:oid`, intended for namespacing ISO OIDs 224 | * `:x500`, intended for namespacing X.500 DNs (in DER or text output format) 225 | 226 | ## Notes 227 | 228 | One thing to be aware of with version 3 and 5 UUIDs, is that unlike version 1 and 6, 229 | the lexicographical ordering of UUIDs of generated one after the other, is entirely 230 | random, as the most significant bits are dependent upon the hash of the namespace and 231 | name, and thus not based on time or even the lexicographical ordering of the name. 232 | 233 | This is generally worth the tradeoff in favor of determinism, but it is something to 234 | be aware of. 235 | 236 | Likewise, since the generation is deterministic, care must be taken to ensure that you 237 | do not try to use the same name for two different objects within the same namespace. This 238 | should be obvious, but since the other schemes are _not_ sensitive in this way, it is worth 239 | calling out. 240 | """ 241 | @spec uuid5(namespace, name :: binary) :: formatted 242 | @spec uuid5(namespace, name :: binary, format) :: formatted 243 | def uuid5(namespace, name, format \\ :default) 244 | when (namespace in @namespaces or is_binary(namespace)) and is_binary(name) and 245 | format in @formats do 246 | namespaced_uuid(5, :sha, namespace, name, format) 247 | end 248 | 249 | @doc """ 250 | Generates a UUID using the proposed version 6 scheme, found 251 | [here](https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-04#section-5.1). 252 | This is a draft extension of RFC 4122, but has not yet been formally accepted. 253 | 254 | Version 6 provides the following benefits over versions 1 and 4: 255 | 256 | * Like version 1, it is time-based, but unlike version 1, it is naturally sortable by time 257 | in its raw binary encoded form 258 | * Like version 4, it provides better guarantees of uniqueness and privacy, by basing itself 259 | on random or pseudo-random data, rather than MAC addresses and other potentially sensitive 260 | information. 261 | * Unlike version 4, which tends to interact poorly with database indices due to being derived 262 | entirely from random or pseudo-random data; version 6 ensures that the most significant bits 263 | of the binary encoded form are a 1:1 match with the most significant bits of the timestamp on 264 | which it was derived. This guarantees that version 6 UUIDs are naturally sortable in the order 265 | in which they were generated (with some randomness among those which are generated at the same 266 | time). 267 | 268 | There have been a number of similar proposals that address the same set of flaws. For example: 269 | 270 | * [KSUID](https://github.com/segmentio/ksuid) 271 | * [ULID](https://github.com/ulid/spec) 272 | 273 | Systems that do not involve legacy UUIDv1 SHOULD consider using UUIDv7 instead. 274 | """ 275 | @spec uuid6() :: formatted 276 | @spec uuid6(format) :: formatted 277 | def uuid6(format \\ :default) when format in @formats do 278 | {time, clock} = Uniq.Generator.next() 279 | node = :crypto.strong_rand_bytes(6) 280 | 281 | # Deconstruct timestamp 282 | <> = <> 283 | 284 | # Encode the version to the most significant bits of the last octet of the timestamp 285 | tlo_and_version = <<6::4, tlo::12>> 286 | 287 | # Encode the variant in the most significant bits of the clock sequence 288 | clock_seq = <<@rfc_variant, clock::biguint(14)>> 289 | 290 | raw = <> 291 | 292 | format(raw, format) 293 | end 294 | 295 | @doc """ 296 | Generates a UUID using the proposed version 7 scheme, found 297 | [here](https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-04#section-5.2). 298 | This is a draft extension of RFC 4122, but has not yet been formally accepted. 299 | 300 | UUID version 7 features a time-ordered value field derived from the widely implemented and well 301 | known Unix Epoch timestamp source, the number of milliseconds seconds since midnight 1 Jan 1970 302 | UTC, leap seconds excluded. As well as improved entropy characteristics over versions 1 or 6. 303 | 304 | Implementations SHOULD utilize UUID version 7 over UUID version 1 and 6 if possible. 305 | """ 306 | @spec uuid7() :: formatted 307 | @spec uuid7(format) :: formatted 308 | def uuid7(format \\ :default) when format in @formats do 309 | time = System.system_time(:millisecond) 310 | <> = :crypto.strong_rand_bytes(10) 311 | 312 | raw = <> 313 | 314 | format(raw, format) 315 | end 316 | 317 | defp namespaced_uuid(version, algorithm, namespace, name, format) do 318 | id = namespace_id(namespace) 319 | 320 | <> = hash(algorithm, id <> name) 321 | 322 | raw = <> 323 | 324 | format(raw, format) 325 | end 326 | 327 | @doc """ 328 | Like `info/1`, but raises if the input UUID is invalid. 329 | """ 330 | @spec info!(binary, :struct) :: info | no_return 331 | @spec info!(binary, :keyword) :: Keyword.t() | no_return 332 | def info!(bin, style \\ :struct) 333 | 334 | def info!(bin, style) when is_binary(bin) do 335 | with {:ok, info} <- info(bin, style) do 336 | info 337 | else 338 | {:error, reason} -> 339 | raise ArgumentError, message: "invalid uuid: #{inspect(reason)}" 340 | end 341 | end 342 | 343 | def info!(_, _) do 344 | raise ArgumentError, message: "invalid uuid: :invalid_format" 345 | end 346 | 347 | @doc """ 348 | This function parses the given UUID, in any of the supported encodings/formats, and produces 349 | the information gleaned from the encoded data. 350 | 351 | Two styles of information are supported, depending on whether the function is called via 352 | the compatibility shim for `:elixir_uuid`, or directly. You may pass `:struct` or `:keyword` 353 | manually if you wish to express a preference for one style or the other. 354 | 355 | The `:struct` form is the UUID structure used internally by this library, and it contains all 356 | of the information needed to re-encode the UUID as binary. 357 | 358 | The `:keyword` form matches 1:1 the keyword list produced by `UUID.info/1` provided by the 359 | `:elixir_uuid` library, and it contains slightly less information, but is useful for compatibility 360 | with legacy code that operates on that structure. 361 | 362 | # Examples 363 | 364 | iex> Uniq.UUID.info("870df8e8-3107-4487-8316-81e089b8c2cf", :keyword) 365 | {:ok, [uuid: "870df8e8-3107-4487-8316-81e089b8c2cf", 366 | binary: <<135, 13, 248, 232, 49, 7, 68, 135, 131, 22, 129, 224, 137, 184, 194, 207>>, 367 | type: :default, 368 | version: 4, 369 | variant: :rfc4122]} 370 | 371 | iex> Uniq.UUID.info("870df8e8-3107-4487-8316-81e089b8c2cf") 372 | {:ok, %Uniq.UUID{ 373 | format: :default, 374 | version: 4, 375 | variant: <<2::2>>, 376 | time: 326283406408022248, 377 | seq: 790, 378 | node: <<129, 224, 137, 184, 194, 207>>, 379 | bytes: <<135, 13, 248, 232, 49, 7, 68, 135, 131, 22, 129, 224, 137, 184, 194, 207>>, 380 | }} 381 | 382 | """ 383 | @spec info(binary, :struct) :: {:ok, info} | {:error, term} 384 | @spec info(binary, :keyword) :: {:ok, Keyword.t()} | {:error, term} 385 | def info(bin, style \\ :struct) 386 | 387 | # Compatibility with :elixir_uuid's info 388 | def info(bin, :keyword) when is_binary(bin) do 389 | with {:ok, uuid} <- parse(bin) do 390 | {:ok, 391 | [ 392 | uuid: uuid |> to_string() |> String.downcase(), 393 | binary: uuid.bytes, 394 | type: uuid.format, 395 | version: uuid.version, 396 | variant: format_variant(uuid.variant) 397 | ]} 398 | end 399 | end 400 | 401 | def info(bin, :struct) when is_binary(bin) do 402 | parse(bin) 403 | end 404 | 405 | def info(_, style) when style in [:keyword, :struct], 406 | do: {:error, :invalid_format} 407 | 408 | @doc """ 409 | Returns true if the given string is a valid UUID. 410 | 411 | ## Options 412 | 413 | * `strict: boolean`, if true, requires strict RFC 4122 conformance, 414 | i.e. version 6 is considered invalid 415 | """ 416 | @spec valid?(binary) :: boolean 417 | @spec valid?(binary, Keyword.t()) :: boolean 418 | def valid?(bin, opts \\ []) 419 | 420 | def valid?(bin, opts) do 421 | strict? = Keyword.get(opts, :strict, false) 422 | 423 | case parse(bin) do 424 | {:ok, %__MODULE__{version: 6}} when strict? -> 425 | false 426 | 427 | {:ok, _} -> 428 | true 429 | 430 | {:error, _} -> 431 | false 432 | end 433 | end 434 | 435 | @doc """ 436 | Parses a `#{__MODULE__}` from a binary. 437 | 438 | Supported formats include human-readable strings, as well as 439 | the raw binary form of the UUID. 440 | 441 | ## Examples 442 | 443 | iex> {:ok, uuid} = Uniq.UUID.parse("f81d4fae-7dec-11d0-a765-00a0c91e6bf6") 444 | {:ok, %Uniq.UUID{ 445 | bytes: <<248, 29, 79, 174, 125, 236, 17, 208, 167, 101, 0, 160, 201, 30, 107, 246>>, 446 | format: :default, 447 | node: <<0, 160, 201, 30, 107, 246>>, 448 | seq: 10085, 449 | time: 130742845922168750, 450 | variant: <<2::size(2)>>, 451 | version: 1 452 | }} 453 | ...> {:ok, %Uniq.UUID{uuid | format: :urn}} == Uniq.UUID.parse("urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6") 454 | true 455 | 456 | iex> match?({:ok, %Uniq.UUID{format: :default, version: 1}}, Uniq.UUID.uuid1() |> Uniq.UUID.parse()) 457 | true 458 | """ 459 | @spec parse(binary) :: {:ok, info} | {:error, term} 460 | def parse(bin) 461 | 462 | def parse("urn:uuid:" <> uuid) do 463 | with {:ok, uuid} <- parse(uuid) do 464 | {:ok, %__MODULE__{uuid | format: :urn}} 465 | end 466 | end 467 | 468 | def parse(<<_::128>> = bin), 469 | do: parse_raw(bin, %__MODULE__{format: :raw}) 470 | 471 | def parse(<>) do 472 | bin 473 | |> decode_hex() 474 | |> parse_raw(%__MODULE__{format: :hex}) 475 | rescue 476 | ArgumentError -> 477 | {:error, {:invalid_format, :hex}} 478 | end 479 | 480 | def parse(<>) do 481 | with {:ok, bin} <- Base.decode16(a <> b <> c <> d <> e, case: :mixed) do 482 | parse_raw(bin, %__MODULE__{format: :default}) 483 | else 484 | :error -> 485 | {:error, {:invalid_format, :default}} 486 | end 487 | end 488 | 489 | def parse(<>) do 490 | with {:ok, value} <- Base.url_decode64(uuid <> "==") do 491 | parse_raw(value, %__MODULE__{format: :slug}) 492 | else 493 | _ -> 494 | {:error, {:invalid_format, :slug}} 495 | end 496 | end 497 | 498 | def parse(_bin), do: {:error, :invalid_format} 499 | 500 | # Parse version 501 | defp parse_raw(<<_::48, version::uint(4), _::bitstring>> = bin, acc) do 502 | case version do 503 | v when v in [1, 3, 4, 5, 6, 7] -> 504 | with {:ok, uuid} <- parse_raw(version, bin, acc) do 505 | {:ok, %__MODULE__{uuid | bytes: bin}} 506 | end 507 | 508 | _ when bin == @nil_id -> 509 | {:ok, %__MODULE__{acc | bytes: @nil_id}} 510 | 511 | _ -> 512 | {:error, {:unknown_version, version}} 513 | end 514 | end 515 | 516 | # Parse variant 517 | defp parse_raw(version, <>, acc), 518 | do: parse_raw(version, @reserved_ncs, time, rest, acc) 519 | 520 | defp parse_raw(version, <>, acc), 521 | do: parse_raw(version, @rfc_variant, time, rest, acc) 522 | 523 | defp parse_raw(version, <>, acc), 524 | do: parse_raw(version, @reserved_ms, time, rest, acc) 525 | 526 | defp parse_raw(version, <>, acc), 527 | do: parse_raw(version, @reserved_future, time, rest, acc) 528 | 529 | defp parse_raw(_version, <<_time::64, variant::bits(3), _rest::bits(61)>>, _acc) do 530 | {:error, {:unknown_variant, variant}} 531 | end 532 | 533 | for variant <- [@reserved_ncs, @rfc_variant, @reserved_ms, @reserved_future] do 534 | variant_size = bit_size(variant) 535 | variant = Macro.escape(variant) 536 | 537 | # Parses RFC 4122, version 1-5 uuids 538 | defp parse_raw(version, unquote(variant), time, rest, acc) when version < 6 do 539 | variant_size = unquote(variant_size) 540 | clock_hi_size = 8 - variant_size 541 | clock_size = 8 + clock_hi_size 542 | 543 | with <> <- 544 | <>, 545 | <> <- 546 | <>, 547 | <> <- 548 | rest, 549 | <> <- 550 | <> do 551 | {:ok, 552 | %__MODULE__{ 553 | acc 554 | | version: version, 555 | variant: unquote(variant), 556 | time: timestamp, 557 | seq: clock, 558 | node: node 559 | }} 560 | else 561 | other -> 562 | {:error, {:invalid_format, other, variant_size, clock_hi_size, clock_size}} 563 | end 564 | end 565 | end 566 | 567 | # Parses proposed version 7 uuids 568 | defp parse_raw(7, <<1::1, 0::1>> = variant, time, rest, acc) do 569 | with <> <- <>, 570 | <<_rand_b::62>> <- rest do 571 | {:ok, 572 | %__MODULE__{ 573 | acc 574 | | version: 7, 575 | variant: variant, 576 | time: time 577 | }} 578 | else 579 | _ -> 580 | {:error, {:invalid_format, :v7}} 581 | end 582 | end 583 | 584 | # Parses proposed version 6 uuids, which are very much like version 1, but with some field ordering changes 585 | defp parse_raw(6, <<1::1, 0::1>> = variant, time, rest, acc) do 586 | with <> <- <>, 587 | <> <- <>, 588 | <> <- 589 | rest do 590 | {:ok, 591 | %__MODULE__{ 592 | acc 593 | | version: 6, 594 | variant: variant, 595 | time: timestamp, 596 | seq: clock, 597 | node: node 598 | }} 599 | else 600 | _ -> 601 | {:error, {:invalid_format, :v6}} 602 | end 603 | end 604 | 605 | defp parse_raw(6, variant, _time, _rest, _acc), do: {:error, {:invalid_variant, variant}} 606 | 607 | # Handles proposed version 7 and 8 uuids 608 | defp parse_raw(version, _variant, _time, _rest, _acc), 609 | do: {:error, {:unsupported_version, version}} 610 | 611 | @doc """ 612 | Formats a `#{__MODULE__}` as a string, using the format it was originally generated with. 613 | 614 | See `to_string/2` if you want to specify what format to produce. 615 | """ 616 | @spec to_string(formatted | info) :: String.t() 617 | def to_string(uuid) 618 | 619 | def to_string(<>), 620 | do: format(raw, :default) 621 | 622 | def to_string(uuid) when is_binary(uuid) do 623 | uuid 624 | |> string_to_binary!() 625 | |> format(:default) 626 | end 627 | 628 | def to_string(%__MODULE__{bytes: raw, format: format}), 629 | do: format(raw, format) 630 | 631 | @doc """ 632 | Same as `to_string/1`, except you can specify the desired format. 633 | 634 | The `format` can be one of the following: 635 | 636 | * `:default`, produces strings like `"f81d4fae-7dec-11d0-a765-00a0c91e6bf6"` 637 | * `:urn`, produces strings like `"urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6"` 638 | * `:hex`, produces strings like `"f81d4fae7dec11d0a76500a0c91e6bf6"` 639 | * `:slug`, produces strings like `"-B1Prn3sEdCnZQCgyR5r9g=="` 640 | * `:raw`, produces the raw binary encoding of the uuid in 128 bits 641 | """ 642 | @spec to_string(formatted | info, format) :: String.t() 643 | def to_string(uuid, format) 644 | 645 | def to_string(<>, format) when format in @formats, 646 | do: format(raw, format) 647 | 648 | def to_string(uuid, format) when is_binary(uuid) and format in @formats do 649 | uuid 650 | |> string_to_binary!() 651 | |> format(format) 652 | end 653 | 654 | def to_string(%__MODULE__{bytes: raw}, format) when format in @formats, 655 | do: format(raw, format) 656 | 657 | @doc """ 658 | This function takes a UUID string in any of the formats supported by `to_string/1`, 659 | and returns the raw, binary-encoded form. 660 | """ 661 | @spec string_to_binary!(String.t()) :: t | no_return 662 | def string_to_binary!(str) 663 | 664 | def string_to_binary!(<<_::128>> = uuid), do: uuid 665 | 666 | def string_to_binary!(<>) do 667 | decode_hex(hex) 668 | end 669 | 670 | def string_to_binary!( 671 | <> 672 | ) do 673 | decode_hex(a <> b <> c <> d <> e) 674 | end 675 | 676 | def string_to_binary!(<>) do 677 | with {:ok, value} <- Base.url_decode64(slug <> "==") do 678 | value 679 | else 680 | _ -> 681 | raise ArgumentError, message: "invalid uuid string" 682 | end 683 | end 684 | 685 | def string_to_binary!(_) do 686 | raise ArgumentError, message: "invalid uuid string" 687 | end 688 | 689 | @doc """ 690 | Compares two UUIDs, using their canonical 128-bit integer form, as described in RFC 4122. 691 | 692 | You may provide the UUIDs in either string, binary, or as a `Uniq.UUID` struct. 693 | """ 694 | @spec compare(String.t() | info, String.t() | info) :: :lt | :eq | :gt 695 | def compare(a, b) 696 | 697 | def compare(%__MODULE__{} = a, %__MODULE__{} = b) do 698 | a = to_canonical_integer(a) 699 | b = to_canonical_integer(b) 700 | 701 | do_compare(a, b) 702 | end 703 | 704 | def compare(%__MODULE__{} = a, <>) do 705 | a = to_canonical_integer(a) 706 | 707 | do_compare(a, b) 708 | end 709 | 710 | def compare(%__MODULE__{} = a, b) when is_binary(b) do 711 | a = to_canonical_integer(a) 712 | b = string_to_binary!(b) 713 | 714 | do_compare(a, b) 715 | end 716 | 717 | def compare(<>, %__MODULE__{} = b) do 718 | b = to_canonical_integer(b) 719 | 720 | do_compare(a, b) 721 | end 722 | 723 | def compare(a, %__MODULE__{} = b) when is_binary(a) do 724 | a = string_to_binary!(a) 725 | b = to_canonical_integer(b) 726 | 727 | do_compare(a, b) 728 | end 729 | 730 | def compare(<>, <>), 731 | do: do_compare(a, b) 732 | 733 | def compare(a, b) when is_binary(a) and is_binary(b) do 734 | a = to_string(a) 735 | b = to_string(b) 736 | 737 | do_compare(a, b) 738 | end 739 | 740 | defp do_compare(a, b) do 741 | cond do 742 | a < b -> 743 | :lt 744 | 745 | a == b -> 746 | :eq 747 | 748 | :else -> 749 | :gt 750 | end 751 | end 752 | 753 | defp to_canonical_integer(%__MODULE__{bytes: <>}) do 754 | value 755 | end 756 | 757 | @doc false 758 | def format(raw, format) 759 | 760 | def format(raw, :raw), do: raw 761 | def format(raw, :default), do: format_default(raw) 762 | def format(raw, :hex), do: encode_hex(raw) 763 | def format(raw, :urn), do: "urn:uuid:#{format(raw, :default)}" 764 | def format(raw, :slug), do: Base.url_encode64(raw, padding: false) 765 | 766 | @compile {:inline, [format_default: 1]} 767 | 768 | defp format_default(<< 769 | a1::4, 770 | a2::4, 771 | a3::4, 772 | a4::4, 773 | a5::4, 774 | a6::4, 775 | a7::4, 776 | a8::4, 777 | b1::4, 778 | b2::4, 779 | b3::4, 780 | b4::4, 781 | c1::4, 782 | c2::4, 783 | c3::4, 784 | c4::4, 785 | d1::4, 786 | d2::4, 787 | d3::4, 788 | d4::4, 789 | e1::4, 790 | e2::4, 791 | e3::4, 792 | e4::4, 793 | e5::4, 794 | e6::4, 795 | e7::4, 796 | e8::4, 797 | e9::4, 798 | e10::4, 799 | e11::4, 800 | e12::4 801 | >>) do 802 | <> 805 | end 806 | 807 | @doc false 808 | 809 | defp format_variant(@reserved_future), do: :reserved_future 810 | defp format_variant(@reserved_ms), do: :reserved_microsoft 811 | defp format_variant(@rfc_variant), do: :rfc4122 812 | defp format_variant(@reserved_ncs), do: :reserved_ncs 813 | defp format_variant(_), do: :unknown 814 | 815 | defp mac_address do 816 | candidate_interface? = fn {_if, info} -> 817 | flags = Keyword.get(info, :flags, []) 818 | 819 | Enum.member?(flags, :up) and Enum.member?(flags, :broadcast) and 820 | Keyword.has_key?(info, :hwaddr) 821 | end 822 | 823 | with {:ok, interfaces} <- :inet.getifaddrs(), 824 | {_if, info} <- Enum.find(interfaces, candidate_interface?) do 825 | IO.iodata_to_binary(info[:hwaddr]) 826 | else 827 | _ -> 828 | # In lieu of a MAC address, we can generate an equivalent number of random bytes 829 | <> = :crypto.strong_rand_bytes(6) 830 | # Ensure the multicast bit is set, as per RFC 4122 831 | <> 832 | end 833 | end 834 | 835 | defp hash(:md5, data), do: :crypto.hash(:md5, data) 836 | defp hash(:sha, data), do: :binary.part(:crypto.hash(:sha, data), 0, 16) 837 | 838 | defp namespace_id(:dns), do: @dns_namespace_id 839 | defp namespace_id(:url), do: @url_namespace_id 840 | defp namespace_id(:oid), do: @oid_namespace_id 841 | defp namespace_id(:x500), do: @x500_namespace_id 842 | defp namespace_id(nil), do: @nil_id 843 | 844 | defp namespace_id(<<_::128>> = ns), do: ns 845 | 846 | defp namespace_id(<>) do 847 | with {:ok, raw} <- Base.decode16(ns, case: :mixed) do 848 | raw 849 | else 850 | _ -> 851 | invalid_namespace!() 852 | end 853 | end 854 | 855 | defp namespace_id( 856 | <> 857 | ) do 858 | with {:ok, raw} <- Base.decode16(a <> b <> c <> d <> e, case: :mixed) do 859 | raw 860 | else 861 | _ -> 862 | invalid_namespace!() 863 | end 864 | end 865 | 866 | defp namespace_id(<>) do 867 | with {:ok, raw} <- Base.url_decode64(ns <> "==") do 868 | raw 869 | else 870 | _ -> 871 | invalid_namespace!() 872 | end 873 | end 874 | 875 | defp namespace_id(_ns), do: invalid_namespace!() 876 | 877 | defp invalid_namespace!, 878 | do: 879 | raise(ArgumentError, 880 | message: "expected a valid namespace atom (:dns, :url, :oid, :x500), or a UUID string" 881 | ) 882 | 883 | import Uniq.Macros, only: [defextension: 2, defshim: 3] 884 | 885 | @compile {:inline, [encode_hex: 1, decode_hex: 1]} 886 | 887 | defshim encode_hex(bin), to: :binary do 888 | defp encode_hex(bin), do: IO.iodata_to_binary(for <>, do: e(bs)) 889 | end 890 | 891 | defshim decode_hex(bin), to: :binary do 892 | defp decode_hex( 893 | <> 895 | ) do 896 | <> 900 | catch 901 | :throw, char -> 902 | raise ArgumentError, message: "#{inspect(<>)} is not valid hex" 903 | end 904 | 905 | @compile {:inline, d: 1} 906 | 907 | defp d(?0), do: 0 908 | defp d(?1), do: 1 909 | defp d(?2), do: 2 910 | defp d(?3), do: 3 911 | defp d(?4), do: 4 912 | defp d(?5), do: 5 913 | defp d(?6), do: 6 914 | defp d(?7), do: 7 915 | defp d(?8), do: 8 916 | defp d(?9), do: 9 917 | defp d(?A), do: 10 918 | defp d(?B), do: 11 919 | defp d(?C), do: 12 920 | defp d(?D), do: 13 921 | defp d(?E), do: 14 922 | defp d(?F), do: 15 923 | defp d(?a), do: 10 924 | defp d(?b), do: 11 925 | defp d(?c), do: 12 926 | defp d(?d), do: 13 927 | defp d(?e), do: 14 928 | defp d(?f), do: 15 929 | defp d(char), do: throw(char) 930 | end 931 | 932 | @compile {:inline, e: 1} 933 | 934 | defp e(0), do: ?0 935 | defp e(1), do: ?1 936 | defp e(2), do: ?2 937 | defp e(3), do: ?3 938 | defp e(4), do: ?4 939 | defp e(5), do: ?5 940 | defp e(6), do: ?6 941 | defp e(7), do: ?7 942 | defp e(8), do: ?8 943 | defp e(9), do: ?9 944 | defp e(10), do: ?a 945 | defp e(11), do: ?b 946 | defp e(12), do: ?c 947 | defp e(13), do: ?d 948 | defp e(14), do: ?e 949 | defp e(15), do: ?f 950 | 951 | ## Ecto 952 | 953 | defextension Ecto.ParameterizedType do 954 | use Ecto.ParameterizedType 955 | 956 | @doc false 957 | @impl Ecto.ParameterizedType 958 | def init(opts) do 959 | schema = Keyword.fetch!(opts, :schema) 960 | field = Keyword.fetch!(opts, :field) 961 | format = Keyword.get(opts, :format, :default) 962 | dump = Keyword.get(opts, :dump, :raw) 963 | type = Keyword.get(opts, :type) 964 | 965 | unless format in @formats do 966 | raise ArgumentError, 967 | message: 968 | "invalid :format option, expected one of #{Enum.join(@formats, ",")}; got #{inspect(format)}" 969 | end 970 | 971 | unless dump in @formats do 972 | raise ArgumentError, 973 | message: 974 | "invalid :dump option, expected one of #{Enum.join(@formats, ",")}; got #{inspect(format)}" 975 | end 976 | 977 | version = Keyword.get(opts, :version, 4) 978 | 979 | unless version in [1, 3, 4, 5, 6, 7] do 980 | raise ArgumentError, 981 | message: 982 | "invalid uuid version, expected one of 1, 3, 4, 5, 6, or 7; got #{inspect(version)}" 983 | end 984 | 985 | namespace = Keyword.get(opts, :namespace) 986 | 987 | case namespace do 988 | nil when version in [3, 5] -> 989 | raise ArgumentError, 990 | message: "you must set :namespace to a valid uuid when :version is 3 or 5" 991 | 992 | nil -> 993 | :ok 994 | 995 | ns when ns in [:dns, :url, :oid, :x500] -> 996 | raise ArgumentError, 997 | message: 998 | "you must set :namespace to a uuid, the predefined namespaces are not permitted here" 999 | 1000 | ns when is_binary(ns) -> 1001 | :ok 1002 | 1003 | ns -> 1004 | raise ArgumentError, 1005 | message: "expected :namespace to be a binary, but got #{inspect(ns)}" 1006 | end 1007 | 1008 | %{ 1009 | schema: schema, 1010 | field: field, 1011 | format: format, 1012 | dump: dump, 1013 | type: type, 1014 | version: version, 1015 | namespace: namespace 1016 | } 1017 | end 1018 | 1019 | @doc false 1020 | @impl Ecto.ParameterizedType 1021 | def type(%{type: nil, dump: :raw}), do: :binary 1022 | def type(%{type: nil}), do: :string 1023 | def type(%{type: t}), do: t 1024 | 1025 | # This is provided as a helper for autogenerating version 3 or 5 uuids 1026 | @doc false 1027 | @impl Ecto.ParameterizedType 1028 | def autogenerate(%{format: format, version: version, namespace: namespace}) do 1029 | case version do 1030 | 1 -> 1031 | uuid1(format) 1032 | 1033 | 4 -> 1034 | uuid4(format) 1035 | 1036 | 6 -> 1037 | uuid6(format) 1038 | 1039 | 7 -> 1040 | uuid7(format) 1041 | 1042 | v when v in [3, 5] -> 1043 | # 64 bits of entropy should be more than sufficient, since the total entropy 1044 | # of the input here is 192 bits, which we get from the namespace (128 bits) + the name (64 bits). 1045 | # That is then represented using only 128 bits (an entire MD5 hash, or 128 of the 1046 | # 160 bits of a SHA1 hash). In short, its doubtful that using more than 8 bytes 1047 | # of random data is going to have any appreciable benefit on uniqueness. Discounting 1048 | # the namespace, the total entropy is only 64 bits, which in practice is constrained 1049 | # by the hash itself, which is then further constrained by the fact that 6 bits of the 1050 | # UUID are reserved for version and variant information. In short, even though we are 1051 | # assuming a namespace that can contain 2^64 unique values, in practice it is less than 1052 | # that, though it still leaves room for an astronomical number of unique identifiers. 1053 | name = :crypto.strong_rand_bytes(8) 1054 | 1055 | case v do 1056 | 3 -> uuid3(namespace, name, format) 1057 | 5 -> uuid5(namespace, name, format) 1058 | end 1059 | end 1060 | end 1061 | 1062 | @doc false 1063 | @impl Ecto.ParameterizedType 1064 | def cast(data, params) 1065 | 1066 | def cast(uuid, %{format: format}) when is_binary(uuid) do 1067 | {:ok, to_string(uuid, format)} 1068 | rescue 1069 | ArgumentError -> 1070 | :error 1071 | end 1072 | 1073 | def cast(%__MODULE__{} = uuid, %{format: format}), 1074 | do: {:ok, to_string(uuid, format)} 1075 | 1076 | def cast(nil, _params), do: {:ok, nil} 1077 | 1078 | def cast(_, _params), do: :error 1079 | 1080 | @doc false 1081 | @impl Ecto.ParameterizedType 1082 | def load(value, loader, params) 1083 | 1084 | def load(uuid, _loader, %{format: format}) when is_binary(uuid) do 1085 | {:ok, to_string(uuid, format)} 1086 | rescue 1087 | ArgumentError -> 1088 | :error 1089 | end 1090 | 1091 | def load(nil, _loader, _params), 1092 | do: {:ok, nil} 1093 | 1094 | @doc false 1095 | @impl Ecto.ParameterizedType 1096 | def dump(value, dumper, params) 1097 | 1098 | def dump(%__MODULE__{} = uuid, _dumper, %{dump: format}), 1099 | do: {:ok, to_string(uuid, format)} 1100 | 1101 | def dump(uuid, _dumper, %{dump: format}) when is_binary(uuid) do 1102 | {:ok, to_string(uuid, format)} 1103 | rescue 1104 | ArgumentError -> 1105 | :error 1106 | end 1107 | 1108 | def dump(nil, _dumper, _params), 1109 | do: {:ok, nil} 1110 | 1111 | @doc false 1112 | @impl Ecto.ParameterizedType 1113 | def embed_as(_format, _params), do: :self 1114 | 1115 | @doc false 1116 | @impl Ecto.ParameterizedType 1117 | def equal?(a, b, params) 1118 | 1119 | def equal?(nil, nil, _), do: true 1120 | def equal?(nil, b, _), do: to_string(b, :raw) == @nil_id 1121 | def equal?(a, nil, _), do: to_string(a, :raw) == @nil_id 1122 | def equal?(a, b, _), do: compare(to_string(a), to_string(b)) == :eq 1123 | end 1124 | 1125 | defimpl String.Chars do 1126 | alias Uniq.UUID 1127 | 1128 | def to_string(uuid), do: UUID.to_string(uuid) 1129 | end 1130 | 1131 | defimpl Inspect do 1132 | import Inspect.Algebra 1133 | 1134 | def inspect(%Uniq.UUID{bytes: bytes, version: version}, opts) do 1135 | # Allow overriding the format in which UUIDs are displayed via custom inspect options 1136 | format = Keyword.get(opts.custom_options, :format, :default) 1137 | uuid = Uniq.UUID.to_string(bytes, format) 1138 | 1139 | concat(["#UUIDv", Kernel.to_string(version), "<", uuid, ">"]) 1140 | end 1141 | end 1142 | end 1143 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Uniq.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :uniq, 7 | version: "0.6.1", 8 | elixir: "~> 1.13", 9 | description: description(), 10 | package: package(), 11 | start_permanent: Mix.env() == :prod, 12 | deps: deps(), 13 | aliases: aliases(), 14 | elixirc_paths: elixirc_paths(Mix.env()), 15 | preferred_cli_env: [ 16 | bench: :bench, 17 | docs: :docs, 18 | "hex.publish": :docs 19 | ], 20 | name: "Uniq", 21 | source_url: "https://github.com/bitwalker/uniq", 22 | homepage_url: "http://github.com/bitwalker/uniq", 23 | docs: [ 24 | main: "readme", 25 | api_reference: false, 26 | extra_section: "Extras", 27 | extras: [ 28 | {:"README.md", [title: "About"]}, 29 | "guides/using_with_ecto.md", 30 | "guides/migration_from_elixir_uuid.md", 31 | {:"LICENSE.md", [title: "License"]} 32 | ] 33 | ] 34 | ] 35 | end 36 | 37 | # Run "mix help compile.app" to learn about applications. 38 | def application do 39 | [ 40 | mod: {Uniq.App, []}, 41 | extra_applications: [:crypto] 42 | ] 43 | end 44 | 45 | defp aliases do 46 | [bench: &run_bench/1] 47 | end 48 | 49 | defp elixirc_paths(:test), do: ["lib", "test/support"] 50 | defp elixirc_paths(_), do: ["lib"] 51 | 52 | defp run_bench([]) do 53 | for file <- Path.wildcard("bench/*.exs") do 54 | Mix.Task.run("run", [file]) 55 | end 56 | end 57 | 58 | defp run_bench([file]) do 59 | Mix.Task.run("run", [file]) 60 | end 61 | 62 | # Run "mix help deps" to learn about dependencies. 63 | defp deps do 64 | [ 65 | {:benchee, "~> 1.0", only: [:bench]}, 66 | {:ecto, "~> 3.0", optional: true}, 67 | {:ex_doc, "> 0.0.0", only: [:docs], runtime: false}, 68 | {:elixir_uuid, "> 0.0.0", only: [:bench]}, 69 | {:stream_data, "~> 0.5", only: [:test]} 70 | ] 71 | end 72 | 73 | defp description do 74 | "Provides UUID generation, parsing, and formatting. Supports RFC 4122, and the v6 draft extension" 75 | end 76 | 77 | defp package do 78 | [ 79 | files: ["lib", "mix.exs", "README.md", "LICENSE.md"], 80 | maintainers: ["Paul Schoenfelder"], 81 | licenses: ["Apache-2.0"], 82 | links: %{ 83 | GitHub: "https://github.com/bitwalker/uniq" 84 | } 85 | ] 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "benchee": {:hex, :benchee, "1.1.0", "f3a43817209a92a1fade36ef36b86e1052627fd8934a8b937ac9ab3a76c43062", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}], "hexpm", "7da57d545003165a012b587077f6ba90b89210fd88074ce3c60ce239eb5e6d93"}, 3 | "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, 4 | "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, 5 | "earmark_parser": {:hex, :earmark_parser, "1.4.32", "fa739a0ecfa34493de19426681b23f6814573faee95dfd4b4aafe15a7b5b32c6", [:mix], [], "hexpm", "b8b0dd77d60373e77a3d7e8afa598f325e49e8663a51bcc2b88ef41838cca755"}, 6 | "ecto": {:hex, :ecto, "3.10.2", "6b887160281a61aa16843e47735b8a266caa437f80588c3ab80a8a960e6abe37", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6a895778f0d7648a4b34b486af59a1c8009041fbdf2b17f1ac215eb829c60235"}, 7 | "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"}, 8 | "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"}, 9 | "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, 10 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, 11 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, 12 | "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, 13 | "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, 14 | "stream_data": {:hex, :stream_data, "0.5.0", "b27641e58941685c75b353577dc602c9d2c12292dd84babf506c2033cd97893e", [:mix], [], "hexpm", "012bd2eec069ada4db3411f9115ccafa38540a3c78c4c0349f151fc761b9e271"}, 15 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, 16 | } 17 | -------------------------------------------------------------------------------- /test/ecto_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Uniq.Ecto.Test do 2 | use ExUnit.Case, async: true 3 | 4 | alias Ecto.TestRepo 5 | alias Uniq.UUID 6 | 7 | defmodule Person.Version4 do 8 | use Ecto.Schema 9 | 10 | @primary_key {:id, Uniq.UUID, autogenerate: true} 11 | schema "person_v4" do 12 | field(:name, :string) 13 | end 14 | end 15 | 16 | defmodule Person.Version5 do 17 | use Ecto.Schema 18 | 19 | @namespace UUID.uuid5(:dns, "person.v5.uniq.example.com", :raw) 20 | 21 | @primary_key {:id, Uniq.UUID, version: 5, namespace: @namespace, autogenerate: true} 22 | schema "person_v5" do 23 | field(:name, :string) 24 | end 25 | end 26 | 27 | defmodule Person.Version6 do 28 | use Ecto.Schema 29 | 30 | @primary_key false 31 | schema "person_v6" do 32 | field(:id, Uniq.UUID, version: 6, autogenerate: true) 33 | field(:name, :string) 34 | end 35 | end 36 | 37 | defmodule Person.Version7 do 38 | use Ecto.Schema 39 | 40 | @primary_key false 41 | schema "person_v7" do 42 | field(:id, Uniq.UUID, version: 7, autogenerate: true) 43 | field(:name, :string) 44 | end 45 | end 46 | 47 | test "can autogenerate primary keys" do 48 | assert %Person.Version4{id: uuid} = 49 | Ecto.Changeset.cast(%Person.Version4{}, %{name: "Paul"}, [:name]) 50 | |> TestRepo.insert!() 51 | 52 | assert {:ok, %UUID{version: 4}} = UUID.parse(uuid) 53 | 54 | assert %Person.Version5{id: uuid} = 55 | Ecto.Changeset.cast(%Person.Version5{}, %{name: "Paul"}, [:name]) 56 | |> TestRepo.insert!() 57 | 58 | assert {:ok, %UUID{version: 5}} = UUID.parse(uuid) 59 | 60 | assert %Person.Version6{id: uuid} = 61 | Ecto.Changeset.cast(%Person.Version6{}, %{name: "Paul"}, [:name]) 62 | |> TestRepo.insert!() 63 | 64 | assert {:ok, %UUID{version: 6}} = UUID.parse(uuid) 65 | 66 | assert %Person.Version7{id: uuid} = 67 | Ecto.Changeset.cast(%Person.Version7{}, %{name: "Paul"}, [:name]) 68 | |> TestRepo.insert!() 69 | 70 | assert {:ok, %UUID{version: 7}} = UUID.parse(uuid) 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /test/generator_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Uniq.Generator.Test do 2 | use ExUnit.Case 3 | 4 | test "generator state is always unique even under parallel generations" do 5 | :ets.new(:result, [:named_table, :public, :set, {:keypos, 1}]) 6 | 7 | parent = self() 8 | 9 | tasks = 10 | Enum.map(1..System.schedulers_online(), fn id -> 11 | {spawn(fn -> 12 | start = System.monotonic_time(:millisecond) 13 | repeat_on_generating(parent, id, start) 14 | end), id} 15 | end) 16 | 17 | assert duplicates(tasks) == [] 18 | end 19 | 20 | defp repeat_on_generating(parent, id, start) do 21 | result = Uniq.Generator.next() 22 | 23 | if !:ets.insert_new(:result, {result, id}) do 24 | [{prev, prev_id}] = :ets.lookup(:result, result) 25 | send(parent, {:duplicate, id, prev, prev_id}) 26 | else 27 | t = System.monotonic_time(:millisecond) 28 | 29 | if t - start > 15_000 do 30 | send(parent, {:done, id}) 31 | else 32 | repeat_on_generating(parent, id, start) 33 | end 34 | end 35 | end 36 | 37 | defp duplicates(tasks), do: duplicates(tasks, []) 38 | defp duplicates([], acc), do: acc 39 | 40 | defp duplicates([{_, id} | tasks], acc) do 41 | receive do 42 | {:duplicate, ^id, gen, conflicting_id} -> 43 | duplicates(tasks, [{gen, id, conflicting_id} | acc]) 44 | 45 | {:done, ^id} -> 46 | duplicates(tasks, acc) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/support/generators.ex: -------------------------------------------------------------------------------- 1 | defmodule Uniq.Test.Generators do 2 | import ExUnitProperties 3 | import StreamData 4 | 5 | alias Uniq.UUID 6 | 7 | @reserved_ncs <<0::1>> 8 | @rfc_variant <<2::2>> 9 | @reserved_ms <<6::3>> 10 | @reserved_future <<7::3>> 11 | @rfc_versions [1, 3, 4, 5] 12 | @versions [1, 3, 4, 5, 6, 7] 13 | @variants [@reserved_ncs, @rfc_variant, @reserved_ms, @reserved_future] 14 | @reserved_variants [@reserved_ncs, @reserved_ms, @reserved_future] 15 | @reserved_variants_uniform [<<0::3>>, <<6::3>>, <<7::3>>] 16 | 17 | @formats [:raw, :default, :hex, :urn, :slug] 18 | 19 | def valid_uuid(format \\ :raw) when format in @formats do 20 | gen all( 21 | {version, variant} <- 22 | bind(member_of(@variants), fn variant -> 23 | # Version 6 requires the use of the correct variant to be valid 24 | if variant in @reserved_variants do 25 | bind(member_of(@rfc_versions), fn version -> constant({version, variant}) end) 26 | else 27 | bind(member_of(@versions), fn version -> constant({version, variant}) end) 28 | end 29 | end), 30 | bits <- bitstring(length: 128) 31 | ) do 32 | variant_size = bit_size(variant) 33 | rest_size = 64 - variant_size 34 | <> = bits 35 | 36 | uuid = 37 | <> 39 | |> UUID.format(format) 40 | 41 | {version, variant, uuid} 42 | end 43 | end 44 | 45 | def invalid_uuid(format \\ :raw) when format in @formats do 46 | gen all( 47 | bits <- 48 | bind(bitstring(length: 128), fn <> = bits -> 50 | case v do 51 | v when v in [6, 7] -> 52 | # Version 6 specifically only allows a single variant to be considered valid 53 | case var do 54 | <<@rfc_variant, _::1>> -> 55 | bind(member_of(@reserved_variants_uniform), fn variant -> 56 | constant( 57 | <> 58 | ) 59 | end) 60 | 61 | _ -> 62 | constant(bits) 63 | end 64 | 65 | v when v in @rfc_versions -> 66 | # Any 3-bit pattern is technically valid as a variant in a UUID per the RFC, so we instead generate 67 | # a known-invalid version. 68 | bind(integer(8..15), fn version -> 69 | constant(<>) 70 | end) 71 | 72 | _ -> 73 | constant(bits) 74 | end 75 | end) 76 | ) do 77 | <<_::48, version::4, _::12, variant::bitstring-size(3), _::61>> = bits 78 | {version, variant, UUID.format(bits, format)} 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /test/support/test_repo.exs: -------------------------------------------------------------------------------- 1 | defmodule Ecto.TestAdapter do 2 | @behaviour Ecto.Adapter 3 | @behaviour Ecto.Adapter.Queryable 4 | @behaviour Ecto.Adapter.Schema 5 | @behaviour Ecto.Adapter.Transaction 6 | 7 | defmacro __before_compile__(_opts), do: :ok 8 | 9 | def ensure_all_started(_, _) do 10 | {:ok, []} 11 | end 12 | 13 | def init(opts) do 14 | :ecto = opts[:otp_app] 15 | "user" = opts[:username] 16 | "pass" = opts[:password] 17 | "hello" = opts[:database] 18 | "local" = opts[:hostname] 19 | 20 | {:ok, Supervisor.child_spec({Task, fn -> :timer.sleep(:infinity) end}, []), %{meta: :meta}} 21 | end 22 | 23 | def checkout(mod, _opts, fun) do 24 | send(self(), {:checkout, fun}) 25 | Process.put({mod, :checked_out?}, true) 26 | 27 | try do 28 | fun.() 29 | after 30 | Process.delete({mod, :checked_out?}) 31 | end 32 | end 33 | 34 | def checked_out?(mod) do 35 | Process.get({mod, :checked_out?}) || false 36 | end 37 | 38 | ## Types 39 | 40 | def loaders(:binary_id, type), do: [Ecto.UUID, type] 41 | def loaders(_primitive, type), do: [type] 42 | 43 | def dumpers(:binary_id, type), do: [type, Ecto.UUID] 44 | def dumpers(_primitive, type), do: [type] 45 | 46 | def autogenerate(:id), do: nil 47 | def autogenerate(:embed_id), do: Ecto.UUID.autogenerate() 48 | def autogenerate(:binary_id), do: Ecto.UUID.bingenerate() 49 | 50 | ## Queryable 51 | 52 | def prepare(operation, query), do: {:nocache, {operation, query}} 53 | 54 | def execute(_, _, {:nocache, {:all, query}}, _, _) do 55 | send(self(), {:all, query}) 56 | Process.get(:test_repo_all_results) || results_for_all_query(query) 57 | end 58 | 59 | def execute(_, _meta, {:nocache, {op, query}}, _params, _opts) do 60 | send(self(), {op, query}) 61 | {1, nil} 62 | end 63 | 64 | def stream(_, _meta, {:nocache, {:all, query}}, _params, _opts) do 65 | Stream.map([:execute], fn :execute -> 66 | send(self(), {:stream, query}) 67 | results_for_all_query(query) 68 | end) 69 | end 70 | 71 | defp results_for_all_query(%{select: %{fields: [_ | _] = fields}}) do 72 | values = List.duplicate(nil, length(fields) - 1) 73 | {1, [[1 | values]]} 74 | end 75 | 76 | defp results_for_all_query(%{select: %{fields: []}}) do 77 | {1, [[]]} 78 | end 79 | 80 | ## Schema 81 | 82 | def insert_all(_, meta, header, rows, on_conflict, returning, placeholders, _opts) do 83 | meta = 84 | Map.merge(meta, %{ 85 | header: header, 86 | on_conflict: on_conflict, 87 | returning: returning, 88 | placeholders: placeholders 89 | }) 90 | 91 | send(self(), {:insert_all, meta, rows}) 92 | {1, nil} 93 | end 94 | 95 | def insert(_, %{context: nil} = meta, fields, on_conflict, returning, _opts) do 96 | meta = Map.merge(meta, %{fields: fields, on_conflict: on_conflict, returning: returning}) 97 | send(self(), {:insert, meta}) 98 | {:ok, Enum.zip(returning, 1..length(returning))} 99 | end 100 | 101 | def insert(_, %{context: context}, _fields, _on_conflict, _returning, _opts) do 102 | context 103 | end 104 | 105 | # Notice the list of changes is never empty. 106 | def update(_, %{context: nil} = meta, [_ | _] = changes, filters, returning, _opts) do 107 | meta = Map.merge(meta, %{changes: changes, filters: filters, returning: returning}) 108 | send(self(), {:update, meta}) 109 | {:ok, Enum.zip(returning, 1..length(returning))} 110 | end 111 | 112 | def update(_, %{context: context}, [_ | _], _filters, _returning, _opts) do 113 | context 114 | end 115 | 116 | def delete(_, %{context: nil} = meta, filters, _opts) do 117 | meta = Map.merge(meta, %{filters: filters}) 118 | send(self(), {:delete, meta}) 119 | {:ok, []} 120 | end 121 | 122 | def delete(_, %{context: context}, _filters, _opts) do 123 | context 124 | end 125 | 126 | ## Transactions 127 | 128 | def transaction(mod, _opts, fun) do 129 | # Makes transactions "trackable" in tests 130 | Process.put({mod, :in_transaction?}, true) 131 | send(self(), {:transaction, fun}) 132 | 133 | try do 134 | {:ok, fun.()} 135 | catch 136 | :throw, {:ecto_rollback, value} -> 137 | {:error, value} 138 | after 139 | Process.delete({mod, :in_transaction?}) 140 | end 141 | end 142 | 143 | def in_transaction?(mod) do 144 | Process.get({mod, :in_transaction?}) || false 145 | end 146 | 147 | def rollback(_, value) do 148 | send(self(), {:rollback, value}) 149 | throw({:ecto_rollback, value}) 150 | end 151 | end 152 | 153 | Application.put_env(:ecto, Ecto.TestRepo, user: "invalid") 154 | 155 | defmodule Ecto.TestRepo do 156 | use Ecto.Repo, otp_app: :ecto, adapter: Ecto.TestAdapter 157 | 158 | def init(type, opts) do 159 | opts = [url: "ecto://user:pass@local/hello"] ++ opts 160 | opts[:parent] && send(opts[:parent], {__MODULE__, type, opts}) 161 | {:ok, opts} 162 | end 163 | end 164 | 165 | Ecto.TestRepo.start_link() 166 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | Application.ensure_all_started(:ecto) 2 | 3 | Code.require_file("support/test_repo.exs", __DIR__) 4 | ExUnit.start() 5 | -------------------------------------------------------------------------------- /test/uniq_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Uniq.Test do 2 | use ExUnit.Case, async: true 3 | import ExUnitProperties 4 | import Uniq.Test.Generators 5 | 6 | doctest Uniq.UUID 7 | 8 | alias Uniq.UUID 9 | 10 | setup do 11 | default = %{ 12 | nil => "00000000-0000-0000-0000-000000000000", 13 | 1 => "92fef5d6-c639-11eb-b8bc-0242ac130003", 14 | # generated in the :dns namespace, name "test" 15 | 3 => "45a113ac-c7f2-30b0-90a5-a399ab912716", 16 | 4 => "e5a4a3c3-45a7-4d5a-9809-e253a6ff8da2", 17 | # generated in the :dns namespace, name "test" 18 | 5 => "4be0643f-1d98-573b-97cd-ca98a65347dd", 19 | 6 => "1e7126af-f130-6780-adb4-8bbe7368fc2f", 20 | 7 => "0182b66c-29e7-7ae8-b60e-4b669fe07c77" 21 | } 22 | 23 | hex = 24 | default 25 | |> Enum.into(%{}, fn {k, v} -> {k, String.replace(v, "-", "")} end) 26 | 27 | raw = 28 | hex 29 | |> Enum.into(%{}, fn {k, v} -> {k, Base.decode16!(v, case: :lower)} end) 30 | 31 | urn = 32 | default 33 | |> Enum.into(%{}, fn {k, v} -> {k, "urn:uuid:" <> v} end) 34 | 35 | slug = 36 | raw 37 | |> Enum.into(%{}, fn {k, v} -> {k, Base.url_encode64(v, padding: false)} end) 38 | 39 | %{uuids: %{raw: raw, default: default, hex: hex, urn: urn, slug: slug}} 40 | end 41 | 42 | describe "parsing" do 43 | test "can handle nil uuid", %{uuids: uuids} do 44 | assert parse(nil, uuids) 45 | end 46 | 47 | test "can parse version 1", %{uuids: uuids} do 48 | assert parse(1, uuids) 49 | end 50 | 51 | test "can parse version 3", %{uuids: uuids} do 52 | assert parse(3, uuids) 53 | end 54 | 55 | test "can parse version 4", %{uuids: uuids} do 56 | assert parse(4, uuids) 57 | end 58 | 59 | test "can parse version 5", %{uuids: uuids} do 60 | assert parse(5, uuids) 61 | end 62 | 63 | test "can parse version 6", %{uuids: uuids} do 64 | assert parse(6, uuids) 65 | end 66 | 67 | test "can parse version 7", %{uuids: uuids} do 68 | assert parse(7, uuids) 69 | end 70 | 71 | property "can parse any 128-bit binary with valid version/variant values" do 72 | check all({version, variant, uuid} <- valid_uuid()) do 73 | assert {:ok, %UUID{version: ^version, variant: ^variant}} = UUID.parse(uuid) 74 | end 75 | end 76 | 77 | property "will reject any 128-bit binary with invalid version/variant values" do 78 | check all({_, _, uuid} <- invalid_uuid()) do 79 | assert {:error, _reason} = UUID.parse(uuid) 80 | end 81 | end 82 | 83 | property "can parse any 32-byte hex string which represents a valid uuid" do 84 | check all({version, variant, uuid} <- valid_uuid(:hex)) do 85 | assert {:ok, %UUID{version: ^version, variant: ^variant}} = UUID.parse(uuid) 86 | end 87 | end 88 | 89 | property "will reject any 32-byte hex string which represents an invalid uuid" do 90 | check all({_, _, uuid} <- invalid_uuid(:hex)) do 91 | assert {:error, _} = UUID.parse(uuid) 92 | end 93 | end 94 | 95 | property "can parse any 22-byte base64-encoded string which represents a valid uuid" do 96 | check all({version, variant, uuid} <- valid_uuid(:slug)) do 97 | assert {:ok, %UUID{version: ^version, variant: ^variant}} = UUID.parse(uuid) 98 | end 99 | end 100 | 101 | property "will reject any 22-byte base64-encoded string which represents an invalid uuid" do 102 | check all({_, _, uuid} <- invalid_uuid(:slug)) do 103 | assert {:error, _} = UUID.parse(uuid) 104 | end 105 | end 106 | end 107 | 108 | describe "formatting" do 109 | test "can format nil uuid", %{uuids: uuids} do 110 | assert format(nil, uuids) 111 | end 112 | 113 | test "can format version 1", %{uuids: uuids} do 114 | assert format(1, uuids) 115 | end 116 | 117 | test "can format version 3", %{uuids: uuids} do 118 | assert format(3, uuids) 119 | end 120 | 121 | test "can format version 4", %{uuids: uuids} do 122 | assert format(4, uuids) 123 | end 124 | 125 | test "can format version 5", %{uuids: uuids} do 126 | assert format(5, uuids) 127 | end 128 | 129 | test "can format version 6", %{uuids: uuids} do 130 | assert format(6, uuids) 131 | end 132 | 133 | test "can format version 7", %{uuids: uuids} do 134 | assert format(7, uuids) 135 | end 136 | end 137 | 138 | describe "generating" do 139 | test "can generate version 1" do 140 | default = UUID.uuid1() 141 | raw = UUID.uuid1(:raw) 142 | 143 | assert {:ok, %UUID{format: :default, version: 1}} = UUID.parse(default) 144 | assert {:ok, %UUID{format: :raw, version: 1}} = UUID.parse(raw) 145 | end 146 | 147 | test "can generate version 3", %{uuids: uuids} do 148 | namespace = <<0::128>> 149 | name = "test" 150 | 151 | default = UUID.uuid3(namespace, name) 152 | raw = UUID.uuid3(namespace, name, :raw) 153 | 154 | assert {:ok, %UUID{format: :default, version: 3}} = UUID.parse(default) 155 | assert {:ok, %UUID{format: :raw, version: 3}} = UUID.parse(raw) 156 | 157 | well_known_default = uuids[:default][3] 158 | well_known_raw = uuids[:raw][3] 159 | 160 | assert ^well_known_default = UUID.uuid3(:dns, name) 161 | assert ^well_known_raw = UUID.uuid3(:dns, name, :raw) 162 | end 163 | 164 | test "can generate version 4" do 165 | default = UUID.uuid4() 166 | raw = UUID.uuid4(:raw) 167 | 168 | assert {:ok, %UUID{format: :default, version: 4}} = UUID.parse(default) 169 | assert {:ok, %UUID{format: :raw, version: 4}} = UUID.parse(raw) 170 | end 171 | 172 | test "can generate version 5", %{uuids: uuids} do 173 | namespace = UUID.uuid1() 174 | name = "test" 175 | 176 | default = UUID.uuid5(namespace, name) 177 | raw = UUID.uuid5(namespace, name, :raw) 178 | 179 | assert {:ok, %UUID{format: :default, version: 5}} = UUID.parse(default) 180 | assert {:ok, %UUID{format: :raw, version: 5}} = UUID.parse(raw) 181 | 182 | well_known_default = uuids[:default][5] 183 | well_known_raw = uuids[:raw][5] 184 | 185 | assert ^well_known_default = UUID.uuid5(:dns, name) 186 | assert ^well_known_raw = UUID.uuid5(:dns, name, :raw) 187 | end 188 | 189 | test "can generate version 6" do 190 | default = UUID.uuid6() 191 | raw = UUID.uuid6(:raw) 192 | 193 | assert {:ok, %UUID{format: :default, version: 6}} = UUID.parse(default) 194 | assert {:ok, %UUID{format: :raw, version: 6}} = UUID.parse(raw) 195 | end 196 | 197 | test "can generate version 7" do 198 | default = UUID.uuid7() 199 | raw = UUID.uuid7(:raw) 200 | 201 | assert {:ok, %UUID{format: :default, version: 7}} = UUID.parse(default) 202 | assert {:ok, %UUID{format: :raw, version: 7}} = UUID.parse(raw) 203 | end 204 | end 205 | 206 | defp parse(version, uuids) do 207 | assert {:ok, %UUID{format: :raw, version: ^version}} = UUID.parse(uuids[:raw][version]) 208 | 209 | assert {:ok, %UUID{format: :default, version: ^version}} = 210 | UUID.parse(uuids[:default][version]) 211 | 212 | assert {:ok, %UUID{format: :hex, version: ^version}} = UUID.parse(uuids[:hex][version]) 213 | assert {:ok, %UUID{format: :urn, version: ^version}} = UUID.parse(uuids[:urn][version]) 214 | assert {:ok, %UUID{format: :slug, version: ^version}} = UUID.parse(uuids[:slug][version]) 215 | 216 | true 217 | end 218 | 219 | defp format(version, uuids) do 220 | raw = uuids[:raw][version] 221 | default = uuids[:default][version] 222 | hex = uuids[:hex][version] 223 | urn = uuids[:urn][version] 224 | slug = uuids[:slug][version] 225 | 226 | assert ^default = UUID.to_string(raw) 227 | assert ^raw = UUID.to_string(raw, :raw) 228 | assert ^hex = UUID.to_string(raw, :hex) |> String.downcase() 229 | assert ^urn = UUID.to_string(raw, :urn) 230 | assert ^slug = UUID.to_string(raw, :slug) 231 | 232 | assert ^raw = UUID.to_string(default, :raw) 233 | assert ^default = UUID.to_string(slug, :default) |> String.downcase() 234 | 235 | true 236 | end 237 | end 238 | --------------------------------------------------------------------------------