if Code.ensure_loaded?(SweetXml) do defmodule Mix.Tasks.Zwave.Gen.XmlMappings do @moduledoc """ Generates a module that provides mappings based on the data in `ZWave_custom_cmd_classes.xml`. mix zwave.gen.xml_mappings ### Flags * `--input ` - reads the xml from the provided path * `--dry-run` - print the generated module out to the console * `--output ` - writes the generated module at the provided path """ use Mix.Task import SweetXml @shortdoc "Generates a module that provides mappings based on the data in `ZWave_custom_cmd_classes.xml`" @switches [dry_run: :boolean] @skip_command_classes [:zwave_cmd_class, :zwave_cmd_class_lr] @command_class_name_overrides %{ 0x4F => :zip_6lowpan, 0x59 => :association_group_info, 0x96 => :av_content_renderer_status } def run(args) do {opts, _, _} = OptionParser.parse(args, switches: @switches) input_path = Keyword.get(opts, :input, "resources/ZWave_custom_cmd_classes.xml") doc = input_path |> File.read!() |> parse() command_class_mappings = build_macro(:command_class_mappings, command_class_mappings(doc)) basic_device_class_mappings = build_macro(:basic_device_class_mappings, basic_device_class_mappings(doc)) generic_device_class_mappings = build_macro(:generic_device_class_mappings, generic_device_class_mappings(doc)) specific_device_class_mappings = build_macro(:specific_device_class_mappings, specific_device_class_mappings(doc)) module = quote do defmodule Grizzly.ZWave.GeneratedMappings do @moduledoc false unquote(command_class_mappings) unquote(basic_device_class_mappings) unquote(generic_device_class_mappings) unquote(specific_device_class_mappings) end end module_code = module |> Macro.to_string() |> Code.format_string!() module_code = """ # !! THIS FILE IS GENERATED BY `mix zwave.gen.xml` # !! DO NOT EDIT DIRECTLY #{module_code} """ if opts[:dry_run] do Mix.shell().info(module_code) else output_path = Keyword.get(opts, :output, "lib/grizzly/zwave/generated_mappings.ex") Mix.Generator.create_file(output_path, module_code) end end defp build_macro(name, value) do quote do def unquote(name)(), do: unquote(value) end end defp command_class_mappings(doc) do doc |> xpath(~x"//zw_classes/cmd_class"l, key: ~x"@key"s, name: ~x"@name"s) |> Enum.map(key_name_mapper("COMMAND_CLASS_", @command_class_name_overrides)) |> Enum.uniq_by(fn {key, _} -> key end) |> Enum.reject(&(elem(&1, 1) in @skip_command_classes)) |> Enum.sort() end defp basic_device_class_mappings(doc) do doc |> xpath(~x"//zw_classes/bas_dev"l, key: ~x"@key"s, name: ~x"@name"s) |> Enum.map(key_name_mapper("BASIC_TYPE_")) |> Enum.sort() end defp generic_device_class_mappings(doc) do doc |> xpath(~x"//zw_classes/gen_dev"l, key: ~x"@key"s, name: ~x"@name"s) |> Enum.map(key_name_mapper("GENERIC_TYPE_")) |> Enum.sort() end defp specific_device_class_mappings(doc) do doc |> xpath(~x"//zw_classes/gen_dev"l, generic_type: ~x"@name"s, specific_types: [~x"./spec_dev"l, key: ~x"@key"s, name: ~x"@name"s] ) |> Enum.flat_map(fn %{generic_type: generic_type, specific_types: specific_types} -> Enum.map(specific_types, fn %{name: name, key: key} -> generic_type = parse_name(generic_type, "GENERIC_TYPE_") specific_type = parse_name(name, "SPECIFIC_TYPE_") key = parse_hex_string(key) Macro.escape({generic_type, key, specific_type}) end) end) |> Enum.sort() end defp key_name_mapper(name_prefix, name_overrides \\ %{}) do fn %{name: name, key: key} -> key = parse_hex_string(key) name = Map.get(name_overrides, key, parse_name(name, name_prefix)) {key, name} end end defp parse_hex_string(string) do string |> String.replace_leading("0x", "") |> String.to_integer(16) end defp parse_name(name, prefix) do name |> String.replace(prefix, "") |> Macro.underscore() |> String.to_atom() end end end