Initial upload
This commit is contained in:
22
elixir/city-office/.exercism/config.json
Normal file
22
elixir/city-office/.exercism/config.json
Normal file
@@ -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."
|
||||
}
|
1
elixir/city-office/.exercism/metadata.json
Normal file
1
elixir/city-office/.exercism/metadata.json
Normal file
@@ -0,0 +1 @@
|
||||
{"track":"elixir","exercise":"city-office","id":"f8d9471439664a2092d435381c8af052","url":"https://exercism.org/tracks/elixir/exercises/city-office","handle":"halfdan","is_requester":true,"auto_approve":false}
|
4
elixir/city-office/.formatter.exs
Normal file
4
elixir/city-office/.formatter.exs
Normal file
@@ -0,0 +1,4 @@
|
||||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
24
elixir/city-office/.gitignore
vendored
Normal file
24
elixir/city-office/.gitignore
vendored
Normal file
@@ -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
|
||||
|
75
elixir/city-office/HELP.md
Normal file
75
elixir/city-office/HELP.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# 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/<FILE>.exs:LINENUM` - runs only a single test, the test from `<FILE>.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 support channel on gitter](https://gitter.im/exercism/support)
|
||||
- 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.
|
||||
If you can't find what you're looking for in the documentation, feel free to ask help in the Exercism's BEAM [gitter channel](https://gitter.im/exercism/xerlang).
|
63
elixir/city-office/HINTS.md
Normal file
63
elixir/city-office/HINTS.md
Normal file
@@ -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
|
173
elixir/city-office/README.md
Normal file
173
elixir/city-office/README.md
Normal file
@@ -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.
|
||||
|
||||
[types]: https://hexdocs.pm/elixir/typespecs.html#types-and-their-syntax
|
||||
[markdown]: https://docs.github.com/en/github/writing-on-github/basic-writing-and-formatting-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
|
65
elixir/city-office/lib/form.ex
Normal file
65
elixir/city-office/lib/form.ex
Normal file
@@ -0,0 +1,65 @@
|
||||
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(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(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(String.t(), 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()
|
||||
|
||||
@doc """
|
||||
Formats the address as an uppercase multiline string.
|
||||
"""
|
||||
@spec format_address(address()) :: String.t()
|
||||
def format_address(%{street: street, postal_code: postal_code, city: city}) do
|
||||
format_address({street, postal_code, city})
|
||||
end
|
||||
|
||||
def format_address({street, postal_code, city}) do
|
||||
"""
|
||||
#{String.upcase(street)}
|
||||
#{String.upcase(postal_code)} #{String.upcase(city)}
|
||||
"""
|
||||
end
|
||||
end
|
28
elixir/city-office/mix.exs
Normal file
28
elixir/city-office/mix.exs
Normal file
@@ -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
|
292
elixir/city-office/test/form_test.exs
Normal file
292
elixir/city-office/test/form_test.exs
Normal file
@@ -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
|
2
elixir/city-office/test/test_helper.exs
Normal file
2
elixir/city-office/test/test_helper.exs
Normal file
@@ -0,0 +1,2 @@
|
||||
ExUnit.start()
|
||||
ExUnit.configure(exclude: :pending, trace: true, seed: 0)
|
Reference in New Issue
Block a user