diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index dfdfab46d..255586532 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -14,8 +14,19 @@ jobs: with: path: | _build + .cargo deps - key: ${{ runner.os }}-build-deps-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-deps-2-${{ hashFiles('mix.lock') }} + + - name: Enable caching + run: | + # Disable volumes so caching can take effect + sed -i -Ee 's/- app_[a-z]+_data:.*$//g' docker-compose.yml + + # Make ourselves the owner + echo "RUN addgroup -g $(id -g) -S appgroup && adduser -u $(id -u) -S appuser -G appgroup" >> docker/app/Dockerfile + echo "USER appuser" >> docker/app/Dockerfile + echo "RUN mix local.hex --force && mix local.rebar --force" >> docker/app/Dockerfile - run: docker compose pull - run: docker compose build @@ -27,6 +38,18 @@ jobs: run: | docker compose run app mix sobelow --config docker compose run app mix deps.audit + + - name: Dialyzer + run: | + docker compose run app mix dialyzer + + typos: + name: 'Check for spelling errors' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: crate-ci/typos@master + lint-and-test: name: 'JavaScript Linting and Unit Tests' runs-on: ubuntu-latest diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 000000000..898ad2e40 --- /dev/null +++ b/.typos.toml @@ -0,0 +1,10 @@ +[default] +extend-ignore-re = [ + # Ignore development secret key. Production secret key should + # be in environment files and not checked into source control. + ".*secret_key_base.*", + + # Key constraints with encoded names + "fk_rails_[a-f0-9]+" +] + diff --git a/assets/js/utils/__tests__/local-autocompleter.spec.ts b/assets/js/utils/__tests__/local-autocompleter.spec.ts index bc612a088..182e13088 100644 --- a/assets/js/utils/__tests__/local-autocompleter.spec.ts +++ b/assets/js/utils/__tests__/local-autocompleter.spec.ts @@ -28,12 +28,12 @@ describe('Local Autocompleter', () => { }); describe('instantiation', () => { - it('should be constructable with compatible data', () => { + it('should be constructible with compatible data', () => { const result = new LocalAutocompleter(mockData); expect(result).toBeInstanceOf(LocalAutocompleter); }); - it('should NOT be constructable with incompatible data', () => { + it('should NOT be constructible with incompatible data', () => { const versionDataOffset = 12; const mockIncompatibleDataArray = new Array(versionDataOffset).fill(0); // Set data version to 1 @@ -45,6 +45,8 @@ describe('Local Autocompleter', () => { }); describe('topK', () => { + const termStem = ['f', 'o'].join(''); + let localAc: LocalAutocompleter; beforeAll(() => { @@ -66,7 +68,7 @@ describe('Local Autocompleter', () => { }); it('should return suggestions sorted by image count', () => { - const result = localAc.topK('fo', defaultK); + const result = localAc.topK(termStem, defaultK); expect(result).toEqual([ expect.objectContaining({ name: 'forest', imageCount: 3 }), expect.objectContaining({ name: 'fog', imageCount: 1 }), @@ -82,13 +84,13 @@ describe('Local Autocompleter', () => { }); it('should return only the required number of suggestions', () => { - const result = localAc.topK('fo', 1); + const result = localAc.topK(termStem, 1); expect(result).toEqual([expect.objectContaining({ name: 'forest', imageCount: 3 })]); }); it('should NOT return suggestions associated with hidden tags', () => { window.booru.hiddenTagList = [1]; - const result = localAc.topK('fo', defaultK); + const result = localAc.topK(termStem, defaultK); expect(result).toEqual([]); }); diff --git a/assets/package-lock.json b/assets/package-lock.json index ea901f29f..bb2b24cdb 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -4997,9 +4997,9 @@ "dev": true }, "node_modules/ws": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "engines": { "node": ">=10.0.0" }, diff --git a/lib/philomena/comments/query.ex b/lib/philomena/comments/query.ex index 6b2bea421..9e9c89865 100644 --- a/lib/philomena/comments/query.ex +++ b/lib/philomena/comments/query.ex @@ -88,7 +88,7 @@ defmodule Philomena.Comments.Query do defp parse(fields, context, query_string) do fields - |> Parser.parser() + |> Parser.new() |> Parser.parse(query_string, context) end diff --git a/lib/philomena/filters/query.ex b/lib/philomena/filters/query.ex index adf53b09e..3b6bb3efc 100644 --- a/lib/philomena/filters/query.ex +++ b/lib/philomena/filters/query.ex @@ -29,7 +29,7 @@ defmodule Philomena.Filters.Query do defp parse(fields, context, query_string) do fields - |> Parser.parser() + |> Parser.new() |> Parser.parse(query_string, context) end diff --git a/lib/philomena/galleries/query.ex b/lib/philomena/galleries/query.ex index 9baad4699..e04ceeccf 100644 --- a/lib/philomena/galleries/query.ex +++ b/lib/philomena/galleries/query.ex @@ -18,7 +18,7 @@ defmodule Philomena.Galleries.Query do query_string = query_string || "" fields() - |> Parser.parser() + |> Parser.new() |> Parser.parse(query_string) end end diff --git a/lib/philomena/images/query.ex b/lib/philomena/images/query.ex index 575c6e71a..9eedcd740 100644 --- a/lib/philomena/images/query.ex +++ b/lib/philomena/images/query.ex @@ -140,7 +140,7 @@ defmodule Philomena.Images.Query do defp parse(fields, context, query_string) do fields - |> Parser.parser() + |> Parser.new() |> Parser.parse(query_string, context) end diff --git a/lib/philomena/posts/query.ex b/lib/philomena/posts/query.ex index 27773776b..331655c71 100644 --- a/lib/philomena/posts/query.ex +++ b/lib/philomena/posts/query.ex @@ -86,7 +86,7 @@ defmodule Philomena.Posts.Query do defp parse(fields, context, query_string) do fields - |> Parser.parser() + |> Parser.new() |> Parser.parse(query_string, context) end diff --git a/lib/philomena/reports/query.ex b/lib/philomena/reports/query.ex index d5adc2ccc..c9d9be444 100644 --- a/lib/philomena/reports/query.ex +++ b/lib/philomena/reports/query.ex @@ -16,7 +16,7 @@ defmodule Philomena.Reports.Query do def compile(query_string) do fields() - |> Parser.parser() + |> Parser.new() |> Parser.parse(query_string || "", %{}) end end diff --git a/lib/philomena/tags.ex b/lib/philomena/tags.ex index 1f6e80c65..de7a01717 100644 --- a/lib/philomena/tags.ex +++ b/lib/philomena/tags.ex @@ -251,7 +251,7 @@ defmodule Philomena.Tags do |> where(tag_id: ^tag.id) |> Repo.delete_all() - # Update other assocations + # Update other associations ArtistLink |> where(tag_id: ^tag.id) |> Repo.update_all(set: [tag_id: target_tag.id]) diff --git a/lib/philomena/tags/query.ex b/lib/philomena/tags/query.ex index 5bfd2126d..da148da45 100644 --- a/lib/philomena/tags/query.ex +++ b/lib/philomena/tags/query.ex @@ -19,7 +19,7 @@ defmodule Philomena.Tags.Query do def compile(query_string) do fields() - |> Parser.parser() + |> Parser.new() |> Parser.parse(query_string || "") end end diff --git a/lib/philomena/users/user_notifier.ex b/lib/philomena/users/user_notifier.ex index 480833211..2cd85d864 100644 --- a/lib/philomena/users/user_notifier.ex +++ b/lib/philomena/users/user_notifier.ex @@ -88,7 +88,7 @@ defmodule Philomena.Users.UserNotifier do Your account has been automatically locked due to too many attempts to sign in. - You can unlock your account by visting the URL below: + You can unlock your account by visiting the URL below: #{url} diff --git a/lib/philomena_media/analyzers/analyzer.ex b/lib/philomena_media/analyzers/analyzer.ex index cf3b28ec7..c96f00056 100644 --- a/lib/philomena_media/analyzers/analyzer.ex +++ b/lib/philomena_media/analyzers/analyzer.ex @@ -1,5 +1,8 @@ defmodule PhilomenaMedia.Analyzers.Analyzer do @moduledoc false + @doc """ + Generate a `m:PhilomenaMedia.Analyzers.Result` for file at the given path. + """ @callback analyze(Path.t()) :: PhilomenaMedia.Analyzers.Result.t() end diff --git a/lib/philomena_media/intensities.ex b/lib/philomena_media/intensities.ex index 5abd71e08..ea0952952 100644 --- a/lib/philomena_media/intensities.ex +++ b/lib/philomena_media/intensities.ex @@ -36,7 +36,7 @@ defmodule PhilomenaMedia.Intensities do > #### Info {: .info} > - > Clients should prefer to use `m:PhilomenaMedia.Processors.intensities/2`, as it handles + > Clients should prefer to use `PhilomenaMedia.Processors.intensities/2`, as it handles > media files of any type supported by this library, not just PNG or JPEG. ## Examples diff --git a/lib/philomena_media/processors.ex b/lib/philomena_media/processors.ex index 23c49dcfe..22ce613b8 100644 --- a/lib/philomena_media/processors.ex +++ b/lib/philomena_media/processors.ex @@ -62,29 +62,31 @@ defmodule PhilomenaMedia.Processors do alias PhilomenaMedia.Processors.{Gif, Jpeg, Png, Svg, Webm} alias PhilomenaMedia.Mime - # The name of a version, like :large + @typedoc "The name of a version, like `:large`." @type version_name :: atom() @type dimensions :: {integer(), integer()} @type version_list :: [{version_name(), dimensions()}] - # The file name of a processed version, like "large.png" + @typedoc "The file name of a processed version, like `large.png`." @type version_filename :: String.t() - # A single file to be copied to satisfy a request for a version name + @typedoc "A single file to be copied to satisfy a request for a version name." @type copy_request :: {:copy, Path.t(), version_filename()} - # A list of thumbnail versions to copy into place + @typedoc "A list of thumbnail versions to copy into place." @type thumbnails :: {:thumbnails, [copy_request()]} - # Replace the original file to strip metadata or losslessly optimize + @typedoc "Replace the original file to strip metadata or losslessly optimize." @type replace_original :: {:replace_original, Path.t()} - # Apply the computed corner intensities + @typedoc "Apply the computed corner intensities." @type intensities :: {:intensities, Intensities.t()} - # An edit script, representing the changes to apply to the storage backend - # after successful processing + @typedoc """ + An edit script, representing the changes to apply to the storage backend + after successful processing. + """ @type edit_script :: [thumbnails() | replace_original() | intensities()] @doc """ diff --git a/lib/philomena_media/processors/processor.ex b/lib/philomena_media/processors/processor.ex index 2c3acc0ba..8b9f568f3 100644 --- a/lib/philomena_media/processors/processor.ex +++ b/lib/philomena_media/processors/processor.ex @@ -5,17 +5,25 @@ defmodule PhilomenaMedia.Processors.Processor do alias PhilomenaMedia.Processors alias PhilomenaMedia.Intensities - # Generate a list of version filenames for the given version list. + @doc """ + Generate a list of version filenames for the given version list. + """ @callback versions(Processors.version_list()) :: [Processors.version_filename()] - # Process the media at the given path against the given version list, and return an - # edit script with the resulting files + @doc """ + Process the media at the given path against the given version list, and return an + edit script with the resulting files. + """ @callback process(Result.t(), Path.t(), Processors.version_list()) :: Processors.edit_script() - # Perform post-processing optimization tasks on the file, to reduce its size - # and strip non-essential metadata + @doc """ + Perform post-processing optimization tasks on the file, to reduce its size + and strip non-essential metadata. + """ @callback post_process(Result.t(), Path.t()) :: Processors.edit_script() - # Generate corner intensities for the given path + @doc """ + Generate corner intensities for the given path. + """ @callback intensities(Result.t(), Path.t()) :: Intensities.t() end diff --git a/lib/philomena_proxy/scrapers.ex b/lib/philomena_proxy/scrapers.ex index 67711045b..a96f08176 100644 --- a/lib/philomena_proxy/scrapers.ex +++ b/lib/philomena_proxy/scrapers.ex @@ -3,16 +3,16 @@ defmodule PhilomenaProxy.Scrapers do Scrape utilities to facilitate uploading media from other websites. """ - # The URL to fetch, as a string. + @typedoc "The URL to fetch, as a string." @type url :: String.t() - # An individual image in a list associated with a scrape result. + @typedoc "An individual image in a list associated with a scrape result." @type image_result :: %{ url: url(), camo_url: url() } - # Result of a successful scrape. + @typedoc "Result of a successful scrape." @type scrape_result :: %{ source_url: url(), description: String.t() | nil, diff --git a/lib/philomena_query/batch.ex b/lib/philomena_query/batch.ex index 4e27c93a7..f02d65906 100644 --- a/lib/philomena_query/batch.ex +++ b/lib/philomena_query/batch.ex @@ -13,13 +13,31 @@ defmodule PhilomenaQuery.Batch do alias Philomena.Repo import Ecto.Query + @typedoc """ + Represents an object which may be operated on via `m:Ecto.Query`. + + This could be a schema object (e.g. `m:Philomena.Images.Image`) or a fully formed query + `from i in Image, where: i.hidden_from_users == false`. + """ @type queryable :: any() @type batch_size :: {:batch_size, integer()} @type id_field :: {:id_field, atom()} @type batch_options :: [batch_size() | id_field()] + @typedoc """ + The callback for `record_batches/3`. + + Takes a list of schema structs which were returned in the batch. Return value is ignored. + """ @type record_batch_callback :: ([struct()] -> any()) + + @typedoc """ + The callback for `query_batches/3`. + + Takes an `m:Ecto.Query` that can be processed with `m:Philomena.Repo` query commands, such + as `Philomena.Repo.update_all/3` or `Philomena.Repo.delete_all/2`. Return value is ignored. + """ @type query_batch_callback :: ([Ecto.Query.t()] -> any()) @doc """ diff --git a/lib/philomena_query/parse/parser.ex b/lib/philomena_query/parse/parser.ex index ba4b05976..a9d402223 100644 --- a/lib/philomena_query/parse/parser.ex +++ b/lib/philomena_query/parse/parser.ex @@ -41,12 +41,34 @@ defmodule PhilomenaQuery.Parse.Parser do TermRangeParser } + @typedoc """ + User-supplied context argument. + + Provided to `parse/3` and passed to the transform callback. + """ @type context :: any() + + @typedoc "Query in the search engine JSON query language." @type query :: map() + @typedoc "Whether the default field is `:term` (not analyzed) or `:ngram` (analyzed)." @type default_field_type :: :term | :ngram + @typedoc """ + Return value of the transform callback. + + On `{:ok, query}`, the query is incorporated into the parse tree at the current location. + On `{:error, error}`, parsing immediately stops and the error is returned from the parser. + """ @type transform_result :: {:ok, query()} | {:error, String.t()} + + @typedoc """ + Type of the transform callback. + + The transform callback receives the context argument passed to `parse/3` and the remainder of + the term. For instance `my:example` would match a transform rule with the key `"my"`, and + the remainder passed to the callback would be `"example"`. + """ @type transform :: (context, String.t() -> transform_result()) @type t :: %__MODULE__{ @@ -112,11 +134,11 @@ defmodule PhilomenaQuery.Parse.Parser do aliases: %{"hidden" => "hidden_from_users"} ] - Parser.parser(options) + Parser.new(options) """ - @spec parser(keyword()) :: t() - def parser(options) do + @spec new(keyword()) :: t() + def new(options) do parser = struct(Parser, options) fields = diff --git a/lib/philomena_query/search.ex b/lib/philomena_query/search.ex index b4960657b..140adf674 100644 --- a/lib/philomena_query/search.ex +++ b/lib/philomena_query/search.ex @@ -18,10 +18,36 @@ defmodule PhilomenaQuery.Search do # todo: fetch through compile_env? @policy Philomena.SearchPolicy + @typedoc """ + Any schema module which has an associated search index. See the policy module + for more information. + """ @type schema_module :: @policy.schema_module() + + @typedoc """ + Represents an object which may be operated on via `m:Ecto.Query`. + + This could be a schema object (e.g. `m:Philomena.Images.Image`) or a fully formed query + `from i in Image, where: i.hidden_from_users == false`. + """ @type queryable :: any() + + @typedoc """ + A query body, as deliverable to any index's `_search` endpoint. + + See the query DSL documentation for additional information: + https://opensearch.org/docs/latest/query-dsl/ + """ @type query_body :: map() + @typedoc """ + Given a term at the given path, replace the old term with the new term. + + `path` is a list of names to be followed to find the old term. For example, + a document containing `{"condiments": "dijon"}` would permit `["condiments"]` + as the path, and a document containing `{"namespaced_tags": {"name": ["old"]}}` + would permit `["namespaced_tags", "name"]` as the path. + """ @type replacement :: %{ path: [String.t()], old: term(), diff --git a/lib/philomena_query/search_index.ex b/lib/philomena_query/search_index.ex index 3a4fe9dad..119d26135 100644 --- a/lib/philomena_query/search_index.ex +++ b/lib/philomena_query/search_index.ex @@ -1,11 +1,34 @@ defmodule PhilomenaQuery.SearchIndex do - # Returns the index name for the index. - # This is usually a collection name like "images". + @moduledoc """ + Behaviour module for schemas with search indexing. + """ + + @doc """ + Returns the index name for the index. + + This is usually a collection name like "images". + + See https://opensearch.org/docs/latest/api-reference/index-apis/create-index/ for + reference on index naming restrictions. + """ @callback index_name() :: String.t() - # Returns the mapping and settings for the index. + @doc """ + Returns the mapping and settings for the index. + + See https://opensearch.org/docs/latest/api-reference/index-apis/put-mapping/ for + reference on the mapping syntax, and the following pages for which types may be + used in mappings: + - https://opensearch.org/docs/latest/field-types/ + - https://opensearch.org/docs/latest/analyzers/index-analyzers/ + """ @callback mapping() :: map() - # Returns the JSON representation of the given struct for indexing in OpenSearch. + @doc """ + Returns the JSON representation of the given struct for indexing in OpenSearch. + + See https://opensearch.org/docs/latest/api-reference/document-apis/index-document/ for + reference on how this value is used. + """ @callback as_json(struct()) :: map() end diff --git a/lib/philomena_web/controllers/admin/site_notice_controller.ex b/lib/philomena_web/controllers/admin/site_notice_controller.ex index 7612422eb..ff284a75f 100644 --- a/lib/philomena_web/controllers/admin/site_notice_controller.ex +++ b/lib/philomena_web/controllers/admin/site_notice_controller.ex @@ -44,7 +44,7 @@ defmodule PhilomenaWeb.Admin.SiteNoticeController do case SiteNotices.update_site_notice(conn.assigns.site_notice, site_notice_params) do {:ok, _site_notice} -> conn - |> put_flash(:info, "Succesfully updated site notice.") + |> put_flash(:info, "Successfully updated site notice.") |> redirect(to: ~p"/admin/site_notices") {:error, changeset} -> @@ -56,7 +56,7 @@ defmodule PhilomenaWeb.Admin.SiteNoticeController do {:ok, _site_notice} = SiteNotices.delete_site_notice(conn.assigns.site_notice) conn - |> put_flash(:info, "Sucessfully deleted site notice.") + |> put_flash(:info, "Successfully deleted site notice.") |> redirect(to: ~p"/admin/site_notices") end diff --git a/lib/philomena_web/templates/conversation/show.html.slime b/lib/philomena_web/templates/conversation/show.html.slime index 5af5ead9d..85bf02998 100644 --- a/lib/philomena_web/templates/conversation/show.html.slime +++ b/lib/philomena_web/templates/conversation/show.html.slime @@ -59,5 +59,5 @@ h1 = @conversation.title p You've managed to send over 1,000 messages in this conversation! p We'd like to ask you to make a new conversation. Don't worry, this one won't go anywhere if you need to refer back to it. p - => link "Click here", to: ~p"/conversations/new?#{[receipient: other.name]}" + => link "Click here", to: ~p"/conversations/new?#{[recipient: other.name]}" ' to make a new conversation with this user. diff --git a/lib/philomena_web/templates/filter/_form.html.slime b/lib/philomena_web/templates/filter/_form.html.slime index 779659068..e46cda9ad 100644 --- a/lib/philomena_web/templates/filter/_form.html.slime +++ b/lib/philomena_web/templates/filter/_form.html.slime @@ -73,7 +73,7 @@ .fieldlabel strong You probably do not want to check this unless you know what you are doing - it cannot be changed later - | . Pulic filters can be shared with other users and used by them; if you make changes to a filter, it will update all users of that filter. + | . Public filters can be shared with other users and used by them; if you make changes to a filter, it will update all users of that filter. - input_value(f, :public) == true -> .fieldlabel diff --git a/lib/philomena_web/templates/topic/poll/_form.html.slime b/lib/philomena_web/templates/topic/poll/_form.html.slime index c1ba66cff..f5d960b02 100644 --- a/lib/philomena_web/templates/topic/poll/_form.html.slime +++ b/lib/philomena_web/templates/topic/poll/_form.html.slime @@ -29,10 +29,10 @@ p.fieldlabel = select @f, :vote_method, ["-": "", "Single option": :single, "Multiple options": :multiple], class: "input" = error_tag @f, :vote_method -= inputs_for @f, :options, fn fo -> += inputs_for @f, :options, fn opt -> .field.js-poll-option.field--inline.flex--no-wrap.flex--centered - = text_input fo, :label, class: "input flex__grow js-option-label", placeholder: "Option" - = error_tag fo, :label + = text_input opt, :label, class: "input flex__grow js-option-label", placeholder: "Option" + = error_tag opt, :label label.input--separate-left.flex__fixed.flex--centered a.js-option-remove href="#" diff --git a/mix.exs b/mix.exs index 41e15a6fc..f9cded723 100644 --- a/mix.exs +++ b/mix.exs @@ -11,7 +11,8 @@ defmodule Philomena.MixProject do start_permanent: Mix.env() == :prod, aliases: aliases(), deps: deps(), - dialyzer: [plt_add_apps: [:mix]] + dialyzer: [plt_add_apps: [:mix]], + docs: [formatters: ["html"]] ] end @@ -85,6 +86,7 @@ defmodule Philomena.MixProject do {:credo, "~> 1.6", only: [:dev, :test], runtime: false}, {:credo_envvar, "~> 0.1", only: [:dev, :test], runtime: false}, {:credo_naming, "~> 2.0", only: [:dev, :test], runtime: false}, + {:ex_doc, "~> 0.30", only: [:dev], runtime: false}, # Security checks {:sobelow, "~> 0.11", only: [:dev, :test], runtime: true}, diff --git a/mix.lock b/mix.lock index f8e68868b..46729250a 100644 --- a/mix.lock +++ b/mix.lock @@ -17,6 +17,7 @@ "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 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", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, "ecto_network": {:hex, :ecto_network, "1.5.0", "a930c910975e7a91237b858ebf0f4ad7b2aae32fa846275aa203cb858459ec73", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 0.0.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.14.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "4d614434ae3e6d373a2f693d56aafaa3f3349714668ffd6d24e760caf578aa2f"}, "ecto_sql": {:hex, :ecto_sql, "3.11.2", "c7cc7f812af571e50b80294dc2e535821b3b795ce8008d07aa5f336591a185a8", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "73c07f995ac17dbf89d3cfaaf688fcefabcd18b7b004ac63b0dc4ef39499ed6b"}, @@ -26,6 +27,7 @@ "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_aws": {:git, "https://github.com/liamwhite/ex_aws.git", "a340859dd8ac4d63bd7a3948f0994e493e49bda4", [ref: "a340859dd8ac4d63bd7a3948f0994e493e49bda4"]}, "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.3", "422468e5c3e1a4da5298e66c3468b465cfd354b842e512cb1f6fbbe4e2f5bdaf", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "4f09dd372cc386550e484808c5ac5027766c8d0cd8271ccc578b82ee6ef4f3b8"}, + "ex_doc": {:hex, :ex_doc, "0.34.0", "ab95e0775db3df71d30cf8d78728dd9261c355c81382bcd4cefdc74610bef13e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "60734fb4c1353f270c3286df4a0d51e65a2c1d9fba66af3940847cc65a8066d7"}, "expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"}, "exq": {:hex, :exq, "0.19.0", "06eb92944dad39f0954dc8f63190d3e24d11734eef88cf5800883e57ebf74f3c", [:mix], [{:elixir_uuid, ">= 1.2.0", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0 and < 6.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:redix, ">= 0.9.0", [hex: :redix, repo: "hexpm", optional: false]}], "hexpm", "24fc0ebdd87cc7406e1034fb46c2419f9c8a362f0ec634d23b6b819514d36390"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, @@ -37,6 +39,9 @@ "inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "mail": {:hex, :mail, "0.3.1", "cb0a14e4ed8904e4e5a08214e686ccf6f9099346885db17d8c309381f865cc5c", [:mix], [], "hexpm", "1db701e89865c1d5fa296b2b57b1cd587587cca8d8a1a22892b35ef5a8e352a6"}, + "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [: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", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"},