diff --git a/README.md b/README.md index 1ab9316..03dfe97 100644 --- a/README.md +++ b/README.md @@ -59,9 +59,11 @@ some helpers functions are provided : ## Example usage : email event request generation ```elixir +now = DateTime.utc_now() + Calibex.request( - dtstart: Timex.now(), - dtend: Timex.shift(Timex.now(), hours: 1), + dtstart: now, + dtend: DateTime.add(now, 3_600, :second), summary: "Mon évènement", organizer: "arnaud.wetzel@example.com", attendee: "jeanpierre@yahoo.fr", diff --git a/config/config.exs b/config/config.exs index 9f1e134..5255cb8 100644 --- a/config/config.exs +++ b/config/config.exs @@ -28,3 +28,4 @@ import Config # here (which is why it is important to import them last). # # import_config "#{Mix.env}.exs" +import_config "#{config_env()}.exs" diff --git a/config/dev.exs b/config/dev.exs new file mode 100644 index 0000000..becde76 --- /dev/null +++ b/config/dev.exs @@ -0,0 +1 @@ +import Config diff --git a/config/prod.exs b/config/prod.exs new file mode 100644 index 0000000..becde76 --- /dev/null +++ b/config/prod.exs @@ -0,0 +1 @@ +import Config diff --git a/config/test.exs b/config/test.exs new file mode 100644 index 0000000..8a05082 --- /dev/null +++ b/config/test.exs @@ -0,0 +1,3 @@ +import Config + +config :elixir, :time_zone_database, Tz.TimeZoneDatabase diff --git a/lib/calibex.ex b/lib/calibex.ex index 8dd0efe..25712e6 100644 --- a/lib/calibex.ex +++ b/lib/calibex.ex @@ -2,22 +2,41 @@ defmodule Calibex do @moduledoc """ Calibex allows you to handle ICal file format. - In the same way as the `mailibex` library, Calibex allows bijective coding/decoding : - making it possible to modify an ical and to keep all fields and struct of the initial ical. + In the same way as the [`mailibex`](https://github.com/kbrw/mailibex) library, Calibex + allows bijective coding/decoding : making it possible to modify an ical and to keep all + fields and struct of the initial ical. - The ICal elixir term is exactly a representation of the ICal file format : for instance : + The ICal elixir term is exactly a representation of the ICal file format. - [vcalendar: [[ + for instance : + + ``` + [ + vcalendar: [ + [ prodid: "-//Google Inc//Google Calendar 70.9054//EN", version: "2.0", - calscale: "GREGORIAN", - vevent: [[ - dtstart: %DateTime{}, - dtend: %DateTime{}, - organizer: [cn: "My Name",value: "mailto:me@example.com"], - attendee: [cutype: "INDIVIDUAL",role: "REQ-PARTICIPANT",partstat: "NEEDS-ACTION",rsvp: true, cn: "Moi", - "x-num-guests": 0, value: "mailto:me@example.com"], - ]]]]] + calscale: "GREGORIAN", + vevent: [ + [ + dtstart: %DateTime{}, + dtend: %DateTime{}, + organizer: [cn: "My Name", value: "mailto:me@example.com"], + attendee: [ + cutype: "INDIVIDUAL", + role: "REQ-PARTICIPANT", + partstat: "NEEDS-ACTION", + rsvp: true, + cn: "Moi", + "x-num-guests": 0, + value: "mailto:me@example.com" + ] + ] + ] + ] + ] + ] + ``` `encode/1` and `decode/1` parse and format an ICal from this terms : see functions doc to find encoding rules. @@ -38,9 +57,17 @@ defmodule Calibex do ## Example usage : email event request generation ``` - Calibex.request(dtstart: Timex.now, dtend: Timex.shift(Timex.now,hours: 1), summary: "Mon évènement", - organizer: "arnaud.wetzel@example.com", attendee: "jeanpierre@yahoo.fr", attendee: "jean@ya.fr") - |> Calibex.encode + now = DateTime.utc_now() + + Calibex.request( + dtstart: now, + dtend: DateTime.add(now, 3_600, :second), + summary: "Mon évènement", + organizer: "arnaud.wetzel@example.com", + attendee: "jeanpierre@yahoo.fr", + attendee: "jean@ya.fr" + ) + |> Calibex.encode() ``` """ @@ -74,8 +101,10 @@ defmodule Calibex do """ defdelegate new(event,fill_attrs), to: Calibex.Helper - @doc "see `new/2`, default fill_attrs are - `[:prodid, :version, :calscale, :organizer, :attendee, :cutype, :role, :partstat, :rsvp, :x_num_guests]`" + @doc """ + see `new/2`, default fill_attrs are + `[:prodid, :version, :calscale, :organizer, :attendee, :cutype, :role, :partstat, :rsvp, :x_num_guests]` + """ defdelegate new(event), to: Calibex.Helper @doc """ @@ -116,7 +145,6 @@ defmodule Calibex do standard rsvp enabled attendee, waiting for event acceptance - `:organizer` TRANSFORM an email string into a `[cn: email,value: "mailto:"<>email]` props value. - `:attendee` TRANSFORM an email string into a `[cn: email,value: "mailto:"<>email]` props value. - """ defdelegate all_fill_attrs(), to: Calibex.Helper end diff --git a/lib/codec.ex b/lib/codec.ex index edba051..a774cb8 100644 --- a/lib/codec.ex +++ b/lib/codec.ex @@ -13,9 +13,17 @@ defmodule Calibex.Codec do "#{encode_key pk}=#{encode_value pv}" end) |> Enum.join(";")}:#{encode_value v[:value]}" end - def encode_value({k,v}), do: "#{encode_key k}:#{encode_value v}" # encode standard key value - def encode_value(%DateTime{}=dt), do: %{dt|microsecond: {0,0}} |> Timex.to_datetime("UTC") |> Timex.format!("{ISO:Basic:Z}") - def encode_value(atom) when is_atom(atom), do: atom |> to_string() |> String.upcase + + # encode standard key value + def encode_value({k, v}), do: "#{encode_key(k)}:#{encode_value(v)}" + + def encode_value(%DateTime{} = dt) do + dt + |> DateTime.shift_zone!("Etc/UTC") + |> Calendar.strftime("%Y%m%dT%H%M%SZ") + end + + def encode_value(atom) when is_atom(atom), do: atom |> to_string() |> String.upcase() def encode_value(other), do: other def encode_key(k) do @@ -33,10 +41,10 @@ defmodule Calibex.Codec do def decode(bin), do: bin |> decode_lines |> decode_blocks def decode_lines(bin) do # split by unfolded line - bin |> String.splitter(["\r\n","\n"]) |> Enum.flat_map_reduce(nil,fn + bin |> String.splitter(["\r\n","\n"]) |> Enum.flat_map_reduce(nil,fn " "<>rest,acc-> {[],acc<>rest} line,prevline-> {prevline && [String.replace(prevline,"\\n","\n")] || [],line} - end) |> elem(0) + end) |> elem(0) end def decode_blocks([]), do: [] def decode_blocks(["BEGIN:"<>binkey|rest]) do # decode each block as a list @@ -53,14 +61,14 @@ defmodule Calibex.Codec do [keyprops,val] = String.split(prop,":",parts: 2) case String.split(keyprops,";") do [key]-> {decode_key(key),val} - [key|props]-> + [key|props]-> props = props |> Enum.map(fn prop-> [k,v] = String.split(prop,"=") {decode_key(k),v} end) - {decode_key(key),[{:value,val}|props]} + {decode_key(key),[{:value,val}|props]} end end - def decode_key(bin), do: + def decode_key(bin), do: bin |> String.replace("-","_") |> String.downcase |> String.to_atom end diff --git a/lib/helpers.ex b/lib/helpers.ex index 860e9c3..711f06e 100644 --- a/lib/helpers.ex +++ b/lib/helpers.ex @@ -42,10 +42,10 @@ defmodule Calibex.Helper do def augment(_,val,_vals), do: val def default(:uid,vals), do: :crypto.hash(:sha,:erlang.term_to_binary(vals)) |> Base.encode16(case: :lower) - def default(:last_modified,_vals), do: Timex.now + def default(:last_modified,_vals), do: DateTime.utc_now() def default(:sequence,_vals), do: 0 - def default(:dtstamp,_vals), do: Timex.now - def default(:created,_vals), do: Timex.now + def default(:dtstamp,_vals), do: DateTime.utc_now() + def default(:created,_vals), do: DateTime.utc_now() def default(:status,_vals), do: :confirmed def default(:cutype,_vals), do: "INDIVIDUAL" def default(:role,_vals), do: "REQ-PARTICIPANT" diff --git a/mix.exs b/mix.exs index 649604d..3ccadd5 100644 --- a/mix.exs +++ b/mix.exs @@ -13,12 +13,13 @@ defmodule Calibex.Mixfile do end def application do - [applications: [:timex]] + [extra_applications: [:crypto]] end - defp deps do - [{:timex, "~> 3.1"}, - {:ex_doc, ">= 0.0.0", only: :dev}] + [ + {:ex_doc, ">= 0.0.0", only: :dev}, + {:tz, "~> 0.26.5", only: :test} + ] end defp package do diff --git a/mix.lock b/mix.lock index 28ff083..aa08f99 100644 --- a/mix.lock +++ b/mix.lock @@ -1,12 +1,10 @@ -%{"certifi": {:hex, :certifi, "1.1.0", "c9b71a547016c2528a590ccfc28de786c7edb74aafa17446b84f54e04efc00ee", [:rebar3], []}, - "combine": {:hex, :combine, "0.9.6", "8d1034a127d4cbf6924c8a5010d3534d958085575fa4d9b878f200d79ac78335", [:mix], []}, - "earmark": {:hex, :earmark, "1.2.2", "f718159d6b65068e8daeef709ccddae5f7fdc770707d82e7d126f584cd925b74", [:mix], []}, - "ex_doc": {:hex, :ex_doc, "0.15.1", "d5f9d588fd802152516fccfdb96d6073753f77314fcfee892b15b6724ca0d596", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}]}, - "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [:mix], []}, - "hackney": {:hex, :hackney, "1.8.0", "8388a22f4e7eb04d171f2cf0285b217410f266d6c13a4c397a6c22ab823a486c", [:rebar3], [{:certifi, "1.1.0", [hex: :certifi, optional: false]}, {:idna, "4.0.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]}, - "idna": {:hex, :idna, "4.0.0", "10aaa9f79d0b12cf0def53038547855b91144f1bfcc0ec73494f38bb7b9c4961", [:rebar3], []}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, - "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []}, - "timex": {:hex, :timex, "3.1.13", "48b33162e3ec33e9a08fb5f98e3f3c19c3e328dded3156096c1969b77d33eef0", [:mix], [{:combine, "~> 0.7", [hex: :combine, optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, optional: false]}]}, - "tzdata": {:hex, :tzdata, "0.5.12", "1c17b68692c6ba5b6ab15db3d64cc8baa0f182043d5ae9d4b6d35d70af76f67b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, optional: false]}]}} +%{ + "earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, + "ex_doc": {:hex, :ex_doc, "0.33.0", "690562b153153c7e4d455dc21dab86e445f66ceba718defe64b0ef6f0bd83ba0", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "3f69adc28274cb51be37d09b03e4565232862a4b10288a3894587b0131412124"}, + "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "tz": {:hex, :tz, "0.26.5", "bfe8efa345670f90351c5c31d22455d0307c5d9895fbdede7deeb215a7b60dbe", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:mint, "~> 1.5", [hex: :mint, repo: "hexpm", optional: true]}], "hexpm", "c4f9392d710582c7108b6b8c635f4981120ec4b2072adbd242290fc842338183"}, +} diff --git a/test/calibex_test.exs b/test/calibex_test.exs index a30321e..8d7cc97 100644 --- a/test/calibex_test.exs +++ b/test/calibex_test.exs @@ -2,35 +2,59 @@ defmodule CalibexTest do use ExUnit.Case test "ical encoding" do - res = Calibex.encode(vcalendar: [[ - prodid: "-//Google Inc//Google Calendar 70.9054//EN", - version: "2.0", - calscale: "GREGORIAN", - method: "REQUEST", - vevent: [[ - dtstart: Timex.to_datetime({{2017,5,14},{9,0,0}},"Europe/Paris"), - dtend: Timex.to_datetime({{2017,5,14},{10,0,0}},"Europe/Paris"), - dtstamp: Timex.to_datetime({{2017,5,13},{10,52,50}},"UTC"), - organizer: [cn: "Arnaud Wetzel",value: "mailto:arnaud.wetzel@example.com"], - uid: "r30al68kn0b2epd9af6kqjg8rg@google.com", - attendee: [cutype: "INDIVIDUAL",role: "REQ-PARTICIPANT",partstat: "NEEDS-ACTION",rsvp: true, cn: "arnaudwetzel@examp.com", - "x-num-guests": 0, value: "mailto:arnaudwetzel@examp.com"], - attendee: [cutype: "INDIVIDUAL",role: "REQ-PARTICIPANT",partstat: "ACCEPTED",rsvp: true, cn: "Arnaud Wetzel", - x_num_guests: 0, value: "mailto:arnaud.wetzel@example.com"], - created: Timex.to_datetime({{2017,5,13},{10,50,32}},"UTC"), - description: String.trim_trailing(""" - Cet événement est associé à un appel vidéo Google Hangouts. - Participer : https://plus.google.com/hangouts/_/exampleexampl.com/arnaud-wetzel?hceid=YXJuYXVkLndldHplbEBrYnJ3YWR2ZW50dXJlLmNvbQ.r30al68kn0b2epd9af6kqjg8rg&hs=121 - - Affichez votre événement sur la page https://www.google.com/calendar/event?action=VIEW&eid=cjMwYWw2OGtuMGIyZXBkOWFmNmtxamc4cmcgYXJuYXVkd2V0emVsQHlhaG9vLmNvbQ&tok=MzEjYXJuYXVkLndldHplbEBrYnJ3YWR2ZW50dXJlLmNvbTdjMzQ0ZGFjN2FlYTM2OGI2NzEzNjAzZjVmNzk2NDVkMjA5ZDc0YjQ&ctz=Europe/Paris&hl=fr. - """), - last_modified: Timex.to_datetime({{2017,5,13},{10,52,49}},"UTC"), - location: "", - sequence: 0, - status: "CONFIRMED", - summary: "c'est un évènement de test", - transp: "OPAQUE" - ]]]]) + res = + Calibex.encode( + vcalendar: [ + [ + prodid: "-//Google Inc//Google Calendar 70.9054//EN", + version: "2.0", + calscale: "GREGORIAN", + method: "REQUEST", + vevent: [ + [ + dtstart: DateTime.new!(~D[2017-05-14], ~T[09:00:00], "Europe/Paris"), + dtend: DateTime.new!(~D[2017-05-14], ~T[10:00:00], "Europe/Paris"), + dtstamp: DateTime.new!(~D[2017-05-13], ~T[10:52:50], "UTC"), + organizer: [cn: "Arnaud Wetzel", value: "mailto:arnaud.wetzel@example.com"], + uid: "r30al68kn0b2epd9af6kqjg8rg@google.com", + attendee: [ + cutype: "INDIVIDUAL", + role: "REQ-PARTICIPANT", + partstat: "NEEDS-ACTION", + rsvp: true, + cn: "arnaudwetzel@examp.com", + "x-num-guests": 0, + value: "mailto:arnaudwetzel@examp.com" + ], + attendee: [ + cutype: "INDIVIDUAL", + role: "REQ-PARTICIPANT", + partstat: "ACCEPTED", + rsvp: true, + cn: "Arnaud Wetzel", + x_num_guests: 0, + value: "mailto:arnaud.wetzel@example.com" + ], + created: DateTime.new!(~D[2017-05-13], ~T[10:50:32], "Etc/UTC"), + description: + String.trim_trailing(""" + Cet événement est associé à un appel vidéo Google Hangouts. + Participer : https://plus.google.com/hangouts/_/exampleexampl.com/arnaud-wetzel?hceid=YXJuYXVkLndldHplbEBrYnJ3YWR2ZW50dXJlLmNvbQ.r30al68kn0b2epd9af6kqjg8rg&hs=121 + + Affichez votre événement sur la page https://www.google.com/calendar/event?action=VIEW&eid=cjMwYWw2OGtuMGIyZXBkOWFmNmtxamc4cmcgYXJuYXVkd2V0emVsQHlhaG9vLmNvbQ&tok=MzEjYXJuYXVkLndldHplbEBrYnJ3YWR2ZW50dXJlLmNvbTdjMzQ0ZGFjN2FlYTM2OGI2NzEzNjAzZjVmNzk2NDVkMjA5ZDc0YjQ&ctz=Europe/Paris&hl=fr. + """), + last_modified: DateTime.new!(~D[2017-05-13], ~T[10:52:49], "Etc/UTC"), + location: "", + sequence: 0, + status: "CONFIRMED", + summary: "c'est un évènement de test", + transp: "OPAQUE" + ] + ] + ] + ] + ) + assert res == File.read!("test/fixtures/invite.ics") end @@ -45,25 +69,62 @@ defmodule CalibexTest do #end test "ical helpers" do - req = Calibex.request(dtstart: Timex.now, dtend: Timex.shift(Timex.now,hours: 1), summary: "Mon évènement", - organizer: "arnaud.wetzel@example.com", attendee: "jeanpierre@yahoo.fr", attendee: "jean@ya.fr") - assert [vcalendar: [[prodid: "-//KBRW//Calibex 0.1.0//EN", version: "2.0", - calscale: "GREGORIAN", method: "REQUEST", - vevent: [[uid: _, - last_modified: %DateTime{}, - sequence: 0, dtstamp: %DateTime{}, - created: %DateTime{}, - status: :confirmed, - dtstart: %DateTime{}, - dtend: %DateTime{}, - summary: "Mon évènement", - organizer: [cn: "arnaud.wetzel@example.com", - value: "mailto:arnaud.wetzel@example.com"], - attendee: [cutype: "INDIVIDUAL", role: "REQ-PARTICIPANT", - partstat: "NEEDS-ACTION", rsvp: true, x_num_guests: 0, - cn: "jeanpierre@yahoo.fr", value: "mailto:jeanpierre@yahoo.fr"], - attendee: [cutype: "INDIVIDUAL", role: "REQ-PARTICIPANT", - partstat: "NEEDS-ACTION", rsvp: true, x_num_guests: 0, cn: "jean@ya.fr", - value: "mailto:jean@ya.fr"]]]]]] = req + now = DateTime.utc_now() + + req = + Calibex.request( + dtstart: now, + dtend: DateTime.add(now, 3_600, :second), + summary: "Mon évènement", + organizer: "arnaud.wetzel@example.com", + attendee: "jeanpierre@yahoo.fr", + attendee: "jean@ya.fr" + ) + + assert [ + vcalendar: [ + [ + prodid: "-//KBRW//Calibex 0.1.0//EN", + version: "2.0", + calscale: "GREGORIAN", + method: "REQUEST", + vevent: [ + [ + uid: _, + last_modified: %DateTime{}, + sequence: 0, + dtstamp: %DateTime{}, + created: %DateTime{}, + status: :confirmed, + dtstart: %DateTime{}, + dtend: %DateTime{}, + summary: "Mon évènement", + organizer: [ + cn: "arnaud.wetzel@example.com", + value: "mailto:arnaud.wetzel@example.com" + ], + attendee: [ + cutype: "INDIVIDUAL", + role: "REQ-PARTICIPANT", + partstat: "NEEDS-ACTION", + rsvp: true, + x_num_guests: 0, + cn: "jeanpierre@yahoo.fr", + value: "mailto:jeanpierre@yahoo.fr" + ], + attendee: [ + cutype: "INDIVIDUAL", + role: "REQ-PARTICIPANT", + partstat: "NEEDS-ACTION", + rsvp: true, + x_num_guests: 0, + cn: "jean@ya.fr", + value: "mailto:jean@ya.fr" + ] + ] + ] + ] + ] + ] = req end end