From 68f0166c4746f2edaed91ec5689e0339571c3bd2 Mon Sep 17 00:00:00 2001 From: Billal GHILAS Date: Mon, 14 Apr 2025 11:42:11 +0100 Subject: [PATCH 1/2] Use job configuration schema when creating job --- lib/recording/create_recording_job.ex | 11 +- lib/recording/schemas/job_configuration.ex | 166 +++++++++++++++++++ lib/recording/schemas/recording_job.ex | 111 +------------ test/recording/create_recording_job_test.exs | 12 +- 4 files changed, 184 insertions(+), 116 deletions(-) create mode 100644 lib/recording/schemas/job_configuration.ex diff --git a/lib/recording/create_recording_job.ex b/lib/recording/create_recording_job.ex index 9614d8b..4a2addf 100644 --- a/lib/recording/create_recording_job.ex +++ b/lib/recording/create_recording_job.ex @@ -1,22 +1,21 @@ defmodule Onvif.Recording.CreateRecordingJob do import SweetXml import XmlBuilder + require Logger + alias Onvif.Recording.Schemas.JobConfiguration + def soap_action, do: "http://www.onvif.org/ver10/recording/wsdl/CreateRecordingJob" def request(device, args) do Onvif.Recording.request(device, args, __MODULE__) end - def request_body(recording_token, priority \\ "0", mode \\ "Active") do + def request_body(%JobConfiguration{} = config) do element(:"s:Body", [ element(:"trc:CreateRecordingJob", [ - element(:"trc:JobConfiguration", [ - element(:"tt:RecordingToken", recording_token), - element(:"tt:Mode", mode), - element(:"tt:Priority", priority) - ]) + element(:"trc:JobConfiguration", JobConfiguration.to_xml(config)) ]) ]) end diff --git a/lib/recording/schemas/job_configuration.ex b/lib/recording/schemas/job_configuration.ex new file mode 100644 index 0000000..a8e3679 --- /dev/null +++ b/lib/recording/schemas/job_configuration.ex @@ -0,0 +1,166 @@ +defmodule Onvif.Recording.Schemas.JobConfiguration do + @moduledoc """ + Schema describing a recording job configuration. + """ + + use Ecto.Schema + + import Ecto.Changeset + import SweetXml + import XmlBuilder + + @primary_key false + @derive Jason.Encoder + embedded_schema do + field(:recording_token, :string) + field(:mode, Ecto.Enum, values: [idle: "Idle", active: "Active"]) + field(:priority, :integer) + + embeds_one :source, Source, primary_key: false, on_replace: :update do + @derive Jason.Encoder + field(:auto_create_receiver, :boolean) + + embeds_one :source_token, SourceToken, primary_key: false, on_replace: :update do + @derive Jason.Encoder + field(:token, :string) + end + + embeds_many :tracks, Tracks, primary_key: false, on_replace: :delete do + @derive Jason.Encoder + field(:source_tag, :string) + field(:destination, :string) + end + end + end + + def parse([]), do: nil + def parse(nil), do: nil + + def parse(doc) do + xmap( + doc, + recording_token: ~x"./tt:RecordingToken/text()"so, + mode: ~x"./tt:Mode/text()"so, + priority: ~x"./tt:Priority/text()"so, + source: ~x"./tt:Source"eo |> transform_by(&parse_source/1) + ) + end + + def to_xml(%__MODULE__{} = job_configuration) do + field_to_xml([], "tt:RecordingToken", job_configuration.recording_token) + |> field_to_xml( + "tt:Mode", + Keyword.get(Ecto.Enum.mappings(__MODULE__, :mode), job_configuration.mode) + ) + |> field_to_xml("tt:Priority", job_configuration.priority) + |> source_to_xml(job_configuration.source) + end + + def to_struct(parsed) do + %__MODULE__{} + |> changeset(parsed) + |> apply_action(:validate) + end + + def changeset(job_configuration, attrs) do + job_configuration + |> cast(attrs, [:recording_token, :mode, :priority]) + |> cast_embed(:source, with: &source_changeset/2) + end + + defp parse_source([]), do: nil + defp parse_source(nil), do: nil + + defp parse_source(doc) do + xmap( + doc, + source_token: ~x"./tt:SourceToken"eo |> transform_by(&parse_source_token/1), + auto_create_receiver: ~x"./tt:AutoCreateReceiver/text()"so, + tracks: ~x"./tt:Tracks"elo |> transform_by(&parse_track/1) + ) + end + + defp parse_source_token([]), do: nil + defp parse_source_token(nil), do: nil + + defp parse_source_token(doc) do + xmap( + doc, + token: ~x"./tt:Token/text()"so + ) + end + + defp parse_track([]), do: nil + defp parse_track(nil), do: nil + + defp parse_track(docs) do + Enum.map(docs, fn doc -> + xmap( + doc, + source_tag: ~x"./tt:SourceTag/text()"so, + destination: ~x"./tt:Destination/text()"so + ) + end) + end + + defp source_changeset(schema, params) do + schema + |> cast(params, [:auto_create_receiver]) + |> cast_embed(:source_token, with: &source_token_changeset/2) + |> cast_embed(:tracks, with: &track_changeset/2) + end + + defp source_token_changeset(schema, params) do + schema + |> cast(params, [:token]) + |> validate_required([:token]) + end + + defp track_changeset(schema, params) do + schema + |> cast(params, [:source_tag, :destination]) + end + + defp source_to_xml(builder, nil), do: builder + + defp source_to_xml(builder, source) do + source = + element( + "tt:Source", + field_to_xml([], "tt:AutoCreateReceiver", source.auto_create_receiver) + |> source_token_to_xml(source.source_token) + |> tracks_to_xml(source.tracks) + ) + + [source | builder] + end + + defp source_token_to_xml(builder, nil), do: builder + + defp source_token_to_xml(builder, source_token) do + source_token = element("tt:SourceToken", field_to_xml([], "tt:Token", source_token.token)) + [source_token | builder] + end + + defp tracks_to_xml(builder, nil), do: builder + defp tracks_to_xml(builder, []), do: builder + + defp tracks_to_xml(builder, tracks) do + tracks = + Enum.map(tracks, fn track -> + element( + "tt:Tracks", + field_to_xml([], "tt:SourceTag", track.source_tag) + |> field_to_xml("tt:Destination", track.destination) + ) + end) + + [tracks | builder] + end + + defp field_to_xml(builder, _field, nil), do: builder + + defp field_to_xml(builder, key, value) do + [element(key, value) | builder] + end +end diff --git a/lib/recording/schemas/recording_job.ex b/lib/recording/schemas/recording_job.ex index a92578a..ef7136c 100644 --- a/lib/recording/schemas/recording_job.ex +++ b/lib/recording/schemas/recording_job.ex @@ -7,6 +7,8 @@ defmodule Onvif.Recording.Schemas.RecordingJob do import Ecto.Changeset import SweetXml + alias Onvif.Recording.Schemas.JobConfiguration + @required [:job_token] @optional [] @@ -17,28 +19,7 @@ defmodule Onvif.Recording.Schemas.RecordingJob do embedded_schema do field(:job_token, :string) - embeds_one :job_configuration, JobConfiguration, primary_key: false, on_replace: :update do - @derive Jason.Encoder - field(:recording_token, :string) - field(:mode, :string) - field(:priority, :string) - - embeds_one :source, Source, primary_key: false, on_replace: :update do - @derive Jason.Encoder - field(:auto_create_receiver, :boolean) - - embeds_one :source_token, SourceToken, primary_key: false, on_replace: :update do - @derive Jason.Encoder - field(:token, :string) - end - - embeds_many :tracks, Tracks, primary_key: false, on_replace: :delete do - @derive Jason.Encoder - field(:source_tag, :string) - field(:destination, :string) - end - end - end + embeds_one(:job_configuration, JobConfiguration) end def parse(nil), do: nil @@ -48,72 +29,17 @@ defmodule Onvif.Recording.Schemas.RecordingJob do xmap( doc, job_token: ~x"./tt:JobToken/text()"so, - job_configuration: ~x"./tt:JobConfiguration"eo |> transform_by(&parse_job_configuration/1) + job_configuration: ~x"./tt:JobConfiguration"eo |> transform_by(&JobConfiguration.parse/1) ) end - def parse_job_configuration([]), do: nil - def parse_job_configuration(nil), do: nil - - def parse_job_configuration(doc) do - xmap( - doc, - recording_token: ~x"./tt:RecordingToken/text()"so, - mode: ~x"./tt:Mode/text()"so, - priority: ~x"./tt:Priority/text()"so, - source: ~x"./tt:Source"eo |> transform_by(&parse_source/1) - ) - end - - def parse_source([]), do: nil - def parse_source(nil), do: nil - - def parse_source(doc) do - xmap( - doc, - source_token: ~x"./tt:SourceToken"eo |> transform_by(&parse_source_token/1), - auto_create_receiver: ~x"./tt:AutoCreateReceiver/text()"so, - tracks: ~x"./tt:Tracks"elo |> transform_by(&parse_track/1) - ) - end - - def parse_source_token([]), do: nil - def parse_source_token(nil), do: nil - - def parse_source_token(doc) do - xmap( - doc, - token: ~x"./tt:Token/text()"so - ) - end - - def parse_track([]), do: nil - def parse_track(nil), do: nil - - def parse_track(docs) do - Enum.map(docs, fn doc -> - xmap( - doc, - source_tag: ~x"./tt:SourceTag/text()"so, - destination: ~x"./tt:Destination/text()"so - ) - end) - end - def to_struct(parsed) do %__MODULE__{} |> changeset(parsed) |> apply_action(:validate) end - @spec to_json(__MODULE__.t()) :: - {:error, - %{ - :__exception__ => any, - :__struct__ => Jason.EncodeError | Protocol.UndefinedError, - optional(atom) => any - }} - | {:ok, binary} + @spec to_json(__MODULE__.t()) :: {:error, Jason.EncodeError.t() | Exception.t()} | {:ok, binary} def to_json(%__MODULE__{} = schema) do Jason.encode(schema) end @@ -122,31 +48,6 @@ defmodule Onvif.Recording.Schemas.RecordingJob do module |> cast(attrs, @required ++ @optional) |> validate_required(@required) - |> cast_embed(:job_configuration, with: &job_configuration_changeset/2) - end - - def job_configuration_changeset(schema, params) do - schema - |> cast(params, [:recording_token, :mode, :priority]) - |> validate_required([:recording_token, :mode, :priority]) - |> cast_embed(:source, with: &source_changeset/2) - end - - def source_changeset(schema, params) do - schema - |> cast(params, [:auto_create_receiver]) - |> cast_embed(:source_token, with: &source_token_changeset/2) - |> cast_embed(:tracks, with: &track_changeset/2) - end - - def source_token_changeset(schema, params) do - schema - |> cast(params, [:token]) - |> validate_required([:token]) - end - - def track_changeset(schema, params) do - schema - |> cast(params, [:source_tag, :destination]) + |> cast_embed(:job_configuration, with: &JobConfiguration.changeset/2) end end diff --git a/test/recording/create_recording_job_test.exs b/test/recording/create_recording_job_test.exs index 465bd32..499d6d6 100644 --- a/test/recording/create_recording_job_test.exs +++ b/test/recording/create_recording_job_test.exs @@ -3,6 +3,8 @@ defmodule Onvif.Recording.CreateRecordingJobTest do @moduletag capture_log: true + alias Onvif.Recording.Schemas.JobConfiguration + describe "CreateRecordingJob/2" do test "create a recording" do xml_response = File.read!("test/recording/fixture/create_recording_job_success.xml") @@ -14,11 +16,11 @@ defmodule Onvif.Recording.CreateRecordingJobTest do end) {:ok, response} = - Onvif.Recording.CreateRecordingJob.request(device, [ - "SD_DISK_20241120_211729_9C896594", - "9", - "Active" - ]) + Onvif.Recording.CreateRecordingJob.request(device, %JobConfiguration{ + recording_token: "SD_DISK_20241120_211729_9C896594", + priority: "9", + mode: "Active" + }) assert response == "SD_DISK_20241120_211729_9C896594" end From 9dd3fc7bd2bf798a0f3e2515919472e67dfd28af Mon Sep 17 00:00:00 2001 From: Billal GHILAS Date: Mon, 14 Apr 2025 11:45:16 +0100 Subject: [PATCH 2/2] fix test --- test/recording/create_recording_job_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/recording/create_recording_job_test.exs b/test/recording/create_recording_job_test.exs index 499d6d6..5cc7872 100644 --- a/test/recording/create_recording_job_test.exs +++ b/test/recording/create_recording_job_test.exs @@ -18,8 +18,8 @@ defmodule Onvif.Recording.CreateRecordingJobTest do {:ok, response} = Onvif.Recording.CreateRecordingJob.request(device, %JobConfiguration{ recording_token: "SD_DISK_20241120_211729_9C896594", - priority: "9", - mode: "Active" + priority: 9, + mode: :active }) assert response == "SD_DISK_20241120_211729_9C896594"