From e9bbba58cdc8fa5c0812ea0a861144e4a7a508c8 Mon Sep 17 00:00:00 2001 From: sangeetha Date: Mon, 25 Sep 2023 11:22:36 +0530 Subject: [PATCH] initial commit --- city-office/.exercism/config.json | 22 +++ city-office/.exercism/metadata.json | 1 + city-office/.formatter.exs | 4 + city-office/.gitignore | 24 +++ city-office/HELP.md | 74 +++++++ city-office/HINTS.md | 63 ++++++ city-office/README.md | 173 ++++++++++++++++ city-office/lib/form.ex | 75 +++++++ city-office/mix.exs | 28 +++ city-office/test/form_test.exs | 292 ++++++++++++++++++++++++++++ city-office/test/test_helper.exs | 2 + 11 files changed, 758 insertions(+) create mode 100644 city-office/.exercism/config.json create mode 100644 city-office/.exercism/metadata.json create mode 100644 city-office/.formatter.exs create mode 100644 city-office/.gitignore create mode 100644 city-office/HELP.md create mode 100644 city-office/HINTS.md create mode 100644 city-office/README.md create mode 100644 city-office/lib/form.ex create mode 100644 city-office/mix.exs create mode 100644 city-office/test/form_test.exs create mode 100644 city-office/test/test_helper.exs diff --git a/city-office/.exercism/config.json b/city-office/.exercism/config.json new file mode 100644 index 0000000..eb609d4 --- /dev/null +++ b/city-office/.exercism/config.json @@ -0,0 +1,22 @@ +{ + "authors": [ + "angelikatyborska" + ], + "contributors": [ + "neenjaw", + "michallepicki" + ], + "files": { + "solution": [ + "lib/form.ex" + ], + "test": [ + "test/form_test.exs" + ], + "exemplar": [ + ".meta/exemplar.ex" + ] + }, + "language_versions": ">=1.10", + "blurb": "Learn about writing documentation and typespecs by getting your code ready for the arrival of a new colleague at the city office." +} diff --git a/city-office/.exercism/metadata.json b/city-office/.exercism/metadata.json new file mode 100644 index 0000000..8519750 --- /dev/null +++ b/city-office/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"elixir","exercise":"city-office","id":"6150971843e64550925d8c7595d13d51","url":"https://exercism.org/tracks/elixir/exercises/city-office","handle":"sangeethailango","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/city-office/.formatter.exs b/city-office/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/city-office/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/city-office/.gitignore b/city-office/.gitignore new file mode 100644 index 0000000..4abee35 --- /dev/null +++ b/city-office/.gitignore @@ -0,0 +1,24 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +match_binary-*.tar + diff --git a/city-office/HELP.md b/city-office/HELP.md new file mode 100644 index 0000000..4d4d1d1 --- /dev/null +++ b/city-office/HELP.md @@ -0,0 +1,74 @@ +# Help + +## Running the tests + +From the terminal, change to the base directory of the exercise then execute the tests with: + +```bash +$ mix test +``` + +This will execute the test file found in the `test` subfolder -- a file ending in `_test.exs` + +Documentation: + +* [`mix test` - Elixir's test execution tool](https://hexdocs.pm/mix/Mix.Tasks.Test.html) +* [`ExUnit` - Elixir's unit test library](https://hexdocs.pm/ex_unit/ExUnit.html) + +## Pending tests + +In test suites of practice exercises, all but the first test have been tagged to be skipped. + +Once you get a test passing, you can unskip the next one by commenting out the relevant `@tag :pending` with a `#` symbol. + +For example: + +```elixir +# @tag :pending +test "shouting" do + assert Bob.hey("WATCH OUT!") == "Whoa, chill out!" +end +``` + +If you wish to run all tests at once, you can include all skipped test by using the `--include` flag on the `mix test` command: + +```bash +$ mix test --include pending +``` + +Or, you can enable all the tests by commenting out the `ExUnit.configure` line in the file `test/test_helper.exs`. + +```elixir +# ExUnit.configure(exclude: :pending, trace: true) +``` + +## Useful `mix test` options + +* `test/.exs:LINENUM` - runs only a single test, the test from `.exs` whose definition is on line `LINENUM` +* `--failed` - runs only tests that failed the last time they ran +* `--max-failures` - the suite stops evaluating tests when this number of test failures +is reached +* `--seed 0` - disables randomization so the tests in a single file will always be ran +in the same order they were defined in + +## Submitting your solution + +You can submit your solution using the `exercism submit lib/form.ex` command. +This command will upload your solution to the Exercism website and print the solution page's URL. + +It's possible to submit an incomplete solution which allows you to: + +- See how others have completed the exercise +- Request help from a mentor + +## Need to get help? + +If you'd like help solving the exercise, check the following pages: + +- The [Elixir track's documentation](https://exercism.org/docs/tracks/elixir) +- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5) +- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs) + +Should those resources not suffice, you could submit your (incomplete) solution to request mentoring. + +If you're stuck on something, it may help to look at some of the [available resources](https://exercism.org/docs/tracks/elixir/resources) out there where answers might be found. \ No newline at end of file diff --git a/city-office/HINTS.md b/city-office/HINTS.md new file mode 100644 index 0000000..01a107f --- /dev/null +++ b/city-office/HINTS.md @@ -0,0 +1,63 @@ +# Hints + +## General + +- Read the official documentation for [typespecs][typespecs]. +- Read the official documentation about [writing documentation][writing-documentation]. +- Read about using module attributes as annotations in the [official Getting Started guide][getting-started-module-attributes]. +- Read about using typespecs in the [official Getting Started guide][getting-started-typespecs]. + +## 1. Document the purpose of the form tools + +- The module attribute `@moduledoc` can be used to write documentation for a module. + +## 2. Document filling out fields with blank values + +- The module attribute `@doc` can be used to write documentation for a function. +- The module attribute `@spec` can be used to write a typespec for a function. +- Place the `@doc` and `@spec` attributes right before the first function clause of the function that those attributes describe. +- Refer to the [typespecs documentation][typespecs-types] for a list of all available types. +- The correct type for strings is [defined in the `String` module][string-t]. + +## 3. Document splitting values into lists of uppercase letters + +- The module attribute `@doc` can be used to write documentation for a function. +- The module attribute `@spec` can be used to write a typespec for a function. +- Place the `@doc` and `@spec` attributes right before the first function clause of the function that those attributes describe. +- Refer to the [typespecs documentation][typespecs-types] for a list of all available types. +- The correct type for strings is [defined in the `String` module][string-t]. +- A list is a parametrized type. + +## 4. Document checking if a value fits a field with a max length + +- The module attribute `@doc` can be used to write documentation for a function. +- The module attribute `@spec` can be used to write a typespec for a function. +- Place the `@doc` and `@spec` attributes right before the first function clause of the function that those attributes describe. +- Refer to the [typespecs documentation][typespecs-types] for a list of all available types. +- The correct type for strings is [defined in the `String` module][string-t]. +- Literal values can be used in a typespec. +- The pipe `|` can be used to represent a union of types. + +## 5. Document different address formats + +- The module attribute `@type` can be use to define a custom public type. +- Types can be compound, e.g. when specifying a type that's a map, you can also specify the types of the values under the specific keys. +- [The type operator `::`][type-operator] can also be used to prepend a variable name to a type. +- Custom types can be used to define other custom types. + +## 6. Document formatting the address + +- The module attribute `@doc` can be used to write documentation for a function. +- The module attribute `@spec` can be used to write a typespec for a function. +- Place the `@doc` and `@spec` attributes right before the first function clause of the function that those attributes describe. +- Refer to the [typespecs documentation][typespecs-types] for a list of all available types. +- The correct type for strings is [defined in the `String` module][string-t]. +- Custom types can be used in a typespec. + +[writing-documentation]: https://hexdocs.pm/elixir/writing-documentation.html +[typespecs]: https://hexdocs.pm/elixir/typespecs.html +[typespecs-types]: https://hexdocs.pm/elixir/typespecs.html#types-and-their-syntax +[getting-started-module-attributes]: https://elixir-lang.org/getting-started/module-attributes.html#as-annotations +[getting-started-typespecs]: https://elixir-lang.org/getting-started/typespecs-and-behaviours.html#types-and-specs +[string-t]: https://hexdocs.pm/elixir/String.html#t:t/0 +[type-operator]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#::/2 \ No newline at end of file diff --git a/city-office/README.md b/city-office/README.md new file mode 100644 index 0000000..81c46f5 --- /dev/null +++ b/city-office/README.md @@ -0,0 +1,173 @@ +# City Office + +Welcome to City Office on Exercism's Elixir Track. +If you need help running the tests or submitting your code, check out `HELP.md`. +If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :) + +## Introduction + +## Docs + +Documentation in Elixir is a first-class citizen. + +There are two module attributes commonly used to document your code - `@moduledoc` for documenting a module and `@doc` for documenting a function that follows the attribute. The `@moduledoc` attribute usually appears on the first line of the module, and the `@doc` attribute usually appears right before a function definition, or the function's typespec if it has one. The documentation is commonly written in a multiline string using the heredoc syntax. + +Elixir documentation is written in [**Markdown**][markdown]. + +```elixir +defmodule String do + @moduledoc """ + Strings in Elixir are UTF-8 encoded binaries. + """ + + @doc """ + Converts all characters in the given string to uppercase according to `mode`. + + ## Examples + + iex> String.upcase("abcd") + "ABCD" + + iex> String.upcase("olá") + "OLÁ" + """ + def upcase(string, mode \\ :default) +end +``` + +## Typespecs + +Elixir is a dynamically typed language, which means it doesn't provide compile-time type checks. Still, type specifications can be used as a form of documentation. + +A type specification can be added to a function using the `@spec` module attribute right before the function definition. `@spec` is followed by the function name and a list of all of its arguments' types, in parentheses, separated by commas. The type of the return value is separated from the function's arguments with a double colon `::`. + +```elixir +@spec longer_than?(String.t(), non_neg_integer()) :: boolean() +def longer_than?(string, length), do: String.length(string) > length +``` + +### Types + +Most commonly used types include: + +- booleans: `boolean()` +- strings: `String.t()` +- numbers: `integer()`, `non_neg_integer()`, `pos_integer()`, `float()` +- lists: `list()` +- a value of any type: `any()` + +Some types can also be parameterized, for example `list(integer)` is a list of integers. + +Literal values can also be used as types. + +A union of types can be written using the pipe `|`. For example, `integer() | :error` means either an integer or the atom literal `:error`. + +A full list of all types can be found in the ["Typespecs" section in the official documentation][types]. + +### Naming arguments + +Arguments in the typespec could also be named which is useful for distinguishing multiple arguments of the same type. The argument name, followed by a double colon, goes before the argument's type. + +```elixir +@spec to_hex({hue :: integer, saturation :: integer, lightness :: integer}) :: String.t() +``` + +### Custom types + +Typespecs aren't limited to just the built-in types. Custom types can be defined using the `@type` module attribute. A custom type definition starts with the type's name, followed by a double colon and then the type itself. + +```elixir +@type color :: {hue :: integer, saturation :: integer, lightness :: integer} + +@spec to_hex(color()) :: String.t() +``` + +A custom type can be used from the same module where it's defined, or from another module. + +[markdown]: https://docs.github.com/en/github/writing-on-github/basic-writing-and-formatting-syntax +[types]: https://hexdocs.pm/elixir/typespecs.html#types-and-their-syntax + +## Instructions + +You have been working in the city office for a while, and you have developed a set of tools that speed up your day-to-day work, for example with filling out forms. + +Now, a new colleague is joining you, and you realized your tools might not be self-explanatory. There are a lot of weird conventions in your office, like always filling out forms with uppercase letters and avoiding leaving fields empty. + +You decide to write some documentation so that it's easier for your new colleague to hop right in and start using your tools. + +## 1. Document the purpose of the form tools + +Add documentation to the `Form` module that describes its purpose. It should read: + +``` +A collection of loosely related functions helpful for filling out various forms at the city office. +``` + +## 2. Document filling out fields with blank values + +Add documentation and a typespec to the `Form.blanks/1` function. The documentation should read: + +``` +Generates a string of a given length. + +This string can be used to fill out a form field that is supposed to have no value. +Such fields cannot be left empty because a malicious third party could fill them out with false data. +``` + +The typespec should explain that the function accepts a single argument, a non-negative integer, and returns a string. + +## 3. Document splitting values into lists of uppercase letters + +Add documentation and a typespec to the `Form.letters/1` function. The documentation should read: + +``` +Splits the string into a list of uppercase letters. + +This is needed for form fields that don't offer a single input for the whole string, +but instead require splitting the string into a predefined number of single-letter inputs. +``` + +The typespec should explain that the function accepts a single argument, a string, and returns a list of strings. + +## 4. Document checking if a value fits a field with a max length + +Add documentation and a typespec to the `Form.check_length/2` function. The documentation should read: + +``` +Checks if the value has no more than the maximum allowed number of letters. + +This is needed to check that the values of fields do not exceed the maximum allowed length. +It also tells you by how much the value exceeds the maximum. +``` + +The typespec should explain that the function accepts two arguments, a string and a non-negative integer, and returns one of two possible values. It returns either the `:ok` atom or a 2-tuple with the first element being the `:error` atom, and the second a positive integer. + +## 5. Document different address formats + +For some unknown to you reason, the city office's internal system uses two different ways of representing addresses - either as a map or as a tuple. + +Document this fact by defining three custom public types: +- `address_map` - a map with the keys `:street`, `:postal_code`, and `:city`. Each key holds a value of type string. +- `address_tuple` - a tuple with three values - `street`, `postal_code`, and `city`. Each value is of type string. Differentiate the values by giving them names in the typespec. +- `address` - can be either an `address_map` or an `address_tuple`. + +## 6. Document formatting the address + +Add documentation and a typespec to the `Form.format_address/1` function. The documentation should read: + +``` +Formats the address as an uppercase multiline string. +``` + +The typespec should explain that the function accepts one argument, an address, and returns a string. + +## Source + +### Created by + +- @angelikatyborska + +### Contributed to by + +- @neenjaw +- @michallepicki \ No newline at end of file diff --git a/city-office/lib/form.ex b/city-office/lib/form.ex new file mode 100644 index 0000000..d5ab640 --- /dev/null +++ b/city-office/lib/form.ex @@ -0,0 +1,75 @@ +defmodule Form do + @moduledoc """ + A collection of loosely related functions helpful for filling out various forms at the city office. + """ + + @doc """ + Generates a string of a given length. + + This string can be used to fill out a form field that is supposed to have no value. + Such fields cannot be left empty because a malicious third party could fill them out with false data. + """ + + @spec blanks(n :: non_neg_integer()) :: String.t() + + def blanks(n) do + String.duplicate("X", n) + end + + @doc """ + Splits the string into a list of uppercase letters. + + This is needed for form fields that don't offer a single input for the whole string, + but instead require splitting the string into a predefined number of single-letter inputs. + """ + + @spec letters(word :: String.t()) :: [String.t()] + + def letters(word) do + word + |> String.upcase() + |> String.split("", trim: true) + end + + @doc """ + Checks if the value has no more than the maximum allowed number of letters. + + This is needed to check that the values of fields do not exceed the maximum allowed length. + It also tells you by how much the value exceeds the maximum. + """ + + @spec check_length(word :: String.t(), length :: non_neg_integer()) :: :ok | {:error, pos_integer()} + + def check_length(word, length) do + diff = String.length(word) - length + + if diff <= 0 do + :ok + else + {:error, diff} + end + end + + @type address_map :: %{street: String.t(), postal_code: String.t(), city: String.t()} + + @type address_tuple :: {street :: String.t(), postal_code :: String.t(), city :: String.t()} + + @type address :: address_map | address_tuple + + def format_address(%{street: street, postal_code: postal_code, city: city}) do + format_address({street, postal_code, city}) + end + + @doc """ + Formats the address as an uppercase multiline string. + """ + + @spec format_address(address) :: String.t() + + def format_address({street, postal_code, city}) do + """ + #{String.upcase(street)} + #{String.upcase(postal_code)} #{String.upcase(city)} + """ + end +end diff --git a/city-office/mix.exs b/city-office/mix.exs new file mode 100644 index 0000000..9e8d828 --- /dev/null +++ b/city-office/mix.exs @@ -0,0 +1,28 @@ +defmodule Form.MixProject do + use Mix.Project + + def project do + [ + app: :city_office, + version: "0.1.0", + # elixir: "~> 1.10", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + # {:dep_from_hexpm, "~> 0.3.0"}, + # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} + ] + end +end diff --git a/city-office/test/form_test.exs b/city-office/test/form_test.exs new file mode 100644 index 0000000..0f9623a --- /dev/null +++ b/city-office/test/form_test.exs @@ -0,0 +1,292 @@ +defmodule FormTest do + use ExUnit.Case + + # Dear Elixir learner, + # If you're reading this test suite to gain some insights, + # please be advised that it is somewhat unusual. + # + # Don't worry if you don't understand this test suite at this stage of your learning journey. + # We had to use some advanced features to be able to write assertions about docs and typespecs. + # You wouldn't normally write assertions for that in a typical codebase. + # We're doing it here strictly for educational purposes. + + defmacrop assert_moduledoc(expected_moduledoc) do + quote do + {:docs_v1, _, _, _, module_doc, _, _} = Code.fetch_docs(Form) + + if module_doc == :none do + flunk("expected the module Form to have documentation") + else + actual_moduledoc = module_doc["en"] + assert actual_moduledoc == unquote(expected_moduledoc) + end + end + end + + defmacrop assert_doc({function_name, function_arity}, expected_doc) do + quote do + {:docs_v1, _, _, _, _, _, docs} = Code.fetch_docs(Form) + + {_, _, _, doc_content, _} = + Enum.find(docs, fn {{kind, function_name, arity}, _, _, _, _} -> + {kind, function_name, arity} == + {:function, unquote(function_name), unquote(function_arity)} + end) + + if doc_content == :none do + flunk( + "expected the function Form.#{unquote(function_name)}/#{unquote(function_arity)} to have documentation" + ) + else + actual_doc = doc_content["en"] + assert actual_doc == unquote(expected_doc) + end + end + end + + defmacrop assert_spec({function_name, function_arity}, arguments_specs, return_spec) do + quote do + {:ok, specs} = Code.Typespec.fetch_specs(Form) + + spec = + Enum.find(specs, fn {{function_name, arity}, _} -> + {function_name, arity} == {unquote(function_name), unquote(function_arity)} + end) + + assert spec, + "expected the function Form.#{unquote(function_name)}/#{unquote(function_arity)} to have a typespec" + + {{unquote(function_name), unquote(function_arity)}, [{:type, _, :fun, _} = function_spec]} = + spec + + {:"::", _, [arguments, return]} = + Code.Typespec.spec_to_quoted(unquote(function_name), function_spec) + + accepted_arguments_specs = + Enum.map(unquote(arguments_specs), fn arguments_spec -> + "#{unquote(function_name)}(#{arguments_spec})" + end) + + actual_arguments_spec = Macro.to_string(arguments) + assert actual_arguments_spec in accepted_arguments_specs + + expected_return_spec = unquote(return_spec) + actual_return_spec = Macro.to_string(return) + assert actual_return_spec == expected_return_spec + end + end + + defmacrop assert_type({module_name, type_name}, expected_type_definition) do + quote do + {:ok, types} = Code.Typespec.fetch_types(unquote(module_name)) + + type = + Enum.find(types, fn {declaration, {type_name, _, _}} -> + declaration == :type && type_name == unquote(type_name) + end) + + assert type, + "expected the module #{unquote(module_name)} to have a public type named #{unquote(type_name)}" + + {:type, type} = type + + {:"::", _, [_, type_definition]} = Code.Typespec.type_to_quoted(type) + + actual_type_definition = Macro.to_string(type_definition) + + if is_list(unquote(expected_type_definition)) do + if actual_type_definition in unquote(expected_type_definition) do + assert true + else + # we know this will fail at this point, but we're using it to provide a nice failure message + assert actual_type_definition == hd(unquote(expected_type_definition)) + end + else + assert actual_type_definition == unquote(expected_type_definition) + end + end + end + + describe "the Form module" do + @tag task_id: 1 + test "has documentation" do + expected_moduledoc = """ + A collection of loosely related functions helpful for filling out various forms at the city office. + """ + + assert_moduledoc(expected_moduledoc) + end + end + + describe "blanks/1" do + @tag task_id: 2 + test "returns a string with Xs of a given length" do + assert Form.blanks(5) == "XXXXX" + end + + @tag task_id: 2 + test "returns an empty string when given length is 0" do + assert Form.blanks(0) == "" + end + + @tag task_id: 2 + test "has documentation" do + expected_doc = """ + Generates a string of a given length. + + This string can be used to fill out a form field that is supposed to have no value. + Such fields cannot be left empty because a malicious third party could fill them out with false data. + """ + + assert_doc({:blanks, 1}, expected_doc) + end + + @tag task_id: 2 + test "has a correct spec" do + assert_spec({:blanks, 1}, ["n :: non_neg_integer()", "non_neg_integer()"], "String.t()") + end + end + + describe "letters/1" do + @tag task_id: 3 + test "returns a list of upcase letters" do + assert Form.letters("Sao Paulo") == ["S", "A", "O", " ", "P", "A", "U", "L", "O"] + end + + @tag task_id: 3 + test "returns an empty list when given an empty string" do + assert Form.letters("") == [] + end + + @tag task_id: 3 + test "has documentation" do + expected_doc = """ + Splits the string into a list of uppercase letters. + + This is needed for form fields that don't offer a single input for the whole string, + but instead require splitting the string into a predefined number of single-letter inputs. + """ + + assert_doc({:letters, 1}, expected_doc) + end + + @tag task_id: 3 + test "has a typespec" do + assert_spec({:letters, 1}, ["word :: String.t()", "String.t()"], "[String.t()]") + end + end + + describe "check_length/2" do + @tag task_id: 4 + test "returns :ok is value is below max length" do + assert Form.check_length("Ruiz", 6) == :ok + end + + @tag task_id: 4 + test "returns :ok is value is of exactly max length" do + assert Form.check_length("Martinez-Cooper", 15) == :ok + end + + @tag task_id: 4 + test "returns an error tuple with the difference between max length and actual length" do + assert Form.check_length("Martinez-Campbell", 10) == {:error, 7} + end + + @tag task_id: 4 + test "has documentation" do + expected_doc = """ + Checks if the value has no more than the maximum allowed number of letters. + + This is needed to check that the values of fields do not exceed the maximum allowed length. + It also tells you by how much the value exceeds the maximum. + """ + + assert_doc({:check_length, 2}, expected_doc) + end + + @tag task_id: 4 + test "has a typespec" do + assert_spec( + {:check_length, 2}, + ["word :: String.t(), length :: non_neg_integer()", "String.t(), non_neg_integer()"], + ":ok | {:error, pos_integer()}" + ) + end + end + + describe "custom types in the Form module" do + @tag task_id: 5 + test "has a custom 'address_map' type" do + expected_type_definitions = [ + "%{street: String.t(), postal_code: String.t(), city: String.t()}", + "%{street: String.t(), city: String.t(), postal_code: String.t()}", + "%{postal_code: String.t(), street: String.t(), city: String.t()}", + "%{postal_code: String.t(), city: String.t(), street: String.t()}", + "%{city: String.t(), street: String.t(), postal_code: String.t()}", + "%{city: String.t(), postal_code: String.t(), street: String.t()}" + ] + + assert_type({Form, :address_map}, expected_type_definitions) + end + + @tag task_id: 5 + test "has a custom 'address_tuple' type with named arguments" do + expected_type_definition = + "{street :: String.t(), postal_code :: String.t(), city :: String.t()}" + + assert_type({Form, :address_tuple}, expected_type_definition) + end + + @tag task_id: 5 + test "has a custom 'address' type that is a union of 'address_map' and 'address_tuple'" do + expected_type_definitions = [ + "address_map() | address_tuple()", + "address_tuple() | address_map()" + ] + + assert_type({Form, :address}, expected_type_definitions) + end + end + + describe "format_address/1" do + @tag task_id: 6 + test "accepts a map" do + input = %{ + street: "Wiejska 4/6/8", + postal_code: "00-902", + city: "Warsaw" + } + + result = """ + WIEJSKA 4/6/8 + 00-902 WARSAW + """ + + assert Form.format_address(input) == result + end + + @tag task_id: 6 + test "accepts a 3 string tuple" do + result = """ + PLATZ DER REPUBLIK 1 + 11011 BERLIN + """ + + assert Form.format_address({"Platz der Republik 1", "11011", "Berlin"}) == result + end + + @tag task_id: 6 + test "has documentation" do + expected_doc = """ + Formats the address as an uppercase multiline string. + """ + + assert_doc({:format_address, 1}, expected_doc) + end + + @tag task_id: 6 + test "has a typespec" do + assert_spec({:format_address, 1}, ["address :: address()", "address()"], "String.t()") + end + end +end diff --git a/city-office/test/test_helper.exs b/city-office/test/test_helper.exs new file mode 100644 index 0000000..e8677a3 --- /dev/null +++ b/city-office/test/test_helper.exs @@ -0,0 +1,2 @@ +ExUnit.start() +ExUnit.configure(exclude: :pending, trace: true, seed: 0)