diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs new file mode 100644 index 0000000..82b28ad --- /dev/null +++ b/.dialyzer_ignore.exs @@ -0,0 +1,3 @@ +[ + {":0:unknown_type Unknown type: YamlElixir.ParsingError.t/0."} +] diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 6c01c7b..2f2db27 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -2,24 +2,25 @@ name: Yamel CD on: workflow_run: - workflows: ["Yamel Master CI"] + workflows: ["Yamel Main CI"] types: - completed jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Elixir - uses: actions/setup-elixir@v1 + uses: erlef/setup-beam@v1 with: - elixir-version: '1.8.x' - otp-version: '22.3' + elixir-version: '1.8.2' + otp-version: '22.3.4.26' + version-type: 'strict' - name: Cache Dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | _build/prod/lib @@ -50,18 +51,19 @@ jobs: run: mix docs check_package: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: build steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Elixir - uses: actions/setup-elixir@v1 + uses: erlef/setup-beam@v1 with: - elixir-version: '1.8.x' - otp-version: '22.3' + elixir-version: '1.8.2' + otp-version: '22.3.4.26' + version-type: 'strict' - name: Cache Dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | _build/prod/lib @@ -77,18 +79,19 @@ jobs: run: mix hex.publish --dry-run update_version: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: check_package env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Elixir - uses: actions/setup-elixir@v1 + uses: erlef/setup-beam@v1 with: - elixir-version: '1.8.x' - otp-version: '22.3' + elixir-version: '1.8.2' + otp-version: '22.3.4.26' + version-type: 'strict' - name: Update README.md and mix.exs run: | elixir ./scripts/update_version.exs @@ -104,18 +107,19 @@ jobs: publish_package: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: update_version steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Elixir - uses: actions/setup-elixir@v1 + uses: erlef/setup-beam@v1 with: - elixir-version: '1.8.x' - otp-version: '22.3' + elixir-version: '1.8.2' + otp-version: '22.3.4.26' + version-type: 'strict' - name: Cache Dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | _build/prod/lib @@ -128,4 +132,4 @@ jobs: - env: HEX_API_KEY: ${{ secrets.HEX_API_KEY }} - run: mix hex.publish --yes \ No newline at end of file + run: mix hex.publish --yes diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a586509..b6d9312 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,23 +1,29 @@ name: Yamel CI on: + push: + branches: [ "main" ] pull_request: - branches: - - master + branches: [ "main" ] jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 + strategy: + matrix: + otp: ['22.3.4.26'] + elixir: ['1.8.2'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Elixir - uses: actions/setup-elixir@v1 + uses: erlef/setup-beam@v1 with: - elixir-version: '1.8.x' - otp-version: '22.3' + elixir-version: '1.8.2' + otp-version: '22.3.4.26' + version-type: 'strict' - name: Cache Dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | deps @@ -40,18 +46,19 @@ jobs: run: mix compile lint: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: build steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Elixir - uses: actions/setup-elixir@v1 + uses: erlef/setup-beam@v1 with: - elixir-version: '1.8.x' - otp-version: '22.3' + elixir-version: '1.8.2' + otp-version: '22.3.4.26' + version-type: 'strict' - name: Cache Dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | deps @@ -65,18 +72,19 @@ jobs: run: mix format --check-formatted test: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: build steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Elixir - uses: actions/setup-elixir@v1 + uses: erlef/setup-beam@v1 with: - elixir-version: '1.8.x' - otp-version: '22.3' + elixir-version: '1.8.2' + otp-version: '22.3.4.26' + version-type: 'strict' - name: Cache Dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | deps @@ -84,27 +92,49 @@ jobs: key: ${{ runner.os }}-mix-test-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} restore-keys: | ${{ runner.os }}-mix-test- - - run: mix test + typecheck: + runs-on: ubuntu-20.04 + needs: build + + steps: + - uses: actions/checkout@v3 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: '1.8.2' + otp-version: '22.3.4.26' + version-type: 'strict' + - name: Cache Dependencies + uses: actions/cache@v3 + with: + path: | + deps + _build/test/lib + key: ${{ runner.os }}-mix-test-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} + restore-keys: | + ${{ runner.os }}-mix-test- + - run: mix dialyzer + coverage: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: test env: MIX_ENV: test GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v2 - + - uses: actions/checkout@v3 - name: Set up Elixir - uses: actions/setup-elixir@v1 + uses: erlef/setup-beam@v1 with: - elixir-version: '1.9.x' - otp-version: '22.3' + elixir-version: '1.8.2' + otp-version: '22.3.4.26' + version-type: 'strict' - name: Cache Dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | deps diff --git a/.github/workflows/master-ci.yml b/.github/workflows/main-ci.yml similarity index 69% rename from .github/workflows/master-ci.yml rename to .github/workflows/main-ci.yml index 0c47d84..08a5b8f 100644 --- a/.github/workflows/master-ci.yml +++ b/.github/workflows/main-ci.yml @@ -1,23 +1,28 @@ -name: Yamel Master CI +name: Yamel Main CI on: push: branches: - - master + - main jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 + strategy: + matrix: + otp: ['22.3.4.26'] + elixir: ['1.8.2'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Elixir - uses: actions/setup-elixir@v1 + uses: erlef/setup-beam@v1 with: - elixir-version: '1.8.x' - otp-version: '22.3' + elixir-version: '1.8.2' + otp-version: '22.3.4.26' + version-type: 'strict' - name: Cache Dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | deps @@ -40,18 +45,19 @@ jobs: run: mix compile lint: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: build steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Elixir - uses: actions/setup-elixir@v1 + uses: erlef/setup-beam@v1 with: - elixir-version: '1.8.x' - otp-version: '22.3' + elixir-version: '1.8.2' + otp-version: '22.3.4.26' + version-type: 'strict' - name: Cache Dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | deps @@ -65,18 +71,19 @@ jobs: run: mix format --check-formatted test: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: build steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Elixir - uses: actions/setup-elixir@v1 + uses: erlef/setup-beam@v1 with: - elixir-version: '1.8.x' - otp-version: '22.3' + elixir-version: '1.8.2' + otp-version: '22.3.4.26' + version-type: 'strict' - name: Cache Dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | deps @@ -88,23 +95,22 @@ jobs: - run: mix test coverage: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: test env: MIX_ENV: test GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v2 - + - uses: actions/checkout@v3 - name: Set up Elixir - uses: actions/setup-elixir@v1 + uses: erlef/setup-beam@v1 with: - elixir-version: '1.9.x' - otp-version: '22.3' - + elixir-version: '1.8.2' + otp-version: '22.3.4.26' + version-type: 'strict' - name: Cache Dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | deps @@ -124,13 +130,13 @@ jobs: trigger-cd: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: coverage env: GITHUB_TOKEN: ${{ secrets.GITHUB_ACTIONS_USER }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Trigger CD run: | git config user.name Lucasbot diff --git a/.gitignore b/.gitignore index b9c112e..3683092 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ erl_crash.dump # Ignore package tarball (built via "mix hex.build"). yamel-*.tar + +/priv/plts/*.plt +/priv/plts/*.plt.hash diff --git a/.tool-versions b/.tool-versions index ffcf012..e650815 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1,2 @@ -elixir 1.8.2 +elixir 1.8.2-otp-22 +erlang 22.3.4.26 diff --git a/README.md b/README.md index f8c7532..7f93190 100644 --- a/README.md +++ b/README.md @@ -8,27 +8,6 @@ --- -### WELCOME HACKTOBERFEST 2022 PARTYPEOPLE!! πŸŽ‰πŸŽŠπŸ₯³πŸ‘©πŸΏβ€πŸ’»πŸ‘©πŸΎβ€πŸ’»πŸ‘©πŸ½β€πŸ’»πŸ‘©πŸΌβ€πŸ’»πŸ‘©πŸ»β€πŸ’»πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»πŸ‘¨πŸ»β€πŸ’»πŸ‘¨πŸΌβ€πŸ’»πŸ‘¨πŸ½β€πŸ’»πŸ‘¨πŸΎβ€πŸ’»πŸ‘¨πŸΏβ€πŸ’» - -#### Here are some few things I'd like to ask you beforehand for this event! - -##### Basic Code of Conduct -1. Be nice to other people, no matter what, should you interact with anyone here on GitHub or outside. - - - Keep in mind: everybody is doing their best, so there's no reason to be harsh to anyone. - -2. Open **short**, but **meaningful** and **valuable**, **PR**(s). This way is easier to review on time. - - - E.g.: Instead of document a whole module, you can document just one function, for instance. This goes with the Quality over Quantity value from Hacktoberfest. - -3. This is a fresh repository which I'm already using in production. I apologize for the lack of more information but feel free to interact with me through the Issues or [twitter](https://twitter.com/lu_gico) with the hashtag #elixirYamel. - -#### Let the hack begin! 🦜 - -#### We appreciate your collaboration! Thank you very much!! πŸ™πŸΏπŸ™πŸΎπŸ™πŸ½πŸ™πŸΌπŸ™πŸ»πŸ™βœ¨ - ---- - An [yaml](https://en.wikipedia.org/wiki/YAML) parser and serializer to work with Yaml files in Elixir. ## Installation diff --git a/lib/yamel.ex b/lib/yamel.ex index 3163c34..798bdf9 100644 --- a/lib/yamel.ex +++ b/lib/yamel.ex @@ -1,27 +1,43 @@ defmodule Yamel do @moduledoc """ - Module to work with YAML strings in Elixir. + Defines functions to work with YAML in Elixir. """ @type t :: map() | list() - @type yaml :: binary() - @type keys :: :atoms | :strings - @type decode_opt :: {:keys, keys} + @type yaml :: String.t() + @type keys :: :atom | :string | :atoms | :strings + @type quotable_types :: :atom | :string | :number | :boolean + @type decode_opts :: [keys: keys] | [] + @type encode_opts :: [quote: quotable_types] | [] + @type parse_error :: YamlElixir.ParsingError.t() @doc ~S""" - Decode a YAML string into it respective data type in elixir, throwing an exception if the - string can not be encoded + Decodes a YAML string to a map or list, throws if fails. + + Throws `parse_error()` exception if given YAML cannot be parsed. + + ## Options + * `:keys` - indicates the type for the map's keys. Default: `:string` ## Examples - iex> Yamel.decode!(" - ...> name: Jane Doe - ...> job: Developer - ...> skill: Elite - ...> employed: True") - %{"employed" => true, "job" => "Developer", "name" => "Jane Doe", "skill" => "Elite"} + iex> Yamel.decode!(" + ...> name: Jane Doe + ...> job: Developer + ...> skill: Elite + ...> employed: True") + %{"employed" => true, "job" => "Developer", "name" => "Jane Doe", "skill" => "Elite"} + + With option `keys: :atom` + + iex> Yamel.decode!(" + ...> name: Jane Doe + ...> job: Developer + ...> skill: Elite + ...> employed: True", keys: :atom) + %{employed: true, job: "Developer", name: "Jane Doe", skill: "Elite"} """ - @spec decode!(yaml(), [decode_opt]) :: Yamel.t() + @spec decode!(yaml(), decode_opts()) :: Yamel.t() def decode!(yaml_string, options \\ []) do keys = Keyword.get(options, :keys, :strings) @@ -31,9 +47,12 @@ defmodule Yamel do end @doc ~S""" - Decode a YAML string into it respective data type in elixir, returning `{:ok, Yamel.t()}` - where the second term is the Yamel in Elixir representation. Otherwise, returns `{:error, reason}` - where `reason` is a `YamlElixir` error module with a message inside + Decodes a YAML string to a map or list. + + Returns `{:ok, Yamel.t()}` or `{:error, reason}`, where reason is `parse_error()` + + ## Options + * `:keys` - indicates the type for the map's keys. Default: `:string` ## Examples @@ -45,14 +64,17 @@ defmodule Yamel do ...> - Mango") {:ok, ["Apple", "Orange","Strawberry", "Mango"]} """ - - @spec decode(yaml(), [decode_opt]) :: {:ok, Yamel.t()} | {:error, reason :: binary()} + @spec decode(yaml(), decode_opts()) :: {:ok, Yamel.t()} | {:error, parse_error()} def decode(yaml_string, options \\ []) do - keys = Keyword.get(options, :keys, :strings) + keys = Keyword.get(options, :keys, :string) - yaml_string - |> YamlElixir.read_from_string() - |> maybe_atom(keys) + case YamlElixir.read_from_string(yaml_string) do + {:ok, yaml} -> + {:ok, maybe_atom(yaml, keys)} + + error -> + error + end end @doc ~S""" @@ -60,9 +82,8 @@ defmodule Yamel do can not be encoded. ## Options - - * `:quote` - The value types to be quoted. - Supported value types are: `[:atom, :boolean, :number, :string]` + + * `:quote` - The value types to be quoted. ## Examples @@ -77,7 +98,7 @@ defmodule Yamel do "- foo\n- \"bar\"\n- 12.3\n- \"true\"\n\n" """ - @spec encode!(Yamel.t()) :: yaml() + @spec encode!(Yamel.t(), encode_opts()) :: yaml() def encode!(map_or_list, opts \\ []) def encode!(map_or_list, opts) @@ -93,9 +114,8 @@ defmodule Yamel do being a string stating the error reason. ## Options - - * `:quote` - The value types to be quoted. - Supported value types are: `[:atom, :boolean, :number, :string]` + + * `:quote` - The value types to be quoted. ## Examples @@ -110,7 +130,7 @@ defmodule Yamel do {:ok, "- foo\n- \"bar\"\n- 12.3\n- \"true\"\n\n"} """ - @spec encode(Yamel.t(), opts :: keyword()) :: {:ok, yaml()} | {:error, reason :: binary()} + @spec encode(Yamel.t(), encode_opts()) :: {:ok, yaml()} | {:error, reason :: String.t()} def encode(map_or_list, opts \\ []) def encode(map_or_list, opts) when is_map(map_or_list) or is_list(map_or_list), @@ -122,7 +142,7 @@ defmodule Yamel do defp maybe_atom({:error, _reason} = error, _keys), do: error - defp maybe_atom(map, :atoms) when is_map(map) do + defp maybe_atom(map, keys) when is_map(map) and keys in [:atom, :atoms] do for {key, value} <- map, into: %{} do cond do is_atom(key) -> {key, maybe_atom(value, :atoms)} @@ -131,7 +151,7 @@ defmodule Yamel do end end - defp maybe_atom(list, :atoms) when is_list(list) do + defp maybe_atom(list, keys) when is_list(list) and keys in [:atom, :atoms] do for value <- list, into: [] do cond do is_map(value) -> maybe_atom(value, :atoms) diff --git a/mix.exs b/mix.exs index cf8bd5f..6bb40b8 100644 --- a/mix.exs +++ b/mix.exs @@ -22,6 +22,10 @@ defmodule Yamel.MixProject do "coveralls.detail": :test, "coveralls.post": :test, "coveralls.html": :test + ], + dialyzer: [ + plt_file: {:no_warn, "priv/plts/yamel.plt"}, + list_unused_filters: true ] ] end @@ -38,7 +42,8 @@ defmodule Yamel.MixProject do [ {:yaml_elixir, "~> 2.4.0"}, {:ex_doc, ">= 0.0.0", runtime: false, only: :dev}, - {:excoveralls, "~> 0.13.2", only: :test} + {:excoveralls, "~> 0.13.2", only: :test}, + {:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false} ] end diff --git a/mix.lock b/mix.lock index aebac3c..0036003 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,8 @@ %{ "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"}, + "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.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.22.6", "0fb1e09a3e8b69af0ae94c8b4e4df36995d8c88d5ec7dbd35617929144b62c00", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "1e0aceda15faf71f1b0983165e6e7313be628a460e22a031e32913b98edbd638"}, "excoveralls": {:hex, :excoveralls, "0.13.2", "5ca05099750c086f144fcf75842c363fc15d7d9c6faa7ad323d010294ced685e", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1e7ed75c158808a5a8f019d3ad63a5efe482994f2f8336c0a8c77d2f0ab152ce"}, "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, diff --git a/test/yamel_test.exs b/test/yamel_test.exs index c5e0635..61b892d 100644 --- a/test/yamel_test.exs +++ b/test/yamel_test.exs @@ -418,7 +418,7 @@ defmodule YamelTest do expected = %{foo: "bar", zoo: %{fruit: "apple", name: "steve", sport: "baseball"}} - assert Yamel.decode!(yaml, keys: :atoms) == expected + assert Yamel.decode!(yaml, keys: :atom) == expected end end @@ -435,7 +435,61 @@ defmodule YamelTest do expected = [%{foo: %{a: 1, b: 2}}, %{bar: %{c: 3, d: 4}}] - assert Yamel.decode(yaml, keys: :atoms) == {:ok, expected} + assert Yamel.decode(yaml, keys: :atom) == {:ok, expected} + end + + test "when structure is a list" do + yaml = ~S""" + - Apple + - Orange + - Strawberry + - Mango + """ + + expected = ["Apple", "Orange", "Strawberry", "Mango"] + + assert Yamel.decode(yaml) == {:ok, expected} + end + + test "when structure is list with option keys: :atom" do + yaml = ~S""" + - Apple + - Orange + - Strawberry + - Mango + """ + + expected = ["Apple", "Orange", "Strawberry", "Mango"] + + assert Yamel.decode(yaml, keys: :atom) == {:ok, expected} + end + + test "when structure is list with map" do + yaml = ~S""" + - Apple + - Orange + - Strawberry + - Mango + - Fruit: Caju + """ + + expected = ["Apple", "Orange", "Strawberry", "Mango", %{"Fruit" => "Caju"}] + + assert Yamel.decode(yaml) == {:ok, expected} + end + + test "when structure is list with map and option keys: :atom" do + yaml = ~S""" + - Apple + - Orange + - Strawberry + - Mango + - Fruit: Caju + """ + + expected = ["Apple", "Orange", "Strawberry", "Mango", %{:Fruit => "Caju"}] + + assert Yamel.decode(yaml, keys: :atom) == {:ok, expected} end end end