diff --git a/.credo.exs b/.credo.exs index d7578b9..d95ad67 100644 --- a/.credo.exs +++ b/.credo.exs @@ -3,7 +3,7 @@ %{ name: "default", files: %{ - included: ["lib/", "src/", "web/", "apps/"], + included: ["lib/", "src/"], excluded: [] }, plugins: [], @@ -22,7 +22,7 @@ # Some rules have a comment before them explaining ways Styler deviates from the Credo rule. # # always expands `A.{B, C}` - {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, + # {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, # including `case`, `fn` and `with` statements {Credo.Check.Consistency.ParameterPatternMatching, false}, # Styler implements this rule with a depth of 3 and minimum repetition of 2 @@ -53,13 +53,19 @@ {Credo.Check.Refactor.MapInto, false}, # in pipes only {Credo.Check.Refactor.MapJoin, false}, - {Credo.Check.Refactor.NegatedConditionsInUnless, false}, - {Credo.Check.Refactor.NegatedConditionsWithElse, false}, + # {Credo.Check.Refactor.NegatedConditionsInUnless, false}, + # {Credo.Check.Refactor.NegatedConditionsWithElse, false}, # allows ecto's `from {Credo.Check.Refactor.PipeChainStart, false}, {Credo.Check.Refactor.RedundantWithClauseResult, false}, {Credo.Check.Refactor.UnlessWithElse, false}, - {Credo.Check.Refactor.WithClauses, false} + {Credo.Check.Refactor.WithClauses, false}, + + # custom ext_fit rules + {Credo.Check.Refactor.Nesting, false}, + {Credo.Check.Refactor.CyclomaticComplexity, false}, + {Credo.Check.Design.TagFIXME, false}, + {Credo.Check.Design.TagTODO, false} ] } } diff --git a/.github/workflows/elixir-quality-checks.yml b/.github/workflows/elixir-quality-checks.yml index 2e4507e..f446052 100644 --- a/.github/workflows/elixir-quality-checks.yml +++ b/.github/workflows/elixir-quality-checks.yml @@ -35,10 +35,10 @@ jobs: # Check formatting even if there were unused deps so that # we give devs as much feedback as possible & save some time. if: always() - # - name: Run Credo - # run: mix credo suggest --min-priority=normal - # # Run Credo even if formatting or the unused deps check failed - # if: always() + - name: Run Credo + run: mix credo suggest --min-priority=normal + # Run Credo even if formatting or the unused deps check failed + if: always() # - name: Check for compile-time dependencies # run: mix xref graph --label compile-connected --fail-above 0 # if: always() diff --git a/lib/ext_fit.ex b/lib/ext_fit.ex index a51db7c..163f3c7 100644 --- a/lib/ext_fit.ex +++ b/lib/ext_fit.ex @@ -1,7 +1,7 @@ defmodule ExtFit do - @external_resource "README.md" @moduledoc "README.md" |> File.read!() |> String.split("") |> Enum.fetch!(1) + @external_resource "README.md" end diff --git a/lib/ext_fit/decode.ex b/lib/ext_fit/decode.ex index 0cb270e..080ff36 100644 --- a/lib/ext_fit/decode.ex +++ b/lib/ext_fit/decode.ex @@ -6,12 +6,22 @@ defmodule ExtFit.Decode do See `decode/2` for details. """ - require Logger import Bitwise - alias ExtFit.{Types, Profile, Processor, Record, Field, Chunk} - alias ExtFit.Record.{FitHeader, FitCrc, FitDefinition, FitData} + + alias ExtFit.Chunk + alias ExtFit.Field + alias ExtFit.Processor + alias ExtFit.Profile + alias ExtFit.Record + alias ExtFit.Record.FitCrc + alias ExtFit.Record.FitData + alias ExtFit.Record.FitDefinition + alias ExtFit.Record.FitHeader + alias ExtFit.Types alias Types.FieldData + require Logger + @default_opts %{ expand_components: true, processors: [ @@ -117,7 +127,7 @@ defmodule ExtFit.Decode do defp build_options(default_opts, opts \\ []) do # TODO: validate options # TODO: add more options(skip crc, skip chunks metadata, etc.), see README.md - Map.merge(default_opts, Enum.into(opts, %{})) + Map.merge(default_opts, Map.new(opts)) end # > 12 byte header is considered legacy, using the 14 byte header is preferred @@ -141,7 +151,7 @@ defmodule ExtFit.Decode do __chunk__: chunk } - if header_size not in [12, 14] do + unless header_size in [12, 14] do Logger.warning("Header is not valid size, still trying to decode: #{inspect(header)}") end @@ -185,7 +195,8 @@ defmodule ExtFit.Decode do {body, rest} end - read_data_records(body, [], state) + body + |> read_data_records([], state) |> case do {:ok, v, state} -> {:ok, v, rest, state} {:error, _err_msg, _records, _rest} = err -> err @@ -197,48 +208,50 @@ defmodule ExtFit.Decode do end defp read_data_records(<>, records, state) do - case header do - # definition message - <<0::1, 1::1, dev_data_flag::1, 0::1, local_mesg_num::integer-little-size(4)-unit(1)>> -> - record_header = %{is_developer_data: dev_data_flag == 1, local_mesg_num: local_mesg_num} - - case read_def_mesg(rest, record_header, state) do - {:ok, _def_mesg, _rest, _state} = result -> result - {:error, reason} -> {:error, reason, records, rest} - end + case_result = + case header do + # definition message + <<0::1, 1::1, dev_data_flag::1, 0::1, local_mesg_num::integer-little-size(4)-unit(1)>> -> + record_header = %{is_developer_data: dev_data_flag == 1, local_mesg_num: local_mesg_num} + + case read_def_mesg(rest, record_header, state) do + {:ok, _def_mesg, _rest, _state} = result -> result + {:error, reason} -> {:error, reason, records, rest} + end - # data message - <<0::1, 0::1, dev_data_flag::1, 0::1, local_mesg_num::integer-little-size(4)-unit(1)>> -> - record_header = %{is_developer_data: dev_data_flag == 1, local_mesg_num: local_mesg_num} + # data message + <<0::1, 0::1, dev_data_flag::1, 0::1, local_mesg_num::integer-little-size(4)-unit(1)>> -> + record_header = %{is_developer_data: dev_data_flag == 1, local_mesg_num: local_mesg_num} - case Map.fetch(state.local_num_mapping, local_mesg_num) do - {:ok, def_mesg} -> - read_data_mesg(rest, record_header, def_mesg, state) + case Map.fetch(state.local_num_mapping, local_mesg_num) do + {:ok, def_mesg} -> + read_data_mesg(rest, record_header, def_mesg, state) - _ -> - {:error, "Missing mesg num: #{local_mesg_num}", records, rest} - end + _ -> + {:error, "Missing mesg num: #{local_mesg_num}", records, rest} + end - # compressed header - <<1::1, local_mesg_num::2, time_offset::unit(1)-size(5)-integer>> -> - record_header = %{ - is_developer_data: false, - local_mesg_num: local_mesg_num, - time_offset: time_offset - } + # compressed header + <<1::1, local_mesg_num::2, time_offset::unit(1)-size(5)-integer>> -> + record_header = %{ + is_developer_data: false, + local_mesg_num: local_mesg_num, + time_offset: time_offset + } - case Map.fetch(state.local_num_mapping, local_mesg_num) do - {:ok, def_mesg} -> - read_data_mesg(rest, record_header, def_mesg, state) + case Map.fetch(state.local_num_mapping, local_mesg_num) do + {:ok, def_mesg} -> + read_data_mesg(rest, record_header, def_mesg, state) - _ -> - {:error, "Missing mesg num: #{local_mesg_num}", records, rest} - end + _ -> + {:error, "Missing mesg num: #{local_mesg_num}", records, rest} + end - _ -> - {:ok, rest, state} - end - |> case do + _ -> + {:ok, rest, state} + end + + case case_result do {:ok, rest, state} -> read_data_records(rest, records, state) @@ -324,7 +337,8 @@ defmodule ExtFit.Decode do <> = rest field = - Map.get(state.dev_field_defs, dev_data_index) + state.dev_field_defs + |> Map.get(dev_data_index) |> Map.fetch!(:fields) |> Map.fetch!(num) @@ -356,7 +370,8 @@ defmodule ExtFit.Decode do } state = - put_in(state, [:local_num_mapping, header.local_mesg_num], def_mesg) + state + |> put_in([:local_num_mapping, header.local_mesg_num], def_mesg) |> Map.put(:chunk, next_chunk) {:ok, def_mesg, rest, state} @@ -392,9 +407,10 @@ defmodule ExtFit.Decode do } end + all_field_defs = def_mesg.field_defs ++ def_mesg.dev_field_defs + {fields, rest, state} = - (def_mesg.field_defs ++ def_mesg.dev_field_defs) - |> Enum.reduce({[], data, state}, fn field_def, {fields, rest, state} -> + Enum.reduce(all_field_defs, {[], data, state}, fn field_def, {fields, rest, state} -> size = field_def.size <> = rest raw_value = if(field_def.field, do: read_value(bin, def_mesg.endian, field_def)) @@ -410,14 +426,9 @@ defmodule ExtFit.Decode do cmp_bin = cond do - raw_value == :invalid || raw_value == [] -> - <<>> - - def_mesg.endian == :big -> - bin |> :binary.decode_unsigned(:big) |> :binary.encode_unsigned(:little) - - true -> - bin + raw_value == :invalid || raw_value == [] -> <<>> + def_mesg.endian == :big -> bin |> :binary.decode_unsigned(:big) |> :binary.encode_unsigned(:little) + true -> bin end {fields_with_components, state} = @@ -431,7 +442,8 @@ defmodule ExtFit.Decode do end) {fields, rest, state} = - Enum.reverse(fields) + fields + |> Enum.reverse() |> Enum.reduce({[], rest, state}, fn %FieldData{} = fd, {fields_data, rest, state} -> {field_data, state} = resolve_field_data(fd, def_mesg, state, fields) @@ -445,14 +457,15 @@ defmodule ExtFit.Decode do %FitData{ is_developer_data: header.is_developer_data, local_mesg_num: header.local_mesg_num, - fields: all_fields |> Enum.map(&process_field_data(&1, state)), + fields: Enum.map(all_fields, &process_field_data(&1, state)), def_mesg: def_mesg, __chunk__: chunk }, rest, %{state | chunk: next_chunk}} end defp unroll_components_into_fields(field, bin, def_mesg, state, fields, parent_cmp \\ nil) do - get_components(field) + field + |> get_components() |> Enum.reduce({fields, state}, fn %Types.ComponentField{} = cmp, {fields, state} -> cmp_raw_value = extract_component(bin, cmp.bits, cmp.bit_offset) {_, cmp_field} = Enum.find(def_mesg.mesg_type.fields, &(elem(&1, 1).num == cmp.num)) @@ -506,8 +519,7 @@ defmodule ExtFit.Decode do value !!raw_value -> - raw_value - |> apply_scale_offset(field) + apply_scale_offset(raw_value, field) true -> nil @@ -581,7 +593,8 @@ defmodule ExtFit.Decode do else <> = bin - read_single_value(v, invalid, endian, name, size) + v + |> read_single_value(invalid, endian, name, size) |> case do :invalid -> {values, rest} @@ -598,7 +611,7 @@ defmodule ExtFit.Decode do end defp read_single_value(v, invalid, endian, name, size) do - try do + case_result = case {endian, name} do {_, name} when name in ~w(enum sint8 uint8 uint8z byte uint16 uint32 uint64 uint16z uint32z uint64z)a -> # <> = v @@ -622,14 +635,14 @@ defmodule ExtFit.Decode do <> = v v end - |> case do - ^invalid -> :invalid - value -> value - end - rescue - _err in MatchError -> - :invalid + + case case_result do + ^invalid -> :invalid + value -> value end + rescue + _err in MatchError -> + :invalid end defp resolve_subfield(%Types.Subfield{} = sf, _values, _endian), do: {sf, nil} @@ -650,8 +663,7 @@ defmodule ExtFit.Decode do defp resolve_in_ref_fields([reffield | reffields], %Types.Subfield{} = subfield, field, values, endian) do match = - values - |> Enum.find(fn + Enum.find(values, fn %Types.FieldData{field: %{num: num}, raw_value: raw_value} -> num == reffield.num && reffield.raw_value == raw_value @@ -687,7 +699,7 @@ defmodule ExtFit.Decode do defp consume_string_until_null_or_end(<<0, _rest::binary>>), do: [] defp consume_string_until_null_or_end(<>) do - [c | consume_string_until_null_or_end(rest)] |> List.to_string() + List.to_string([c | consume_string_until_null_or_end(rest)]) end # From spec: @@ -703,7 +715,7 @@ defmodule ExtFit.Decode do end defp apply_scale_offset(value, %{offset: offset, scale: scale}) do - do_apply_scale_offset(List.wrap(value), offset, scale) |> hd() + value |> List.wrap() |> do_apply_scale_offset(offset, scale) |> hd() end defp do_apply_scale_offset(values, offset, scale) do @@ -715,18 +727,21 @@ defmodule ExtFit.Decode do end end + @dev_field_names ~w(developer_data_index field_definition_number fit_base_type_id field_name units native_field_num)a defp maybe_store_dev_field(%FitData{fields: _fields, def_mesg: %{mesg_type: %{num: num}}} = mesg, state) do cond do num == Profile.Types.mesg_num(:developer_data_id) -> dev_data_index = - get_data_mesg_field(mesg, :developer_data_index) + mesg + |> get_data_mesg_field(:developer_data_index) |> case do %{value: value} -> value _ -> raise("Missing dev data index") end application_id = - get_data_mesg_field(mesg, :application_id) + mesg + |> get_data_mesg_field(:application_id) |> case do %{value: value} -> value _ -> nil @@ -736,10 +751,8 @@ defmodule ExtFit.Decode do num == Profile.Types.mesg_num(:field_description) -> desc = - ~w(developer_data_index field_definition_number - fit_base_type_id field_name units native_field_num)a - |> Enum.reduce(%{}, fn name, desc -> - value = (get_data_mesg_field(mesg, name) || %{}) |> Map.get(:value) + Enum.reduce(@dev_field_names, %{}, fn name, desc -> + value = Map.get(get_data_mesg_field(mesg, name) || %{}, :value) Map.put(desc, name, value) end) @@ -766,13 +779,9 @@ defmodule ExtFit.Decode do defp maybe_store_dev_field(%FitDefinition{}, state), do: state defp get_data_mesg_field(%FitData{fields: fields}, match) do - fields - |> Enum.find(fn - %{field: %{name: name}} -> - name == match - - _ -> - false + Enum.find(fields, fn + %{field: %{name: name}} -> name == match + _ -> false end) end @@ -784,7 +793,7 @@ defmodule ExtFit.Decode do defp upsert_dev_field(_state, nil, _params, _overwrite), do: raise("Missing dev data index") defp upsert_dev_field(state, dev_data_index, params, overwrite) do - existing_data = if(!overwrite, do: Map.get(state.dev_field_defs, dev_data_index)) || %{fields: %{}} + existing_data = unless(overwrite, do: Map.get(state.dev_field_defs, dev_data_index)) || %{fields: %{}} data = existing_data @@ -810,16 +819,18 @@ defmodule ExtFit.Decode do full_offset = offset + pos nbit = rem(full_offset, 8) - with <> <- bytes do - acc = acc ||| (byte &&& Enum.at(@bit_mask, nbit)) >>> nbit <<< pos + case bytes do + <> -> + acc = acc ||| (byte &&& Enum.at(@bit_mask, nbit)) >>> nbit <<< pos - if nbit == 7 do - {acc, rest} - else - {acc, bytes} - end - else - _ -> {acc, nil} + if nbit == 7 do + {acc, rest} + else + {acc, bytes} + end + + _ -> + {acc, nil} end end) |> elem(0) diff --git a/lib/ext_fit/processor.ex b/lib/ext_fit/processor.ex index b78278b..2a9ed52 100644 --- a/lib/ext_fit/processor.ex +++ b/lib/ext_fit/processor.ex @@ -3,8 +3,9 @@ defmodule ExtFit.Processor do Defines behaviour for processing records and field data. """ - alias ExtFit.Types.{FieldData} - alias ExtFit.Record.{FitData, FitDefinition} + alias ExtFit.Record.FitData + alias ExtFit.Record.FitDefinition + alias ExtFit.Types.FieldData @doc """ Called for each record. diff --git a/lib/ext_fit/processor/debug_processor.ex b/lib/ext_fit/processor/debug_processor.ex index 9f4897d..2b686a6 100644 --- a/lib/ext_fit/processor/debug_processor.ex +++ b/lib/ext_fit/processor/debug_processor.ex @@ -4,11 +4,12 @@ defmodule ExtFit.Processor.DebugProcessor do """ @behaviour ExtFit.Processor - require Logger alias ExtFit.Record, warn: false alias ExtFit.Types.FieldData + require Logger + @log_index [] @inspect_opts [width: :infinity, limit: :infinity, charlists: :as_lists] diff --git a/lib/ext_fit/processor/default_processor.ex b/lib/ext_fit/processor/default_processor.ex index fefd6f4..303f878 100644 --- a/lib/ext_fit/processor/default_processor.ex +++ b/lib/ext_fit/processor/default_processor.ex @@ -10,9 +10,12 @@ defmodule ExtFit.Processor.DefaultProcessor do """ @behaviour ExtFit.Processor + alias ExtFit.Field - alias ExtFit.Types.{FieldType, BaseType, FieldData} - alias ExtFit.Record.{FitData} + alias ExtFit.Record.FitData + alias ExtFit.Types.BaseType + alias ExtFit.Types.FieldData + alias ExtFit.Types.FieldType # Datetimes (uint32) represent seconds since this ``FIT_UTC_REFERENCE`` # (unix timestamp for UTC 00:00 Dec 31 1989). @@ -23,7 +26,7 @@ defmodule ExtFit.Processor.DefaultProcessor do # @fit_datetime_min 0x10000000 def process_record(%FitData{fields: fields, def_mesg: %{mesg_type: %{name: :hr}}} = fdm) do - with true <- !!Enum.find(fields, &(Field.name(&1) == :event_timestamp_12)) do + if Enum.find(fields, &(Field.name(&1) == :event_timestamp_12)) do fields = Enum.map(fields, fn %{field: %{name: :event_timestamp}, value: value} = field when is_number(value) -> @@ -34,7 +37,9 @@ defmodule ExtFit.Processor.DefaultProcessor do |> Cldr.Digits.fraction_as_integer() value = - DateTime.from_unix!(trunc(@fit_utc_reference_s + value)) + (@fit_utc_reference_s + value) + |> trunc() + |> DateTime.from_unix!() |> Map.put(:microsecond, {fraction, 6}) %{field | units: nil, value: value} @@ -45,7 +50,7 @@ defmodule ExtFit.Processor.DefaultProcessor do %{fdm | fields: fields} else - _ -> fdm + fdm end end @@ -76,7 +81,8 @@ defmodule ExtFit.Processor.DefaultProcessor do end defp process_field_value(value, %{type: %FieldType{name: :local_date_time}}) do - DateTime.from_unix!(@fit_utc_reference_s + (value || 0)) + (@fit_utc_reference_s + (value || 0)) + |> DateTime.from_unix!() |> DateTime.to_naive() end diff --git a/lib/ext_fit/profile.ex b/lib/ext_fit/profile.ex index c916768..a8e4f11 100644 --- a/lib/ext_fit/profile.ex +++ b/lib/ext_fit/profile.ex @@ -9,25 +9,25 @@ defmodule ExtFit.Profile do defmacro __using__(:types) do quote do + @before_compile {unquote(__MODULE__), :__compile_types_callbacks__} + import ExtFit.Profile, only: [type: 3, type: 4] Enum.each(unquote(@accumulating_types_attrs), fn attr -> Module.register_attribute(__MODULE__, attr, accumulate: true) end) - - @before_compile {unquote(__MODULE__), :__compile_types_callbacks__} end end defmacro __using__(:messages) do quote do + @before_compile {unquote(__MODULE__), :__compile_messages_callbacks__} + import ExtFit.Profile, only: [message: 2] Enum.each(unquote(@accumulating_messages_attrs), fn attr -> Module.register_attribute(__MODULE__, attr, accumulate: true) end) - - @before_compile {unquote(__MODULE__), :__compile_messages_callbacks__} end end @@ -79,7 +79,7 @@ defmodule ExtFit.Profile do # Opts are accepted but at this moment they're not used later anywhere def __type__(name, type, values, _opts, %Macro.Env{module: mod}) do - types = mod |> Module.get_attribute(:extfit_types) + types = Module.get_attribute(mod, :extfit_types) if Keyword.has_key?(types, name) do raise ArgumentError, "the type #{inspect(name)} is already set" @@ -89,21 +89,17 @@ defmodule ExtFit.Profile do name: name, base_type: Types.base_type_by_name(type), values: - Enum.map(values, fn - {num, value_name, _} -> - {num, %Types.FieldTypeValue{num: num, name: value_name}} - - {num, value_name} -> - {num, %Types.FieldTypeValue{num: num, name: value_name}} + Map.new(values, fn + {num, value_name, _} -> {num, %Types.FieldTypeValue{num: num, name: value_name}} + {num, value_name} -> {num, %Types.FieldTypeValue{num: num, name: value_name}} end) - |> Enum.into(%{}) } Module.put_attribute(mod, :extfit_types, field_type) end def __message__(message_mod, name, %Macro.Env{module: mod}) do - messages = mod |> Module.get_attribute(:extfit_messages) + messages = Module.get_attribute(mod, :extfit_messages) if Keyword.has_key?(messages, name) do raise ArgumentError, "the message #{inspect(name)} is already set" @@ -114,17 +110,18 @@ defmodule ExtFit.Profile do def __message_mod__(_mod, name, block) do {num, _} = - Types.by_name(:mesg_num).values - |> Enum.find({:error, {:value_not_found, name}}, fn {_, v} -> v.name == to_string(name) end) + Enum.find(Types.by_name(:mesg_num).values, {:error, {:value_not_found, name}}, fn {_, v} -> + v.name == to_string(name) + end) quote do + @before_compile {unquote(__MODULE__), :__compile_message_mod_callbacks__} + import ExtFit.Profile, only: [field: 4, field: 3] Module.register_attribute(__MODULE__, :extfit_struct_fields, accumulate: false) Module.register_attribute(__MODULE__, unquote(@accumulating_fields_attr), accumulate: true) - @before_compile {unquote(__MODULE__), :__compile_message_mod_callbacks__} - @num unquote(num) @name unquote(name) unquote(block) @@ -132,7 +129,7 @@ defmodule ExtFit.Profile do end def __field__(mod, num, name, type, opts) do - fields = mod |> Module.get_attribute(@accumulating_fields_attr) + fields = Module.get_attribute(mod, @accumulating_fields_attr) if Keyword.has_key?(fields, name) do raise ArgumentError, "the field #{inspect(name)} is already set" @@ -142,14 +139,10 @@ defmodule ExtFit.Profile do end defmacro __compile_types_callbacks__(%Macro.Env{module: mod}) do - extfit_types = mod |> Module.get_attribute(:extfit_types) + extfit_types = Module.get_attribute(mod, :extfit_types) types = - extfit_types - |> Enum.map(fn type -> - {type.name, type} - end) - |> Enum.into(%{}) + Map.new(extfit_types, fn type -> {type.name, type} end) Enum.each(unquote(@accumulating_types_attrs), &Module.delete_attribute(mod, &1)) Module.put_attribute(mod, :types, types) @@ -162,26 +155,24 @@ defmodule ExtFit.Profile do | Name | Base type | | ------ | ---- | - #{Enum.map(@types, fn {name, type} -> "| #{name} | #{type.base_type.name} |" end) |> Enum.sort() |> Enum.join("\n")} + #{@types |> Enum.map(fn {name, type} -> "| #{name} | #{type.base_type.name} |" end) |> Enum.sort() |> Enum.join("\n")} """ @doc """ Get type by name """ - @spec by_name(atom) :: %ExtFit.Types.FieldType{} | {:error, {:type_not_found, atom}} + @spec by_name(atom) :: ExtFit.Types.FieldType.t() | {:error, {:type_not_found, atom}} def by_name(name) when is_atom(name), do: Map.get(@types, name) || {:error, {:type_not_found, name}} end end defmacro __compile_messages_callbacks__(%Macro.Env{module: mod}) do - extfit_messages = mod |> Module.get_attribute(:extfit_messages) || [] + extfit_messages = Module.get_attribute(mod, :extfit_messages) || [] messages = - extfit_messages - |> Enum.map(fn {name, mod} -> {name, struct(mod)} end) - |> Enum.into(%{}) + Map.new(extfit_messages, fn {name, mod} -> {name, struct(mod)} end) - messages_num_mapping = Enum.map(messages, fn {_, msg} -> {msg.num, msg.name} end) |> Enum.into(%{}) + messages_num_mapping = Map.new(messages, fn {_, msg} -> {msg.num, msg.name} end) Enum.each(unquote(@accumulating_messages_attrs), &Module.delete_attribute(mod, &1)) Module.put_attribute(mod, :messages, messages) @@ -230,14 +221,10 @@ defmodule ExtFit.Profile do end defmacro __compile_message_mod_callbacks__(%Macro.Env{module: mod}) do - fields = mod |> Module.get_attribute(:extfit_acc_fields) || [] + fields = Module.get_attribute(mod, :extfit_acc_fields) || [] fields = - fields - |> Enum.map(&elem(&1, 1)) - |> load_pending_values() - |> Enum.map(&{&1.name, &1}) - |> Enum.into(%{}) + fields |> Enum.map(&elem(&1, 1)) |> load_pending_values() |> Map.new(&{&1.name, &1}) Module.delete_attribute(mod, @accumulating_fields_attr) Module.put_attribute(mod, :fields, fields) @@ -252,20 +239,21 @@ defmodule ExtFit.Profile do @fields_table_doc @fields |> Enum.sort_by(fn {_, %{num: num}} -> num end) - |> Enum.map(fn {name, field} -> - [ - "| " <> to_string(name), - field.type.name, - field.num, - field.scale, - field.offset, - field.units, - to_string(field.array) <> " |" - ] - |> Enum.map(&(&1 || "")) - |> Enum.join(" | ") + |> Enum.map_join("\n", fn {name, field} -> + Enum.map_join( + [ + "| " <> to_string(name), + field.type.name, + field.num, + field.scale, + field.offset, + field.units, + to_string(field.array) <> " |" + ], + " | ", + &(&1 || "") + ) end) - |> Enum.join("\n") @moduledoc """ Message: `:#{@name}` identified by num=#{@num} @@ -284,11 +272,11 @@ defmodule ExtFit.Profile do {subfields, opts} = Keyword.pop(opts, :subfields, []) {components, opts} = Keyword.pop(opts, :components, []) {units, opts} = Keyword.pop(opts, :units, []) - units = units |> List.wrap() + units = List.wrap(units) {scale, opts} = Keyword.pop(opts, :scale, []) - scale = scale |> List.wrap() + scale = List.wrap(scale) {offset, opts} = Keyword.pop(opts, :offset, []) - offset = offset |> List.wrap() + offset = List.wrap(offset) {bits, opts} = Keyword.pop(opts, :bits, []) {array, opts} = Keyword.pop(opts, :array, false) {accumulate, opts} = Keyword.pop(opts, :accumulate, []) @@ -298,7 +286,7 @@ defmodule ExtFit.Profile do raise ArgumentError, "unknown option(s) #{inspect(opts)}" end - if !type_struct do + unless type_struct do raise ArgumentError, "the type #{inspect(type)} is unknown" end @@ -310,16 +298,16 @@ defmodule ExtFit.Profile do type_struct = Types.by_name(type) {components, opts} = Map.pop(opts, :components, []) {accumulate, opts} = Map.pop(opts, :accumulate, []) - accumulate = accumulate |> List.wrap() + accumulate = List.wrap(accumulate) {ref_fields, opts} = Map.pop(opts, :ref_fields, []) {units, opts} = Map.pop(opts, :units, []) - units = units |> List.wrap() + units = List.wrap(units) {scale, opts} = Map.pop(opts, :scale, []) - scale = scale |> List.wrap() + scale = List.wrap(scale) {offset, opts} = Map.pop(opts, :offset, []) {bits, opts} = Map.pop(opts, :bits, []) - if !type_struct do + unless type_struct do raise ArgumentError, "the type #{inspect(type)} is unknown" end @@ -338,24 +326,18 @@ defmodule ExtFit.Profile do }} end) - %Types.Subfield{ - name: name, - type: type_struct, - num: num, - components: components, - ref_fields: ref_fields - } - |> set_field_components({scale, offset, units, bits, accumulate}, components) + set_field_components( + %Types.Subfield{name: name, type: type_struct, num: num, components: components, ref_fields: ref_fields}, + {scale, offset, units, bits, accumulate}, + components + ) end) - %Types.Field{ - num: num, - type: type_struct, - name: name, - array: array, - subfields: subfields - } - |> set_field_components({scale, offset, units, bits, accumulate}, components) + set_field_components( + %Types.Field{num: num, type: type_struct, name: name, array: array, subfields: subfields}, + {scale, offset, units, bits, accumulate}, + components + ) end defp set_field_components(field, {scale, offset, units, bits, accumulate}, components) do @@ -394,14 +376,10 @@ defmodule ExtFit.Profile do end defp load_pending_values(fields) do - fields - |> Enum.map(&load_pending_field_values(&1, fields)) + Enum.map(fields, &load_pending_field_values(&1, fields)) end - defp load_pending_field_values( - %{components: components, ref_fields: ref_fields} = field, - fields - ) do + defp load_pending_field_values(%{components: components, ref_fields: ref_fields} = field, fields) do %{ field | components: load_pending_components(components, fields), diff --git a/lib/ext_fit/record.ex b/lib/ext_fit/record.ex index 559cd48..c358caf 100644 --- a/lib/ext_fit/record.ex +++ b/lib/ext_fit/record.ex @@ -1,6 +1,11 @@ defmodule ExtFit.Record do - alias ExtFit.{Field, Types} - alias __MODULE__.{FitHeader, FitCrc, FitData, FitDefinition} + @moduledoc false + alias __MODULE__.FitCrc + alias __MODULE__.FitData + alias __MODULE__.FitDefinition + alias __MODULE__.FitHeader + alias ExtFit.Field + alias ExtFit.Types @type t :: %FitHeader{} | %FitCrc{} | %FitData{} | %FitDefinition{} @@ -65,7 +70,7 @@ defmodule ExtFit.Record do def debug(%FitDefinition{} = record) do {:definition, Map.get(record.mesg_type || %{}, :name), record.local_mesg_num, record.global_mesg_num, - Enum.map(record.field_defs, &Field.name/1) |> Enum.join(",")} + Enum.map_join(record.field_defs, ",", &Field.name/1)} end def debug(%FitData{} = record) do diff --git a/lib/ext_fit/record/fit_crc.ex b/lib/ext_fit/record/fit_crc.ex index 19869de..f1a4cd2 100644 --- a/lib/ext_fit/record/fit_crc.ex +++ b/lib/ext_fit/record/fit_crc.ex @@ -1,4 +1,5 @@ defmodule ExtFit.Record.FitCrc do + @moduledoc false @derive {Inspect, except: ~w(__chunk__)a} defstruct crc: 0, matched: nil, __chunk__: nil diff --git a/lib/ext_fit/record/fit_data.ex b/lib/ext_fit/record/fit_data.ex index c1fb98c..a046b47 100644 --- a/lib/ext_fit/record/fit_data.ex +++ b/lib/ext_fit/record/fit_data.ex @@ -1,4 +1,5 @@ defmodule ExtFit.Record.FitData do + @moduledoc false @header ~w(is_developer_data local_mesg_num time_offset)a @payload ~w(def_mesg fields)a diff --git a/lib/ext_fit/record/fit_definition.ex b/lib/ext_fit/record/fit_definition.ex index 34a1376..49a887d 100644 --- a/lib/ext_fit/record/fit_definition.ex +++ b/lib/ext_fit/record/fit_definition.ex @@ -1,4 +1,5 @@ defmodule ExtFit.Record.FitDefinition do + @moduledoc false @header ~w(is_developer_data local_mesg_num time_offset)a @payload ~w(mesg_type global_mesg_num endian field_defs dev_field_defs)a diff --git a/lib/ext_fit/record/fit_header.ex b/lib/ext_fit/record/fit_header.ex index 9637350..d00ae72 100644 --- a/lib/ext_fit/record/fit_header.ex +++ b/lib/ext_fit/record/fit_header.ex @@ -1,4 +1,5 @@ defmodule ExtFit.Record.FitHeader do + @moduledoc false @derive {Inspect, except: ~w(__chunk__)a} defstruct header_size: 0, type_frame: nil, diff --git a/lib/ext_fit/types.ex b/lib/ext_fit/types.ex index 64918f1..1c64e6c 100644 --- a/lib/ext_fit/types.ex +++ b/lib/ext_fit/types.ex @@ -1,4 +1,5 @@ defmodule ExtFit.Types.BaseType do + @moduledoc false defstruct name: nil, id: nil, size: nil, invalid: nil @type t() :: %__MODULE__{ @@ -21,6 +22,7 @@ defmodule ExtFit.Types.BaseType do end defmodule ExtFit.Types.FieldType do + @moduledoc false @derive {Inspect, except: ~w(values)a} defstruct name: nil, base_type: nil, values: %{} @@ -33,6 +35,7 @@ defmodule ExtFit.Types.FieldType do end defmodule ExtFit.Types.FieldTypeValue do + @moduledoc false defstruct num: nil, name: nil @type t() :: %__MODULE__{ @@ -42,6 +45,7 @@ defmodule ExtFit.Types.FieldTypeValue do end defmodule ExtFit.Types.Field do + @moduledoc false @derive {Inspect, optional: ~w(scale offset units components array subfields)a} defstruct name: nil, type: nil, @@ -67,6 +71,7 @@ defmodule ExtFit.Types.Field do end defmodule ExtFit.Types.Subfield do + @moduledoc false @derive {Inspect, optional: ~w(scale offset units components ref_fields)a} defstruct name: nil, type: nil, @@ -90,6 +95,7 @@ defmodule ExtFit.Types.Subfield do end defmodule ExtFit.Types.DevField do + @moduledoc false defstruct dev_data_index: nil, name: nil, type: nil, num: nil, units: nil, native_field_num: nil @type t() :: %__MODULE__{ @@ -103,6 +109,7 @@ defmodule ExtFit.Types.DevField do end defmodule ExtFit.Types.ReferenceField do + @moduledoc false defstruct name: nil, value: nil, raw_value: nil, num: nil @type t() :: %__MODULE__{ @@ -114,6 +121,7 @@ defmodule ExtFit.Types.ReferenceField do end defmodule ExtFit.Types.ComponentField do + @moduledoc false @derive {Inspect, optional: ~w(scale offset units is_accumulated)a} defstruct name: nil, num: nil, @@ -137,6 +145,7 @@ defmodule ExtFit.Types.ComponentField do end defmodule ExtFit.Types.MessageType do + @moduledoc false defstruct name: nil, num: nil, fields: [] @type t() :: %__MODULE__{ @@ -147,6 +156,7 @@ defmodule ExtFit.Types.MessageType do end defmodule ExtFit.Types.FieldDefinition do + @moduledoc false defstruct field: nil, num: nil, base_type: nil, size: nil @type t() :: %__MODULE__{ @@ -157,6 +167,7 @@ defmodule ExtFit.Types.FieldDefinition do end defmodule ExtFit.Types.DevFieldDefinition do + @moduledoc false defstruct field: nil, dev_data_index: nil, num: nil, size: nil, base_type: nil @type t() :: %__MODULE__{ @@ -169,6 +180,7 @@ defmodule ExtFit.Types.DevFieldDefinition do end defmodule ExtFit.Types.FieldData do + @moduledoc false alias ExtFit.Field defstruct field_def: nil, @@ -203,8 +215,9 @@ defmodule ExtFit.Types.FieldData do end defmodule ExtFit.Types do - alias ExtFit.Types.BaseType + @moduledoc false alias ExtFit.Profile + alias ExtFit.Types.BaseType @base_type_byte %BaseType{ name: :byte, @@ -288,7 +301,7 @@ defmodule ExtFit.Types do } } - @base_type_by_name @base_types |> Enum.map(fn {_, type} -> {type.name, type} end) |> Enum.into(%{}) + @base_type_by_name Map.new(@base_types, fn {_, type} -> {type.name, type} end) # Orders matters here!!! @base_type_num [ diff --git a/mix.exs b/mix.exs index 578d292..d093ed5 100644 --- a/mix.exs +++ b/mix.exs @@ -49,7 +49,7 @@ defmodule ExtFit.MixProject do {:cldr_utils, "~> 2.24"}, {:excoveralls, "~> 0.18.0", only: [:dev, :test], runtime: false}, {:ex_doc, "~> 0.31", only: :dev, runtime: false}, - {:dialyxir, "~> 1.3", only: [:dev], runtime: false}, + {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, {:csv, "~> 3.2", only: [:test]}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:styler, "~> 0.11", only: [:dev, :test], runtime: false} @@ -65,7 +65,8 @@ defmodule ExtFit.MixProject do "format --check-formatted", "deps.unlock --check-unused", "test --warnings-as-errors", - "dialyzer --format short" + "dialyzer --format short", + "credo" ] ] end diff --git a/test/ext_fit/decode_csv_compare_test.exs b/test/ext_fit/decode_csv_compare_test.exs index 2f0ada7..c047077 100644 --- a/test/ext_fit/decode_csv_compare_test.exs +++ b/test/ext_fit/decode_csv_compare_test.exs @@ -1,10 +1,16 @@ defmodule ExtFit.DecodeCsvCompareTest do use ExUnit.Case, async: true + + alias ExtFit.Decode + alias ExtFit.Field + alias ExtFit.Processor + alias ExtFit.Record + alias ExtFit.Record.FitCrc + alias ExtFit.Record.FitHeader + require Logger - alias ExtFit.{Decode, Field, Record, Processor} - alias ExtFit.Record.{FitHeader, FitCrc} - @files Path.join(__DIR__, "../support/files") |> Path.expand() + @files __DIR__ |> Path.join("../support/files") |> Path.expand() @test_focus_file System.get_env("EXT_FIT_FOCUS_FILE") @@ -72,28 +78,30 @@ defmodule ExtFit.DecodeCsvCompareTest do skipped_records = length(records) - length(record_to_compare) # Firt row is headers - fit2csv = get_fit2csv_data(filename) |> Enum.drop(1) + fit2csv = filename |> get_fit2csv_data() |> Enum.drop(1) fit2csv_count = length(fit2csv) cmp_rows = Enum.min([fit2csv_count, 300]) assert length(records) - skipped_records == length(fit2csv) - ext_fit_records = record_to_compare |> Enum.take(cmp_rows) + ext_fit_records = Enum.take(record_to_compare, cmp_rows) fit2csv_records = Enum.take(fit2csv, cmp_rows) - Enum.zip(ext_fit_records, fit2csv_records) + ext_fit_records + |> Enum.zip(fit2csv_records) |> Enum.with_index() |> Enum.map(fn {{record, row}, idx} -> record_values = - field_data_values(record) + record + |> field_data_values() |> Enum.map(fn {name, value} -> {normalize_name(name), normalize_value(value)} end) |> Enum.filter(fn {name, _} -> name not in @dropped_fields end) |> Enum.reduce([], fn {name, value}, acc -> if Enum.find(acc, &(elem(&1, 0) == name)) do Enum.map(acc, fn {^name, found_value} -> - {name, List.flatten([found_value, value]) |> normalize_value()} + {name, [found_value, value] |> List.flatten() |> normalize_value()} elem -> elem @@ -114,7 +122,8 @@ defmodule ExtFit.DecodeCsvCompareTest do name = normalize_name(name) known_record_value = - Enum.find(record_values, &(elem(&1, 0) == name)) + record_values + |> Enum.find(&(elem(&1, 0) == name)) |> case do {^name, value} -> value _ -> :unknown @@ -123,7 +132,8 @@ defmodule ExtFit.DecodeCsvCompareTest do # additonal mapping, sometime CSV has additional values at the end of array # null bytes / invalid values to mark "the end", we don't care about that when reading value = - normalize_fit2csv_value(filename, name, value) + filename + |> normalize_fit2csv_value(name, value) |> case do [^known_record_value | _] -> known_record_value value -> value @@ -152,13 +162,11 @@ defmodule ExtFit.DecodeCsvCompareTest do end defp field_data_values(records) when is_list(records) do - records - |> Enum.map(&field_data_values/1) + Enum.map(records, &field_data_values/1) end defp field_data_values(%{fields: fields}) do - fields - |> Enum.map(&{Field.name(&1), &1.value}) + Enum.map(fields, &{Field.name(&1), &1.value}) end defp field_data_values(%{field_defs: defs, dev_field_defs: dev_field_defs}), @@ -169,7 +177,8 @@ defmodule ExtFit.DecodeCsvCompareTest do defp get_fit2csv_data(filename) do filename = String.replace_suffix(filename, ".fit", ".csv") - Path.join(@files, filename) + @files + |> Path.join(filename) |> File.stream!() |> CSV.decode!() |> Enum.to_list() @@ -199,32 +208,33 @@ defmodule ExtFit.DecodeCsvCompareTest do def normalize_value(%DateTime{} = dt), do: dt |> DateTime.truncate(:second) |> DateTime.to_iso8601() |> normalize_value() - def normalize_value(%NaiveDateTime{} = ndt), - do: ndt |> DateTime.from_naive!("Etc/UTC") |> normalize_value() + def normalize_value(%NaiveDateTime{} = ndt), do: ndt |> DateTime.from_naive!("Etc/UTC") |> normalize_value() def normalize_value(nil), do: "" def normalize_value(v) when is_bitstring(v) do - v = v |> String.downcase() - - try do - v - |> String.to_float() - |> Decimal.from_float() - |> Decimal.round(2) - |> Decimal.to_string() - rescue - ArgumentError -> - try do - v - |> String.to_integer() - |> Decimal.round(2) - |> Decimal.to_string() - rescue - ArgumentError -> v - end - end - |> case do + v = String.downcase(v) + + try_result = + try do + v + |> String.to_float() + |> Decimal.from_float() + |> Decimal.round(2) + |> Decimal.to_string() + rescue + ArgumentError -> + try do + v + |> String.to_integer() + |> Decimal.round(2) + |> Decimal.to_string() + rescue + ArgumentError -> v + end + end + + case try_result do "0.00" -> "" "0" -> "0" "65535.00" -> "" diff --git a/test/ext_fit/decode_test.exs b/test/ext_fit/decode_test.exs index f237f71..0215fd7 100644 --- a/test/ext_fit/decode_test.exs +++ b/test/ext_fit/decode_test.exs @@ -1,12 +1,20 @@ defmodule ExtFit.DecodeTest do use ExUnit.Case, async: true - doctest ExtFit.Decode + + alias ExtFit.Chunk + alias ExtFit.Decode + alias ExtFit.Field + alias ExtFit.Record + alias ExtFit.Record.FitCrc + alias ExtFit.Record.FitData + alias ExtFit.Record.FitDefinition + alias ExtFit.Record.FitHeader require Logger - alias ExtFit.{Decode, Field, Record, Chunk} - alias ExtFit.Record.{FitHeader, FitCrc, FitDefinition, FitData} - @files Path.join(__DIR__, "../support/files") |> Path.expand() + doctest ExtFit.Decode + + @files __DIR__ |> Path.join("../support/files") |> Path.expand() test "reads Activity.fit file" do filename = "Activity.fit" @@ -23,7 +31,7 @@ defmodule ExtFit.DecodeTest do type_frame: ".FIT" } - assert List.last(records) == %FitCrc{crc: 41429, __chunk__: %Chunk{index: 33, offset: 769, size: 2}} + assert List.last(records) == %FitCrc{crc: 41_429, __chunk__: %Chunk{index: 33, offset: 769, size: 2}} end test "reads Settings.fit file" do @@ -53,7 +61,7 @@ defmodule ExtFit.DecodeTest do assert header == %FitHeader{ body_size: 162, - crc: %FitCrc{crc: 53438, matched: nil}, + crc: %FitCrc{crc: 53_438, matched: nil}, header_size: 14, profile_ver: 1640, proto_ver: 32, @@ -72,7 +80,7 @@ defmodule ExtFit.DecodeTest do assert header == %FitHeader{ body_size: 715_710, - crc: %FitCrc{crc: 23070, matched: nil}, + crc: %FitCrc{crc: 23_070, matched: nil}, header_size: 14, profile_ver: 2027, proto_ver: 32, @@ -81,7 +89,7 @@ defmodule ExtFit.DecodeTest do } session = Enum.find(data_records, &(Record.name(&1) == :session && &1.__struct__ == FitData)) - avg_speed = session.fields |> Enum.find(&(Field.name(&1) == :avg_speed)) + avg_speed = Enum.find(session.fields, &(Field.name(&1) == :avg_speed)) assert avg_speed.value == 5.86 end @@ -106,9 +114,10 @@ defmodule ExtFit.DecodeTest do compressed_speed_distance = data_records |> Enum.drop(26) |> Enum.take(3) values = - Enum.map(compressed_speed_distance, fn + compressed_speed_distance + |> Enum.map(fn %FitData{} = record -> - record.fields |> Enum.filter(&(Field.name(&1) in ~w(speed distance cadence)a)) + Enum.filter(record.fields, &(Field.name(&1) in ~w(speed distance cadence)a)) _ -> nil @@ -130,9 +139,9 @@ defmodule ExtFit.DecodeTest do assert length(records) == 6250 assert hd(records) == %FitHeader{ - body_size: 58949, + body_size: 58_949, __chunk__: %ExtFit.Chunk{index: 0, offset: 0, size: 14}, - crc: %ExtFit.Record.FitCrc{crc: 46851, matched: nil}, + crc: %ExtFit.Record.FitCrc{crc: 46_851, matched: nil}, header_size: 14, profile_ver: 2032, proto_ver: 16, @@ -140,7 +149,8 @@ defmodule ExtFit.DecodeTest do } field_data_values = - Enum.at(records, -254) + records + |> Enum.at(-254) |> Map.fetch!(:fields) |> Enum.map(&{Field.name(&1), &1.value}) |> Enum.filter(&(elem(&1, 0) == :event_timestamp)) @@ -165,7 +175,7 @@ defmodule ExtFit.DecodeTest do |> read_file() |> Decode.decode!() - assert length(records) == 11327 + assert length(records) == 11_327 end test "reads developer types - developer-types-sample.fit" do @@ -207,7 +217,7 @@ defmodule ExtFit.DecodeTest do |> read_file() |> Decode.decode!() - assert length(records) == 16478 + assert length(records) == 16_478 %FitData{fields: fields} = Enum.at(records, 606) @@ -217,7 +227,7 @@ defmodule ExtFit.DecodeTest do |> Enum.sort_by(&elem(&1, 0)) assert result == [ - {:accumulated_power, 57288}, + {:accumulated_power, 57_288}, {:activity_type, 1}, {:cadence, 92}, {:cycle_length16, 0.0}, @@ -253,7 +263,7 @@ defmodule ExtFit.DecodeTest do |> read_file() |> Decode.decode!() - inital_chunks = Enum.take(records, 10) |> Enum.map(& &1.__chunk__) + inital_chunks = records |> Enum.take(10) |> Enum.map(& &1.__chunk__) %FitCrc{__chunk__: last_chunk} = List.last(records) assert inital_chunks == [ @@ -269,7 +279,7 @@ defmodule ExtFit.DecodeTest do %Chunk{index: 9, offset: 186, size: 21} ] - assert last_chunk == %Chunk{index: 16477, offset: 409_415, size: 2} + assert last_chunk == %Chunk{index: 16_477, offset: 409_415, size: 2} end describe "options" do @@ -279,13 +289,15 @@ defmodule ExtFit.DecodeTest do {:ok, records} = Decode.decode(file, expand_components: false) event_timestamp_values = - Enum.at(records, -254) + records + |> Enum.at(-254) |> Map.fetch!(:fields) |> Enum.map(&{Field.name(&1), &1.value}) |> Enum.filter(&(elem(&1, 0) == :event_timestamp)) event_timestamp_12_values = - Enum.at(records, -254) + records + |> Enum.at(-254) |> Map.fetch!(:fields) |> Enum.map(&{Field.name(&1), &1.value}) |> Enum.filter(&(elem(&1, 0) == :event_timestamp_12)) @@ -308,13 +320,11 @@ defmodule ExtFit.DecodeTest do end defp field_data_values(records) when is_list(records) do - records - |> Enum.map(&field_data_values/1) + Enum.map(records, &field_data_values/1) end defp field_data_values(%{fields: fields}) do - fields - |> Enum.map(&{Field.name(&1), &1.value}) + Enum.map(fields, &{Field.name(&1), &1.value}) end defp field_data_values(%{field_defs: defs, dev_field_defs: dev_field_defs}),