├── test ├── test_helper.exs └── astarte_core │ ├── proto_astarte_reference_test.exs │ ├── group_test.exs │ ├── interface │ └── enums_test.exs │ ├── realm_test.exs │ ├── device_test.exs │ ├── device │ └── capabilities_test.exs │ ├── triggers │ ├── policy │ │ ├── policy_protobuf_test.exs │ │ └── handler_test.exs │ └── simple_triggers_protobuf_test.exs │ ├── mapping │ ├── enums_test.exs │ └── value_type_test.exs │ ├── interface_descriptor_test.exs │ ├── mapping_test.exs │ └── cql_utils_test.exs ├── .github ├── dco.yml └── workflows │ └── build-workflow.yaml ├── .tool-versions ├── README.md ├── codecov.yaml ├── .formatter.exs ├── AUTHORS.md ├── lib └── astarte_core │ ├── triggers │ ├── simple_events │ │ ├── device_disconnected_event.pb.ex │ │ ├── device_connected_event.pb.ex │ │ ├── path_removed_event.pb.ex │ │ ├── interface_removed_event.pb.ex │ │ ├── incoming_data_event.pb.ex │ │ ├── path_created_event.pb.ex │ │ ├── value_stored_event.pb.ex │ │ ├── interface_added_event.pb.ex │ │ ├── value_change_event.pb.ex │ │ ├── interface_minor_updated_event.pb.ex │ │ ├── value_change_applied_event.pb.ex │ │ ├── device_disconnected_event.proto │ │ ├── device_connected_event.proto │ │ ├── path_removed_event.proto │ │ ├── device_error_event.pb.ex │ │ ├── device_error_event.proto │ │ ├── interface_removed_event.proto │ │ ├── interface_added_event.proto │ │ ├── incoming_data_event.proto │ │ ├── path_created_event.proto │ │ ├── value_stored_event.proto │ │ ├── value_change_event.proto │ │ ├── interface_minor_updated_event.proto │ │ ├── value_change_applied_event.proto │ │ ├── incoming_introspection_event.proto │ │ ├── incoming_introspection_event.pb.ex │ │ ├── simple_event.proto │ │ └── simple_event.pb.ex │ ├── policy_protobuf │ │ ├── error_range.pb.ex │ │ ├── policy.pb.ex │ │ ├── error_keyword.pb.ex │ │ ├── error_range.proto │ │ ├── error_keyword.proto │ │ ├── handler.pb.ex │ │ ├── policy.proto │ │ └── handler.proto │ ├── simple_triggers_protobuf │ │ ├── trigger_target_container.pb.ex │ │ ├── tagged_simple_trigger.pb.ex │ │ ├── simple_trigger_container.pb.ex │ │ ├── trigger_target_container.proto │ │ ├── tagged_simple_trigger.proto │ │ ├── simple_trigger_container.proto │ │ ├── amqp_trigger_target.proto │ │ ├── device_trigger.proto │ │ ├── device_trigger.pb.ex │ │ ├── amqp_trigger_target.pb.ex │ │ ├── data_trigger.proto │ │ ├── data_trigger.pb.ex │ │ └── utils.ex │ ├── trigger.pb.ex │ ├── policy_protobuf.ex │ ├── trigger.proto │ ├── policy │ │ ├── keyword_error.ex │ │ ├── range_error.ex │ │ ├── error_type.ex │ │ ├── handler.ex │ │ └── policy.ex │ └── data_trigger.ex │ ├── proto │ ├── astarte_reference.pb.ex │ └── astarte_reference.proto │ ├── group.ex │ ├── device │ └── capabilities.ex │ ├── realm.ex │ ├── interface │ ├── type.ex │ ├── aggregation.ex │ └── ownership.ex │ ├── mapping │ ├── database_retention_policy.ex │ ├── retention.ex │ ├── reliability.ex │ ├── endpoints_automaton.ex │ └── value_type.ex │ ├── device.ex │ ├── storage_type.ex │ ├── interface_descriptor.ex │ └── cql_utils.ex ├── coveralls.json ├── .gitignore ├── SECURITY.md ├── config └── config.exs ├── mix.exs ├── specs ├── policy.json ├── interface.json └── mapping.json ├── CHANGELOG.md └── mix.lock /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.github/dco.yml: -------------------------------------------------------------------------------- 1 | require: 2 | members: false 3 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.15.7-otp-26 2 | erlang 26.1 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Astarte Core 2 | 3 | Astarte Core library 4 | -------------------------------------------------------------------------------- /codecov.yaml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: off 4 | patch: off 5 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto, :protobuf], 3 | inputs: [ 4 | "lib/**/*.{ex,exs}", 5 | "test/**/*.{ex,exs}", 6 | "mix.exs" 7 | ] 8 | ] 9 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | This software has been developed by: 2 | * Davide Bettio 3 | * Riccardo Binetti 4 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/device_disconnected_event.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.SimpleEvents.DeviceDisconnectedEvent do 2 | @moduledoc false 3 | 4 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "DeviceDisconnectedEvent" 7 | end 8 | -------------------------------------------------------------------------------- /coveralls.json: -------------------------------------------------------------------------------- 1 | { 2 | "default_stop_words": [ 3 | "defmodule", 4 | "defrecord", 5 | "defimpl", 6 | "def.+(.+\/\/.+).+do" 7 | ], 8 | 9 | "custom_stop_words": [ 10 | ], 11 | 12 | "coverage_options": { 13 | "treat_no_relevant_lines_as_covered": true, 14 | "output_dir": "cover/" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/policy_protobuf/error_range.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.PolicyProtobuf.ErrorRange do 2 | @moduledoc false 3 | 4 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "ErrorRange" 7 | 8 | field :error_codes, 1, repeated: true, type: :int32, json_name: "errorCodes" 9 | end 10 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/device_connected_event.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.SimpleEvents.DeviceConnectedEvent do 2 | @moduledoc false 3 | 4 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "DeviceConnectedEvent" 7 | 8 | field :device_ip_address, 1, proto3_optional: true, type: :string, json_name: "deviceIpAddress" 9 | end 10 | -------------------------------------------------------------------------------- /lib/astarte_core/proto/astarte_reference.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.AstarteReference do 2 | @moduledoc false 3 | 4 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "AstarteReference" 7 | 8 | field :object_type, 1, type: :int32, json_name: "objectType" 9 | field :object_uuid, 2, proto3_optional: true, type: :bytes, json_name: "objectUuid" 10 | end 11 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/path_removed_event.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.SimpleEvents.PathRemovedEvent do 2 | @moduledoc false 3 | 4 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "PathRemovedEvent" 7 | 8 | field :interface, 1, proto3_optional: true, type: :string 9 | field :path, 2, proto3_optional: true, type: :string 10 | end 11 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/interface_removed_event.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.SimpleEvents.InterfaceRemovedEvent do 2 | @moduledoc false 3 | 4 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "InterfaceRemovedEvent" 7 | 8 | field :interface, 1, proto3_optional: true, type: :string 9 | field :major_version, 2, type: :int32, json_name: "majorVersion" 10 | end 11 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/incoming_data_event.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.SimpleEvents.IncomingDataEvent do 2 | @moduledoc false 3 | 4 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "IncomingDataEvent" 7 | 8 | field :interface, 1, proto3_optional: true, type: :string 9 | field :path, 2, proto3_optional: true, type: :string 10 | field :bson_value, 3, proto3_optional: true, type: :bytes, json_name: "bsonValue" 11 | end 12 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/path_created_event.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.SimpleEvents.PathCreatedEvent do 2 | @moduledoc false 3 | 4 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "PathCreatedEvent" 7 | 8 | field :interface, 1, proto3_optional: true, type: :string 9 | field :path, 2, proto3_optional: true, type: :string 10 | field :bson_value, 3, proto3_optional: true, type: :bytes, json_name: "bsonValue" 11 | end 12 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/value_stored_event.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.SimpleEvents.ValueStoredEvent do 2 | @moduledoc false 3 | 4 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "ValueStoredEvent" 7 | 8 | field :interface, 1, proto3_optional: true, type: :string 9 | field :path, 2, proto3_optional: true, type: :string 10 | field :bson_value, 3, proto3_optional: true, type: :bytes, json_name: "bsonValue" 11 | end 12 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/interface_added_event.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.SimpleEvents.InterfaceAddedEvent do 2 | @moduledoc false 3 | 4 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "InterfaceAddedEvent" 7 | 8 | field :interface, 1, proto3_optional: true, type: :string 9 | field :major_version, 2, type: :int32, json_name: "majorVersion" 10 | field :minor_version, 3, type: :int32, json_name: "minorVersion" 11 | end 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/value_change_event.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.SimpleEvents.ValueChangeEvent do 2 | @moduledoc false 3 | 4 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "ValueChangeEvent" 7 | 8 | field :interface, 1, proto3_optional: true, type: :string 9 | field :path, 2, proto3_optional: true, type: :string 10 | field :old_bson_value, 3, proto3_optional: true, type: :bytes, json_name: "oldBsonValue" 11 | field :new_bson_value, 4, proto3_optional: true, type: :bytes, json_name: "newBsonValue" 12 | end 13 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_triggers_protobuf/trigger_target_container.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.SimpleTriggersProtobuf.TriggerTargetContainer do 2 | @moduledoc false 3 | 4 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "TriggerTargetContainer" 7 | 8 | oneof :trigger_target, 0 9 | 10 | field :version, 1, type: :int32, deprecated: true 11 | 12 | field :amqp_trigger_target, 2, 13 | type: Astarte.Core.Triggers.SimpleTriggersProtobuf.AMQPTriggerTarget, 14 | json_name: "amqpTriggerTarget", 15 | oneof: 0 16 | end 17 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/interface_minor_updated_event.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.SimpleEvents.InterfaceMinorUpdatedEvent do 2 | @moduledoc false 3 | 4 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "InterfaceMinorUpdatedEvent" 7 | 8 | field :interface, 1, proto3_optional: true, type: :string 9 | field :major_version, 2, type: :int32, json_name: "majorVersion" 10 | field :old_minor_version, 3, type: :int32, json_name: "oldMinorVersion" 11 | field :new_minor_version, 4, type: :int32, json_name: "newMinorVersion" 12 | end 13 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/value_change_applied_event.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.SimpleEvents.ValueChangeAppliedEvent do 2 | @moduledoc false 3 | 4 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "ValueChangeAppliedEvent" 7 | 8 | field :interface, 1, proto3_optional: true, type: :string 9 | field :path, 2, proto3_optional: true, type: :string 10 | field :old_bson_value, 3, proto3_optional: true, type: :bytes, json_name: "oldBsonValue" 11 | field :new_bson_value, 4, proto3_optional: true, type: :bytes, json_name: "newBsonValue" 12 | end 13 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/trigger.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.Trigger do 2 | @moduledoc false 3 | 4 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "Trigger" 7 | 8 | field :version, 1, type: :int32, deprecated: true 9 | field :trigger_uuid, 2, proto3_optional: true, type: :bytes, json_name: "triggerUuid" 10 | field :simple_triggers_uuids, 3, repeated: true, type: :bytes, json_name: "simpleTriggersUuids" 11 | field :action, 4, proto3_optional: true, type: :bytes 12 | field :name, 5, proto3_optional: true, type: :string 13 | field :policy, 6, proto3_optional: true, type: :string 14 | end 15 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Security Policy 8 | 9 | ## Supported Versions 10 | 11 | | Version | Supported | 12 | | ------- | ------------------ | 13 | | 0.10.x | :x: | 14 | | 0.11.x | :x: | 15 | | 1.0.x | :x: | 16 | | 1.1.x | :white_check_mark: | 17 | | 1.2.x | :white_check_mark: | 18 | 19 | ## Reporting a Vulnerability 20 | 21 | To submit a vulnerability report, please contact us at security AT secomind.com. 22 | Please, **do not use GitHub issues for vulnerability reports**. 23 | Your submission will be promptly reviewed and validated by a member of our team. 24 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_triggers_protobuf/tagged_simple_trigger.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.SimpleTriggersProtobuf.TaggedSimpleTrigger do 2 | @moduledoc false 3 | 4 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "TaggedSimpleTrigger" 7 | 8 | field :version, 1, type: :int32, deprecated: true 9 | field :object_id, 2, proto3_optional: true, type: :bytes, json_name: "objectId" 10 | field :object_type, 3, type: :int32, json_name: "objectType" 11 | 12 | field :simple_trigger_container, 4, 13 | type: Astarte.Core.Triggers.SimpleTriggersProtobuf.SimpleTriggerContainer, 14 | json_name: "simpleTriggerContainer" 15 | end 16 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_triggers_protobuf/simple_trigger_container.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.SimpleTriggersProtobuf.SimpleTriggerContainer do 2 | @moduledoc false 3 | 4 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "SimpleTriggerContainer" 7 | 8 | oneof :simple_trigger, 0 9 | 10 | field :version, 1, type: :int32, deprecated: true 11 | 12 | field :device_trigger, 2, 13 | type: Astarte.Core.Triggers.SimpleTriggersProtobuf.DeviceTrigger, 14 | json_name: "deviceTrigger", 15 | oneof: 0 16 | 17 | field :data_trigger, 3, 18 | type: Astarte.Core.Triggers.SimpleTriggersProtobuf.DataTrigger, 19 | json_name: "dataTrigger", 20 | oneof: 0 21 | end 22 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/policy_protobuf/policy.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.PolicyProtobuf.Policy do 2 | @moduledoc false 3 | 4 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "Policy" 7 | 8 | field :name, 1, proto3_optional: true, type: :string 9 | field :maximum_capacity, 2, type: :int32, json_name: "maximumCapacity" 10 | field :retry_times, 3, type: :int32, json_name: "retryTimes" 11 | field :event_ttl, 4, type: :int32, json_name: "eventTtl" 12 | 13 | field :error_handlers, 5, 14 | repeated: true, 15 | type: Astarte.Core.Triggers.PolicyProtobuf.Handler, 16 | json_name: "errorHandlers" 17 | 18 | field :prefetch_count, 6, type: :int32, json_name: "prefetchCount" 19 | end 20 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/policy_protobuf/error_keyword.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.PolicyProtobuf.ErrorKeyword.KeywordType do 2 | @moduledoc false 3 | 4 | use Protobuf, enum: true, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "ErrorKeyword.KeywordType" 7 | 8 | field :INVALID, 0 9 | field :ANY_ERROR, 1 10 | field :CLIENT_ERROR, 2 11 | field :SERVER_ERROR, 3 12 | end 13 | 14 | defmodule Astarte.Core.Triggers.PolicyProtobuf.ErrorKeyword do 15 | @moduledoc false 16 | 17 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 18 | 19 | def fully_qualified_name, do: "ErrorKeyword" 20 | 21 | field :keyword, 1, 22 | type: Astarte.Core.Triggers.PolicyProtobuf.ErrorKeyword.KeywordType, 23 | enum: true 24 | end 25 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/device_disconnected_event.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2017 Ispirata Srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | message DeviceDisconnectedEvent { 22 | } 23 | -------------------------------------------------------------------------------- /test/astarte_core/proto_astarte_reference_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.AstarteReferenceTest do 2 | use ExUnit.Case 3 | 4 | describe "payload serialized with ExProtobuf" do 5 | test "still works for AstarteReference" do 6 | alias Astarte.Core.AstarteReference 7 | 8 | serialized_reference = 9 | <<8, 1, 18, 36, 48, 56, 100, 97, 55, 98, 56, 101, 45, 98, 49, 98, 100, 45, 52, 50, 54, 97, 10 | 45, 57, 102, 101, 56, 45, 54, 102, 99, 50, 57, 99, 98, 48, 100, 101, 97, 97>> 11 | 12 | reference = %AstarteReference{ 13 | object_type: 1, 14 | object_uuid: "08da7b8e-b1bd-426a-9fe8-6fc29cb0deaa" 15 | } 16 | 17 | assert AstarteReference.encode(reference) == serialized_reference 18 | assert AstarteReference.decode(serialized_reference) == reference 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/policy_protobuf/error_range.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2022 SECO Mind srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | message ErrorRange { 22 | repeated int32 error_codes = 1; 23 | } 24 | -------------------------------------------------------------------------------- /lib/astarte_core/proto/astarte_reference.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2018 Ispirata Srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | message AstarteReference { 22 | int32 object_type = 1; 23 | optional bytes object_uuid = 2; 24 | } 25 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/device_connected_event.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2017 Ispirata Srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | message DeviceConnectedEvent { 22 | optional string device_ip_address = 1; 23 | } 24 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/path_removed_event.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2017 Ispirata Srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | message PathRemovedEvent { 22 | optional string interface = 1; 23 | optional string path = 2; 24 | } 25 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/device_error_event.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.SimpleEvents.DeviceErrorEvent.MetadataEntry do 2 | @moduledoc false 3 | 4 | use Protobuf, map: true, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "DeviceErrorEvent.MetadataEntry" 7 | 8 | field :key, 1, type: :string 9 | field :value, 2, type: :string 10 | end 11 | 12 | defmodule Astarte.Core.Triggers.SimpleEvents.DeviceErrorEvent do 13 | @moduledoc false 14 | 15 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 16 | 17 | def fully_qualified_name, do: "DeviceErrorEvent" 18 | 19 | field :error_name, 1, proto3_optional: true, type: :string, json_name: "errorName" 20 | 21 | field :metadata, 2, 22 | repeated: true, 23 | type: Astarte.Core.Triggers.SimpleEvents.DeviceErrorEvent.MetadataEntry, 24 | map: true 25 | end 26 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/device_error_event.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2020 Ispirata Srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | message DeviceErrorEvent { 22 | optional string error_name = 1; 23 | map metadata = 2; 24 | } 25 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/interface_removed_event.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2017 Ispirata Srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | message InterfaceRemovedEvent { 22 | optional string interface = 1; 23 | int32 major_version = 2; 24 | } 25 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/interface_added_event.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2017 Ispirata Srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | message InterfaceAddedEvent { 22 | optional string interface = 1; 23 | int32 major_version = 2; 24 | int32 minor_version = 3; 25 | } 26 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/incoming_data_event.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2017 Ispirata Srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | message IncomingDataEvent { 22 | optional string interface = 1; 23 | optional string path = 2; 24 | optional bytes bson_value = 3; 25 | } 26 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/path_created_event.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2017 Ispirata Srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | message PathCreatedEvent { 22 | optional string interface = 1; 23 | optional string path = 2; 24 | optional bytes bson_value = 3; 25 | } 26 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/value_stored_event.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2017 Ispirata Srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | message ValueStoredEvent { 22 | optional string interface = 1; 23 | optional string path = 2; 24 | optional bytes bson_value = 3; 25 | } 26 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/policy_protobuf.ex: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2022 SECO Mind srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.Triggers.PolicyProtobuf do 20 | @external_resource Path.expand("/policy_protobuf", __DIR__) 21 | 22 | use Protobuf, from: Path.wildcard(Path.expand("policy_protobuf/*.proto", __DIR__)) 23 | end 24 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/value_change_event.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2017 Ispirata Srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | message ValueChangeEvent { 22 | optional string interface = 1; 23 | optional string path = 2; 24 | optional bytes old_bson_value = 3; 25 | optional bytes new_bson_value = 4; 26 | } 27 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/interface_minor_updated_event.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2017 Ispirata Srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | message InterfaceMinorUpdatedEvent { 22 | optional string interface = 1; 23 | int32 major_version = 2; 24 | int32 old_minor_version = 3; 25 | int32 new_minor_version = 4; 26 | } 27 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/value_change_applied_event.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2017 Ispirata Srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | message ValueChangeAppliedEvent { 22 | optional string interface = 1; 23 | optional string path = 2; 24 | optional bytes old_bson_value = 3; 25 | optional bytes new_bson_value = 4; 26 | } 27 | -------------------------------------------------------------------------------- /test/astarte_core/group_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.GroupTest do 2 | use ExUnit.Case 3 | 4 | alias Astarte.Core.Group 5 | 6 | test "empty group name fails" do 7 | assert Group.valid_name?("") == false 8 | end 9 | 10 | test "group name with reserved prefixes fail" do 11 | assert Group.valid_name?("@other") == false 12 | assert Group.valid_name?("~other") == false 13 | assert Group.valid_name?("@~other") == false 14 | assert Group.valid_name?("~@other") == false 15 | end 16 | 17 | test "valid group names are accepted" do 18 | assert Group.valid_name?("plainname") == true 19 | assert Group.valid_name?("a/name-with@many*strange§characters") == true 20 | assert Group.valid_name?("astarte_is_not_reserved_anymore") == true 21 | assert Group.valid_name?("devices-either") == true 22 | assert Group.valid_name?("a~in-second-position-is-fine") == true 23 | assert Group.valid_name?("a@too") == true 24 | assert Group.valid_name?("🤔") == true 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/policy_protobuf/error_keyword.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2022 SECO Mind srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | message ErrorKeyword { 22 | 23 | enum KeywordType { 24 | INVALID = 0; 25 | ANY_ERROR = 1; 26 | CLIENT_ERROR = 2; 27 | SERVER_ERROR = 3; 28 | } 29 | 30 | KeywordType keyword = 1; 31 | } 32 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/policy_protobuf/handler.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.PolicyProtobuf.Handler.StrategyType do 2 | @moduledoc false 3 | 4 | use Protobuf, enum: true, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "Handler.StrategyType" 7 | 8 | field :INVALID, 0 9 | field :DISCARD, 1 10 | field :RETRY, 2 11 | end 12 | 13 | defmodule Astarte.Core.Triggers.PolicyProtobuf.Handler do 14 | @moduledoc false 15 | 16 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 17 | 18 | def fully_qualified_name, do: "Handler" 19 | 20 | oneof :on, 0 21 | 22 | field :strategy, 1, type: Astarte.Core.Triggers.PolicyProtobuf.Handler.StrategyType, enum: true 23 | 24 | field :error_keyword, 2, 25 | type: Astarte.Core.Triggers.PolicyProtobuf.ErrorKeyword, 26 | json_name: "errorKeyword", 27 | oneof: 0 28 | 29 | field :error_range, 3, 30 | type: Astarte.Core.Triggers.PolicyProtobuf.ErrorRange, 31 | json_name: "errorRange", 32 | oneof: 0 33 | end 34 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/incoming_introspection_event.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2017-2024 SECO Mind Srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | message InterfaceVersion { 22 | int32 major = 1; 23 | int32 minor = 2; 24 | } 25 | 26 | message IncomingIntrospectionEvent { 27 | optional string introspection = 1 [deprecated = true]; 28 | map introspection_map = 2; 29 | } 30 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/trigger.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2017 Ispirata Srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | message Trigger { 22 | int32 version = 1 [deprecated = true]; 23 | 24 | optional bytes trigger_uuid = 2; 25 | repeated bytes simple_triggers_uuids = 3; 26 | optional bytes action = 4; 27 | // TODO: add condition 28 | 29 | optional string name = 5; 30 | optional string policy = 6; 31 | } 32 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/policy_protobuf/policy.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2022 SECO Mind srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | import "lib/astarte_core/triggers/policy_protobuf/handler.proto"; 22 | 23 | message Policy { 24 | optional string name = 1; 25 | int32 maximum_capacity = 2; 26 | int32 retry_times = 3; 27 | int32 event_ttl = 4; 28 | repeated Handler error_handlers = 5; 29 | int32 prefetch_count = 6; 30 | } 31 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_triggers_protobuf/trigger_target_container.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2017 Ispirata Srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | import "lib/astarte_core/triggers/simple_triggers_protobuf/amqp_trigger_target.proto"; 22 | 23 | message TriggerTargetContainer { 24 | int32 version = 1 [deprecated = true]; 25 | 26 | oneof trigger_target { 27 | AMQPTriggerTarget amqp_trigger_target = 2; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/astarte_core/interface/enums_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Interface.EnumsTest do 2 | use ExUnit.Case 3 | 4 | alias Astarte.Core.Interface.Aggregation 5 | alias Astarte.Core.Interface.Ownership 6 | alias Astarte.Core.Interface.Type 7 | 8 | # Hardcode the values to avoid changing the serialization by accident 9 | test "Interface.Aggregation consistency" do 10 | assert Aggregation.to_int(:individual) == 1 11 | assert Aggregation.from_int(1) == :individual 12 | 13 | assert Aggregation.to_int(:object) == 2 14 | assert Aggregation.from_int(2) == :object 15 | end 16 | 17 | test "Interface.Ownership consistency" do 18 | assert Ownership.to_int(:device) == 1 19 | assert Ownership.from_int(1) == :device 20 | 21 | assert Ownership.to_int(:server) == 2 22 | assert Ownership.from_int(2) == :server 23 | end 24 | 25 | test "Interface.Type consistency" do 26 | assert Type.to_int(:properties) == 1 27 | assert Type.from_int(1) == :properties 28 | 29 | assert Type.to_int(:datastream) == 2 30 | assert Type.from_int(2) == :datastream 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_triggers_protobuf/tagged_simple_trigger.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2018 Ispirata Srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | import "lib/astarte_core/triggers/simple_triggers_protobuf/simple_trigger_container.proto"; 22 | 23 | message TaggedSimpleTrigger { 24 | int32 version = 1 [deprecated = true]; 25 | 26 | optional bytes object_id = 2; 27 | int32 object_type = 3; 28 | SimpleTriggerContainer simple_trigger_container = 4; 29 | } 30 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/incoming_introspection_event.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.SimpleEvents.InterfaceVersion do 2 | @moduledoc false 3 | 4 | use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" 5 | 6 | field :major, 1, type: :int32 7 | field :minor, 2, type: :int32 8 | end 9 | 10 | defmodule Astarte.Core.Triggers.SimpleEvents.IncomingIntrospectionEvent.IntrospectionMapEntry do 11 | @moduledoc false 12 | 13 | use Protobuf, map: true, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" 14 | 15 | field :key, 1, type: :string 16 | field :value, 2, type: Astarte.Core.Triggers.SimpleEvents.InterfaceVersion 17 | end 18 | 19 | defmodule Astarte.Core.Triggers.SimpleEvents.IncomingIntrospectionEvent do 20 | @moduledoc false 21 | 22 | use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.12.0" 23 | 24 | field :introspection, 1, proto3_optional: true, type: :string, deprecated: true 25 | 26 | field :introspection_map, 2, 27 | repeated: true, 28 | type: Astarte.Core.Triggers.SimpleEvents.IncomingIntrospectionEvent.IntrospectionMapEntry, 29 | json_name: "introspectionMap", 30 | map: true 31 | end 32 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/policy/keyword_error.ex: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2022 SECO Mind srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.Triggers.Policy.ErrorKeyword do 20 | use TypedEctoSchema 21 | import Ecto.Changeset 22 | 23 | @on_constants [ 24 | "client_error", 25 | "server_error", 26 | "any_error" 27 | ] 28 | 29 | @derive Jason.Encoder 30 | @primary_key false 31 | typed_embedded_schema do 32 | field :keyword, :string 33 | end 34 | 35 | def validate(changeset) do 36 | validate_inclusion(changeset, :keyword, @on_constants) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_triggers_protobuf/simple_trigger_container.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2017 Ispirata Srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | import "lib/astarte_core/triggers/simple_triggers_protobuf/data_trigger.proto"; 22 | import "lib/astarte_core/triggers/simple_triggers_protobuf/device_trigger.proto"; 23 | 24 | message SimpleTriggerContainer { 25 | int32 version = 1 [deprecated = true]; 26 | 27 | oneof simple_trigger { 28 | DeviceTrigger device_trigger = 2; 29 | DataTrigger data_trigger = 3; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/policy_protobuf/handler.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2022 SECO Mind srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | import "lib/astarte_core/triggers/policy_protobuf/error_range.proto"; 22 | import "lib/astarte_core/triggers/policy_protobuf/error_keyword.proto"; 23 | 24 | message Handler { 25 | 26 | enum StrategyType { 27 | INVALID = 0; 28 | DISCARD = 1; 29 | RETRY = 2; 30 | } 31 | StrategyType strategy = 1; 32 | oneof on { 33 | ErrorKeyword error_keyword = 2; 34 | ErrorRange error_range = 3; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :astarte_core, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:astarte_core, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /lib/astarte_core/group.ex: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2019 Ispirata Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.Group do 20 | @moduledoc """ 21 | Functions that deal with Astarte groups 22 | """ 23 | 24 | @doc """ 25 | Returns true if `group_name` is a valid group name, false otherwise. 26 | 27 | Valid group names _do not_ start with reserved characters `@` and `~`. 28 | """ 29 | def valid_name?("") do 30 | false 31 | end 32 | 33 | def valid_name?("~" <> _rest) do 34 | false 35 | end 36 | 37 | def valid_name?("@" <> _rest) do 38 | false 39 | end 40 | 41 | def valid_name?(_group_name) do 42 | true 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/policy/range_error.ex: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2022 SECO Mind srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.Triggers.Policy.ErrorRange do 20 | use TypedEctoSchema 21 | import Ecto.Changeset 22 | 23 | @error_range 400..599 24 | 25 | @derive Jason.Encoder 26 | @primary_key false 27 | typed_embedded_schema do 28 | field :error_codes, {:array, :integer} 29 | end 30 | 31 | def validate(changeset) do 32 | changeset 33 | |> validate_subset(:error_codes, @error_range, 34 | message: "Must be an HTTP error code between 400 and 599" 35 | ) 36 | |> validate_length(:error_codes, min: 1) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_triggers_protobuf/amqp_trigger_target.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2017-2020 Ispirata Srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | message AMQPTriggerTarget { 22 | int32 version = 1 [deprecated = true]; 23 | 24 | // those should stay null, they get overwritten once loaded 25 | optional bytes simple_trigger_id = 2; 26 | optional bytes parent_trigger_id = 3; 27 | 28 | optional string routing_key = 4; 29 | map static_headers = 5; 30 | optional string exchange = 6; 31 | 32 | int32 message_expiration_ms = 7; 33 | int32 message_priority = 8; 34 | bool message_persistent = 9; 35 | } 36 | -------------------------------------------------------------------------------- /test/astarte_core/realm_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.RealmTest do 2 | use ExUnit.Case 3 | 4 | alias Astarte.Core.Realm 5 | 6 | test "empty realm name is rejected" do 7 | assert Realm.valid_name?("") == false 8 | end 9 | 10 | test "realm name with symbols are rejected" do 11 | assert Realm.valid_name?("my_realm") == false 12 | assert Realm.valid_name?("my-realm") == false 13 | assert Realm.valid_name?("my/realm") == false 14 | assert Realm.valid_name?("my@realm") == false 15 | assert Realm.valid_name?("🤔") == false 16 | end 17 | 18 | test "reserved realm name are rejected" do 19 | assert Realm.valid_name?("astarte") == false 20 | assert Realm.valid_name?("system") == false 21 | assert Realm.valid_name?("system_schema") == false 22 | assert Realm.valid_name?("system_other") == false 23 | end 24 | 25 | test "realm name that are longer than 48 valid characters are rejected" do 26 | # 48 characters 27 | assert Realm.valid_name?("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") == true 28 | # 49 characters 29 | assert Realm.valid_name?("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") == false 30 | end 31 | 32 | test "valid realm names are accepted" do 33 | assert Realm.valid_name?("goodrealmname") == true 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/astarte_core/device/capabilities.ex: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2025 SECO Mind Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | defmodule Astarte.Core.Device.Capabilities do 22 | use Ecto.Schema 23 | import Ecto.Changeset 24 | 25 | alias Astarte.Core.Device.Capabilities 26 | 27 | @required_fields [] 28 | 29 | @permitted_fields [:purge_properties_compression_format] ++ @required_fields 30 | 31 | @primary_key false 32 | embedded_schema do 33 | field :purge_properties_compression_format, Ecto.Enum, 34 | values: [zlib: 0, plaintext: 1], 35 | default: :zlib 36 | end 37 | 38 | def changeset(%Capabilities{} = capabilities, params \\ %{}) do 39 | capabilities 40 | |> cast(params, @permitted_fields) 41 | |> validate_required(@required_fields) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_triggers_protobuf/device_trigger.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2017 Ispirata Srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | message DeviceTrigger { 22 | int32 version = 1 [deprecated = true]; 23 | 24 | enum DeviceEventType { 25 | INVALID = 0; 26 | DEVICE_CONNECTED = 1; 27 | DEVICE_DISCONNECTED = 2; 28 | DEVICE_EMPTY_CACHE_RECEIVED = 3; 29 | DEVICE_ERROR = 4; 30 | INCOMING_INTROSPECTION = 5; 31 | INTERFACE_ADDED = 6; 32 | INTERFACE_REMOVED = 7; 33 | INTERFACE_MINOR_UPDATED = 8; 34 | } 35 | 36 | DeviceEventType device_event_type = 2; 37 | optional string device_id = 3; 38 | optional string group_name = 4; 39 | optional string interface_name = 5; 40 | int32 interface_major = 6; 41 | } 42 | -------------------------------------------------------------------------------- /lib/astarte_core/realm.ex: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2019 Ispirata Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.Realm do 20 | @moduledoc """ 21 | Functions that deal with Astarte realms 22 | """ 23 | 24 | @realm_regex ~r/^[a-z][a-z0-9]{0,47}$/ 25 | 26 | @doc """ 27 | Returns true if `realm_name` is a valid realm name, false otherwise. 28 | 29 | Valid realm names match this regular expression: `#{inspect(@realm_regex)}`. 30 | In addition, the `astarte` and `system` names and the `system_` prefix are reserved. 31 | """ 32 | def valid_name?("astarte") do 33 | false 34 | end 35 | 36 | def valid_name?("system") do 37 | false 38 | end 39 | 40 | def valid_name?("system_" <> _rest) do 41 | false 42 | end 43 | 44 | def valid_name?(realm_name) do 45 | String.match?(realm_name, @realm_regex) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_triggers_protobuf/device_trigger.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.SimpleTriggersProtobuf.DeviceTrigger.DeviceEventType do 2 | @moduledoc false 3 | 4 | use Protobuf, enum: true, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "DeviceTrigger.DeviceEventType" 7 | 8 | field :INVALID, 0 9 | field :DEVICE_CONNECTED, 1 10 | field :DEVICE_DISCONNECTED, 2 11 | field :DEVICE_EMPTY_CACHE_RECEIVED, 3 12 | field :DEVICE_ERROR, 4 13 | field :INCOMING_INTROSPECTION, 5 14 | field :INTERFACE_ADDED, 6 15 | field :INTERFACE_REMOVED, 7 16 | field :INTERFACE_MINOR_UPDATED, 8 17 | end 18 | 19 | defmodule Astarte.Core.Triggers.SimpleTriggersProtobuf.DeviceTrigger do 20 | @moduledoc false 21 | 22 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 23 | 24 | def fully_qualified_name, do: "DeviceTrigger" 25 | 26 | field :version, 1, type: :int32, deprecated: true 27 | 28 | field :device_event_type, 2, 29 | type: Astarte.Core.Triggers.SimpleTriggersProtobuf.DeviceTrigger.DeviceEventType, 30 | json_name: "deviceEventType", 31 | enum: true 32 | 33 | field :device_id, 3, proto3_optional: true, type: :string, json_name: "deviceId" 34 | field :group_name, 4, proto3_optional: true, type: :string, json_name: "groupName" 35 | field :interface_name, 5, proto3_optional: true, type: :string, json_name: "interfaceName" 36 | field :interface_major, 6, type: :int32, json_name: "interfaceMajor" 37 | end 38 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_triggers_protobuf/amqp_trigger_target.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.SimpleTriggersProtobuf.AMQPTriggerTarget.StaticHeadersEntry do 2 | @moduledoc false 3 | 4 | use Protobuf, map: true, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "AMQPTriggerTarget.StaticHeadersEntry" 7 | 8 | field :key, 1, type: :string 9 | field :value, 2, type: :string 10 | end 11 | 12 | defmodule Astarte.Core.Triggers.SimpleTriggersProtobuf.AMQPTriggerTarget do 13 | @moduledoc false 14 | 15 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 16 | 17 | def fully_qualified_name, do: "AMQPTriggerTarget" 18 | 19 | field :version, 1, type: :int32, deprecated: true 20 | field :simple_trigger_id, 2, proto3_optional: true, type: :bytes, json_name: "simpleTriggerId" 21 | field :parent_trigger_id, 3, proto3_optional: true, type: :bytes, json_name: "parentTriggerId" 22 | field :routing_key, 4, proto3_optional: true, type: :string, json_name: "routingKey" 23 | 24 | field :static_headers, 5, 25 | repeated: true, 26 | type: Astarte.Core.Triggers.SimpleTriggersProtobuf.AMQPTriggerTarget.StaticHeadersEntry, 27 | json_name: "staticHeaders", 28 | map: true 29 | 30 | field :exchange, 6, proto3_optional: true, type: :string 31 | field :message_expiration_ms, 7, type: :int32, json_name: "messageExpirationMs" 32 | field :message_priority, 8, type: :int32, json_name: "messagePriority" 33 | field :message_persistent, 9, type: :bool, json_name: "messagePersistent" 34 | end 35 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_triggers_protobuf/data_trigger.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2017 Ispirata Srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | message DataTrigger { 22 | int32 version = 1 [deprecated = true]; 23 | 24 | enum DataTriggerType { 25 | INVALID = 0; 26 | INCOMING_DATA = 1; 27 | VALUE_CHANGE = 2; 28 | VALUE_CHANGE_APPLIED = 3; 29 | PATH_CREATED = 4; 30 | PATH_REMOVED = 5; 31 | VALUE_STORED = 6; 32 | } 33 | 34 | enum MatchOperator { 35 | INVALID_OPERATOR = 0; 36 | ANY = 1; 37 | EQUAL_TO = 2; 38 | NOT_EQUAL_TO = 3; 39 | GREATER_THAN = 4; 40 | GREATER_OR_EQUAL_TO = 5; 41 | LESS_THAN = 6; 42 | LESS_OR_EQUAL_TO = 7; 43 | CONTAINS = 8; 44 | NOT_CONTAINS = 9; 45 | } 46 | 47 | DataTriggerType data_trigger_type = 2; 48 | optional string interface_name = 3; 49 | int32 interface_major = 4; 50 | optional string match_path = 5; 51 | MatchOperator value_match_operator = 6; 52 | optional bytes known_value = 7; 53 | optional string device_id = 8; 54 | optional string group_name = 9; 55 | } 56 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/data_trigger.ex: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2017 Ispirata Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.Triggers.DataTrigger do 20 | use TypedStruct 21 | alias Astarte.Core.Triggers.SimpleTriggersProtobuf.AMQPTriggerTarget 22 | 23 | @type amqp_trigger_target :: AMQPTriggerTarget.t() 24 | @type known_value :: term() | nil 25 | @type interface_id :: :any_interface | :binary 26 | @type path_match_tokens :: :any_endpoint | String.t() 27 | @type value_match_operator :: 28 | :ANY 29 | | :EQUAL_TO 30 | | :NOT_EQUAL_TO 31 | | :GREATER_THAN 32 | | :GREATER_OR_EQUAL_TO 33 | | :LESS_THAN 34 | | :LESS_OR_EQUAL_TO 35 | | :CONTAINS 36 | | :NOT_CONTAINS 37 | 38 | typedstruct do 39 | field :interface_id, interface_id() 40 | field :path_match_tokens, path_match_tokens() 41 | field :value_match_operator, value_match_operator() 42 | field :known_value, known_value() 43 | field :trigger_targets, [amqp_trigger_target()], enforce: true 44 | end 45 | 46 | def are_congruent?(trigger_a, trigger_b) do 47 | trigger_a.interface_id == trigger_b.interface_id and 48 | trigger_a.path_match_tokens == trigger_b.path_match_tokens and 49 | trigger_a.value_match_operator == trigger_b.value_match_operator and 50 | trigger_a.known_value == trigger_b.known_value 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/astarte_core/device_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.DeviceTest do 2 | use ExUnit.Case 3 | 4 | alias Astarte.Core.Device 5 | 6 | @invalid_characters_device_id "notbase64§" 7 | @short_device_id "uTSXEtcgQ3" 8 | @regular_device_id "74Zp9OoNRc-Vsi5RcsIf4A" 9 | @long_device_id "uTSXEtcgQ3qczX3ixpZeFrWgx0kxk0bfrUzkqTIhCck" 10 | 11 | test "invalid device ids decoding fails" do 12 | assert {:error, :invalid_device_id} = Device.decode_device_id(@invalid_characters_device_id) 13 | assert {:error, :invalid_device_id} = Device.decode_device_id(@short_device_id) 14 | end 15 | 16 | test "long device ids decoding fails with no options" do 17 | assert {:error, :extended_id_not_allowed} = Device.decode_device_id(@long_device_id) 18 | end 19 | 20 | test "regular device id decoding succeeds" do 21 | assert {:ok, _device_id} = Device.decode_device_id(@regular_device_id) 22 | end 23 | 24 | test "long device id decoding succeeds with allow_extended_id" do 25 | assert {:ok, _device_id} = Device.decode_device_id(@long_device_id, allow_extended_id: true) 26 | end 27 | 28 | test "extended device id decoding succeeds with long id" do 29 | assert {:ok, _device_id, _extended_device_id} = 30 | Device.decode_extended_device_id(@long_device_id) 31 | end 32 | 33 | test "extended device id decoding gives an empty extended id on regular id" do 34 | assert {:ok, _device_id, ""} = Device.decode_extended_device_id(@regular_device_id) 35 | end 36 | 37 | test "encoding fails with device id not 128 bit long" do 38 | assert {:ok, device_id, extended_id} = Device.decode_extended_device_id(@long_device_id) 39 | long_id = device_id <> extended_id 40 | 41 | assert_raise FunctionClauseError, fn -> 42 | Device.encode_device_id(long_id) 43 | end 44 | end 45 | 46 | test "encoding/decoding roundtrip" do 47 | assert {:ok, device_id} = Device.decode_device_id(@regular_device_id) 48 | assert Device.encode_device_id(device_id) == @regular_device_id 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/astarte_core/device/capabilities_test.exs: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2025 SECO Mind Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | defmodule Astarte.Core.Device.CapabilitiesTest do 22 | use ExUnit.Case 23 | 24 | alias Astarte.Core.Device.Capabilities 25 | 26 | test "capabilities with :purge_properties_compression_format :zlib" do 27 | params = %{ 28 | "purge_properties_compression_format" => "zlib" 29 | } 30 | 31 | changeset = Capabilities.changeset(%Capabilities{}, params) 32 | 33 | assert %Ecto.Changeset{valid?: true} = changeset 34 | 35 | {:ok, capabilities} = Ecto.Changeset.apply_action(changeset, :insert) 36 | 37 | assert %Capabilities{purge_properties_compression_format: :zlib} = capabilities 38 | end 39 | 40 | test "capabilities with :purge_properties_compression_format :plaintext" do 41 | params = %{ 42 | "purge_properties_compression_format" => "plaintext" 43 | } 44 | 45 | changeset = Capabilities.changeset(%Capabilities{}, params) 46 | 47 | assert %Ecto.Changeset{valid?: true} = changeset 48 | 49 | {:ok, capabilities} = Ecto.Changeset.apply_action(changeset, :insert) 50 | 51 | assert %Capabilities{purge_properties_compression_format: :plaintext} = capabilities 52 | end 53 | 54 | test "capabilities with invalid :purge_properties_compression_format fails" do 55 | params = %{ 56 | purge_properties_compression_format: :invalid 57 | } 58 | 59 | changeset = Capabilities.changeset(%Capabilities{}, params) 60 | 61 | assert %Ecto.Changeset{valid?: false} = changeset 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/astarte_core/triggers/policy/policy_protobuf_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.PolicyProtobufTest do 2 | use ExUnit.Case 3 | 4 | describe "payload serialized with ExProtobuf" do 5 | test "still works for ErrorRange" do 6 | alias Astarte.Core.Triggers.PolicyProtobuf.ErrorRange 7 | 8 | serialized_error = <<10, 4, 162, 3, 164, 3>> 9 | 10 | error = %ErrorRange{ 11 | error_codes: [418, 420] 12 | } 13 | 14 | assert ErrorRange.encode(error) == serialized_error 15 | assert ErrorRange.decode(serialized_error) == error 16 | end 17 | 18 | test "still works for ErrorKeyword" do 19 | alias Astarte.Core.Triggers.PolicyProtobuf.ErrorKeyword 20 | 21 | serialized_error = <<8, 2>> 22 | 23 | error = %ErrorKeyword{ 24 | keyword: :CLIENT_ERROR 25 | } 26 | 27 | assert ErrorKeyword.encode(error) == serialized_error 28 | assert ErrorKeyword.decode(serialized_error) == error 29 | end 30 | 31 | test "still works for Handler" do 32 | alias Astarte.Core.Triggers.PolicyProtobuf.Handler 33 | alias Astarte.Core.Triggers.PolicyProtobuf.ErrorKeyword 34 | 35 | serialized_handler = <<8, 2, 18, 2, 8, 2>> 36 | 37 | handler = %Handler{ 38 | on: {:error_keyword, %ErrorKeyword{keyword: :CLIENT_ERROR}}, 39 | strategy: :RETRY 40 | } 41 | 42 | assert Handler.encode(handler) == serialized_handler 43 | assert Handler.decode(serialized_handler) == handler 44 | end 45 | 46 | test "still works for Policy" do 47 | alias Astarte.Core.Triggers.PolicyProtobuf.Policy 48 | alias Astarte.Core.Triggers.PolicyProtobuf.Handler 49 | alias Astarte.Core.Triggers.PolicyProtobuf.ErrorKeyword 50 | 51 | serialized_policy = 52 | <<10, 8, 97, 95, 112, 111, 108, 105, 99, 121, 16, 10, 42, 6, 8, 2, 18, 2, 8, 2>> 53 | 54 | policy = %Policy{ 55 | name: "a_policy", 56 | maximum_capacity: 10, 57 | error_handlers: [ 58 | %Handler{on: {:error_keyword, %ErrorKeyword{keyword: :CLIENT_ERROR}}, strategy: :RETRY} 59 | ] 60 | } 61 | 62 | assert Policy.encode(policy) == serialized_policy 63 | assert Policy.decode(serialized_policy) == policy 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_triggers_protobuf/data_trigger.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.SimpleTriggersProtobuf.DataTrigger.DataTriggerType do 2 | @moduledoc false 3 | 4 | use Protobuf, enum: true, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "DataTrigger.DataTriggerType" 7 | 8 | field :INVALID, 0 9 | field :INCOMING_DATA, 1 10 | field :VALUE_CHANGE, 2 11 | field :VALUE_CHANGE_APPLIED, 3 12 | field :PATH_CREATED, 4 13 | field :PATH_REMOVED, 5 14 | field :VALUE_STORED, 6 15 | end 16 | 17 | defmodule Astarte.Core.Triggers.SimpleTriggersProtobuf.DataTrigger.MatchOperator do 18 | @moduledoc false 19 | 20 | use Protobuf, enum: true, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 21 | 22 | def fully_qualified_name, do: "DataTrigger.MatchOperator" 23 | 24 | field :INVALID_OPERATOR, 0 25 | field :ANY, 1 26 | field :EQUAL_TO, 2 27 | field :NOT_EQUAL_TO, 3 28 | field :GREATER_THAN, 4 29 | field :GREATER_OR_EQUAL_TO, 5 30 | field :LESS_THAN, 6 31 | field :LESS_OR_EQUAL_TO, 7 32 | field :CONTAINS, 8 33 | field :NOT_CONTAINS, 9 34 | end 35 | 36 | defmodule Astarte.Core.Triggers.SimpleTriggersProtobuf.DataTrigger do 37 | @moduledoc false 38 | 39 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 40 | 41 | def fully_qualified_name, do: "DataTrigger" 42 | 43 | field :version, 1, type: :int32, deprecated: true 44 | 45 | field :data_trigger_type, 2, 46 | type: Astarte.Core.Triggers.SimpleTriggersProtobuf.DataTrigger.DataTriggerType, 47 | json_name: "dataTriggerType", 48 | enum: true 49 | 50 | field :interface_name, 3, proto3_optional: true, type: :string, json_name: "interfaceName" 51 | field :interface_major, 4, type: :int32, json_name: "interfaceMajor" 52 | field :match_path, 5, proto3_optional: true, type: :string, json_name: "matchPath" 53 | 54 | field :value_match_operator, 6, 55 | type: Astarte.Core.Triggers.SimpleTriggersProtobuf.DataTrigger.MatchOperator, 56 | json_name: "valueMatchOperator", 57 | enum: true 58 | 59 | field :known_value, 7, proto3_optional: true, type: :bytes, json_name: "knownValue" 60 | field :device_id, 8, proto3_optional: true, type: :string, json_name: "deviceId" 61 | field :group_name, 9, proto3_optional: true, type: :string, json_name: "groupName" 62 | end 63 | -------------------------------------------------------------------------------- /test/astarte_core/mapping/enums_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Mapping.EnumsTest do 2 | use ExUnit.Case 3 | 4 | alias Astarte.Core.Mapping.Reliability 5 | alias Astarte.Core.Mapping.Retention 6 | alias Astarte.Core.Mapping.ValueType 7 | 8 | # Hardcode the values to avoid changing the serialization by accident 9 | test "Mapping.ValueType consistency" do 10 | assert ValueType.to_int(:double) == 1 11 | assert ValueType.from_int(1) == :double 12 | 13 | assert ValueType.to_int(:doublearray) == 2 14 | assert ValueType.from_int(2) == :doublearray 15 | 16 | assert ValueType.to_int(:integer) == 3 17 | assert ValueType.from_int(3) == :integer 18 | 19 | assert ValueType.to_int(:integerarray) == 4 20 | assert ValueType.from_int(4) == :integerarray 21 | 22 | assert ValueType.to_int(:longinteger) == 5 23 | assert ValueType.from_int(5) == :longinteger 24 | 25 | assert ValueType.to_int(:longintegerarray) == 6 26 | assert ValueType.from_int(6) == :longintegerarray 27 | 28 | assert ValueType.to_int(:string) == 7 29 | assert ValueType.from_int(7) == :string 30 | 31 | assert ValueType.to_int(:stringarray) == 8 32 | assert ValueType.from_int(8) == :stringarray 33 | 34 | assert ValueType.to_int(:boolean) == 9 35 | assert ValueType.from_int(9) == :boolean 36 | 37 | assert ValueType.to_int(:booleanarray) == 10 38 | assert ValueType.from_int(10) == :booleanarray 39 | 40 | assert ValueType.to_int(:binaryblob) == 11 41 | assert ValueType.from_int(11) == :binaryblob 42 | 43 | assert ValueType.to_int(:binaryblobarray) == 12 44 | assert ValueType.from_int(12) == :binaryblobarray 45 | 46 | assert ValueType.to_int(:datetime) == 13 47 | assert ValueType.from_int(13) == :datetime 48 | 49 | assert ValueType.to_int(:datetimearray) == 14 50 | assert ValueType.from_int(14) == :datetimearray 51 | end 52 | 53 | test "Mapping.Reliability consistency" do 54 | assert Reliability.to_int(:unreliable) == 1 55 | assert Reliability.from_int(1) == :unreliable 56 | 57 | assert Reliability.to_int(:guaranteed) == 2 58 | assert Reliability.from_int(2) == :guaranteed 59 | 60 | assert Reliability.to_int(:unique) == 3 61 | assert Reliability.from_int(3) == :unique 62 | end 63 | 64 | test "Mapping.Retention consistency" do 65 | assert Retention.to_int(:discard) == 1 66 | assert Retention.from_int(1) == :discard 67 | 68 | assert Retention.to_int(:volatile) == 2 69 | assert Retention.from_int(2) == :volatile 70 | 71 | assert Retention.to_int(:stored) == 3 72 | assert Retention.from_int(3) == :stored 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /test/astarte_core/triggers/policy/handler_test.exs: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2022 SECO Mind srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.Triggers.Policy.HandlerTest do 20 | use ExUnit.Case 21 | alias Astarte.Core.Triggers.Policy.Handler 22 | 23 | test "valid keyword handler" do 24 | params = %{ 25 | "on" => "any_error", 26 | "strategy" => "discard" 27 | } 28 | 29 | assert %Ecto.Changeset{valid?: true} = Handler.changeset(%Handler{}, params) 30 | end 31 | 32 | test "valid Http error codes handler" do 33 | params = %{ 34 | "on" => [400, 401, 502], 35 | "strategy" => "discard" 36 | } 37 | 38 | assert %Ecto.Changeset{valid?: true} = Handler.changeset(%Handler{}, params) 39 | end 40 | 41 | test "invalid keyword handler fails" do 42 | params = %{ 43 | "on" => "invalid_error", 44 | "strategy" => "discard" 45 | } 46 | 47 | assert %Ecto.Changeset{valid?: false, errors: [on: _]} = Handler.changeset(%Handler{}, params) 48 | end 49 | 50 | test "empty http error codes handler fails" do 51 | params = %{ 52 | "on" => [], 53 | "strategy" => "discard" 54 | } 55 | 56 | assert %Ecto.Changeset{valid?: false, errors: [on: _]} = Handler.changeset(%Handler{}, params) 57 | end 58 | 59 | test "invalid (< 400) http error codes handler fails" do 60 | params = %{ 61 | "on" => [399], 62 | "strategy" => "discard" 63 | } 64 | 65 | assert %Ecto.Changeset{valid?: false, errors: [on: _]} = Handler.changeset(%Handler{}, params) 66 | end 67 | 68 | test "invalid (> 599) http error codes handler fails" do 69 | params = %{ 70 | "on" => [600], 71 | "strategy" => "discard" 72 | } 73 | 74 | assert %Ecto.Changeset{valid?: false, errors: [on: _]} = Handler.changeset(%Handler{}, params) 75 | end 76 | 77 | test "invalid strategy handler fails" do 78 | params = %{ 79 | "on" => "any_error", 80 | "strategy" => "none" 81 | } 82 | 83 | assert %Ecto.Changeset{valid?: false, errors: [strategy: _]} = 84 | Handler.changeset(%Handler{}, params) 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2017-2021 Ispirata Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.Mixfile do 20 | use Mix.Project 21 | 22 | def project do 23 | [ 24 | app: :astarte_core, 25 | version: "1.3.0-dev", 26 | elixir: "~> 1.15", 27 | build_embedded: Mix.env() == :prod, 28 | start_permanent: Mix.env() == :prod, 29 | test_coverage: [tool: ExCoveralls], 30 | preferred_cli_env: [ 31 | coveralls: :test, 32 | "coveralls.detail": :test, 33 | "coveralls.post": :test, 34 | "coveralls.html": :test 35 | ], 36 | description: description(), 37 | package: package(), 38 | dialyzer: [plt_core_path: dialyzer_cache_directory(Mix.env())], 39 | deps: deps(), 40 | source_url: "https://github.com/astarte-platform/astarte_core", 41 | homepage_url: "https://astarte-platform.org/" 42 | ] 43 | end 44 | 45 | def application do 46 | # Specify extra applications you'll use from Erlang/Elixir 47 | [extra_applications: [:logger]] 48 | end 49 | 50 | defp dialyzer_cache_directory(:ci) do 51 | "dialyzer_cache" 52 | end 53 | 54 | defp dialyzer_cache_directory(_) do 55 | nil 56 | end 57 | 58 | defp deps do 59 | [ 60 | {:cyanide, "~> 2.0"}, 61 | {:ecto, "~> 3.4"}, 62 | {:ecto_morph, "~> 0.1.23"}, 63 | {:typed_ecto_schema, "~> 0.4"}, 64 | {:typedstruct, "~> 0.5"}, 65 | {:protobuf, "~> 0.12"}, 66 | {:jason, "~> 1.2"}, 67 | {:elixir_uuid, "~> 1.2"}, 68 | {:excoveralls, "~> 0.15", only: :test}, 69 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, 70 | {:dialyxir, "~> 1.4", only: [:dev, :ci], runtime: false} 71 | ] 72 | end 73 | 74 | defp description do 75 | """ 76 | Astarte Core library. 77 | """ 78 | end 79 | 80 | defp package do 81 | [ 82 | maintainers: ["Davide Bettio", "Riccardo Binetti"], 83 | licenses: ["Apache-2.0"], 84 | links: %{ 85 | "Astarte" => "https://astarte-platform.org", 86 | "Ispirata" => "https://ispirata.com", 87 | "GitHub" => "https://github.com/astarte-platform/astarte_core" 88 | } 89 | ] 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/astarte_core/interface/type.ex: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2017-2024 SECO Mind Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.Interface.Type do 20 | use Ecto.Type 21 | 22 | @type t :: :properties | :datastream 23 | 24 | @interface_type_properties 1 25 | @interface_type_datastream 2 26 | @valid_atoms [ 27 | :properties, 28 | :datastream 29 | ] 30 | 31 | @impl true 32 | def type, do: :integer 33 | 34 | @impl true 35 | def cast(nil), do: {:ok, nil} 36 | 37 | def cast(atom) when is_atom(atom) do 38 | if Enum.member?(@valid_atoms, atom) do 39 | {:ok, atom} 40 | else 41 | :error 42 | end 43 | end 44 | 45 | def cast(string) when is_binary(string) do 46 | case string do 47 | "properties" -> {:ok, :properties} 48 | "datastream" -> {:ok, :datastream} 49 | _ -> :error 50 | end 51 | end 52 | 53 | def cast(int) when is_integer(int) do 54 | load(int) 55 | end 56 | 57 | def cast(_), do: :error 58 | 59 | def cast!(value) do 60 | case cast(value) do 61 | {:ok, type} -> 62 | type 63 | 64 | :error -> 65 | raise ArgumentError, 66 | message: "#{inspect(value)} is not a valid interface type representation" 67 | end 68 | end 69 | 70 | @impl true 71 | def dump(type) when is_atom(type) do 72 | case type do 73 | :properties -> {:ok, @interface_type_properties} 74 | :datastream -> {:ok, @interface_type_datastream} 75 | _ -> :error 76 | end 77 | end 78 | 79 | def dump!(type) when is_atom(type) do 80 | case dump(type) do 81 | {:ok, type_int} -> type_int 82 | :error -> raise ArgumentError, message: "#{inspect(type)} is not a valid interface type" 83 | end 84 | end 85 | 86 | @impl true 87 | def load(type_int) when is_integer(type_int) do 88 | case type_int do 89 | @interface_type_properties -> {:ok, :properties} 90 | @interface_type_datastream -> {:ok, :datastream} 91 | _ -> :error 92 | end 93 | end 94 | 95 | def to_int(interface) when is_atom(interface) do 96 | dump!(interface) 97 | end 98 | 99 | def from_int(int) when is_integer(int) do 100 | cast!(int) 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/simple_event.proto: -------------------------------------------------------------------------------- 1 | // 2 | // This file is part of Astarte. 3 | // 4 | // Copyright 2017 Ispirata Srl 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | syntax = "proto3"; 20 | 21 | import "lib/astarte_core/triggers/simple_events/device_connected_event.proto"; 22 | import "lib/astarte_core/triggers/simple_events/device_disconnected_event.proto"; 23 | import "lib/astarte_core/triggers/simple_events/device_error_event.proto"; 24 | import "lib/astarte_core/triggers/simple_events/incoming_data_event.proto"; 25 | import "lib/astarte_core/triggers/simple_events/value_change_event.proto"; 26 | import "lib/astarte_core/triggers/simple_events/value_change_applied_event.proto"; 27 | import "lib/astarte_core/triggers/simple_events/path_created_event.proto"; 28 | import "lib/astarte_core/triggers/simple_events/path_removed_event.proto"; 29 | import "lib/astarte_core/triggers/simple_events/value_stored_event.proto"; 30 | import "lib/astarte_core/triggers/simple_events/incoming_introspection_event.proto"; 31 | import "lib/astarte_core/triggers/simple_events/interface_added_event.proto"; 32 | import "lib/astarte_core/triggers/simple_events/interface_removed_event.proto"; 33 | import "lib/astarte_core/triggers/simple_events/interface_minor_updated_event.proto"; 34 | 35 | message SimpleEvent { 36 | int32 version = 1 [deprecated = true]; 37 | 38 | optional bytes simple_trigger_id = 2; 39 | optional bytes parent_trigger_id = 3; 40 | 41 | optional string realm = 4; 42 | optional string device_id = 5; 43 | optional int64 timestamp = 18; 44 | 45 | oneof event { 46 | DeviceConnectedEvent device_connected_event = 6; 47 | DeviceDisconnectedEvent device_disconnected_event = 7; 48 | IncomingDataEvent incoming_data_event = 8; 49 | ValueChangeEvent value_change_event = 9; 50 | ValueChangeAppliedEvent value_change_applied_event = 10; 51 | PathCreatedEvent path_created_event = 11; 52 | PathRemovedEvent path_removed_event = 12; 53 | ValueStoredEvent value_stored_event = 13; 54 | IncomingIntrospectionEvent incoming_introspection_event = 14; 55 | InterfaceAddedEvent interface_added_event = 15; 56 | InterfaceRemovedEvent interface_removed_event = 16; 57 | InterfaceMinorUpdatedEvent interface_minor_updated_event = 17; 58 | DeviceErrorEvent device_error_event = 19; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/astarte_core/interface/aggregation.ex: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2017-2024 SECO Mind Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.Interface.Aggregation do 20 | use Ecto.Type 21 | 22 | @type t :: :individual | :object 23 | 24 | @interface_aggregation_individual 1 25 | @interface_aggregation_object 2 26 | @valid_atoms [ 27 | :individual, 28 | :object 29 | ] 30 | 31 | @impl true 32 | def type, do: :integer 33 | 34 | @impl true 35 | def cast(nil), do: {:ok, nil} 36 | 37 | def cast(atom) when is_atom(atom) do 38 | if Enum.member?(@valid_atoms, atom) do 39 | {:ok, atom} 40 | else 41 | :error 42 | end 43 | end 44 | 45 | def cast(string) when is_binary(string) do 46 | case string do 47 | "individual" -> {:ok, :individual} 48 | "object" -> {:ok, :object} 49 | _ -> :error 50 | end 51 | end 52 | 53 | def cast(int) when is_integer(int) do 54 | load(int) 55 | end 56 | 57 | def cast(_), do: :error 58 | 59 | def cast!(value) do 60 | case cast(value) do 61 | {:ok, aggregation} -> 62 | aggregation 63 | 64 | :error -> 65 | raise ArgumentError, 66 | message: "#{inspect(value)} is not a valid aggregation representation" 67 | end 68 | end 69 | 70 | @impl true 71 | def dump(aggregation) when is_atom(aggregation) do 72 | case aggregation do 73 | :individual -> {:ok, @interface_aggregation_individual} 74 | :object -> {:ok, @interface_aggregation_object} 75 | _ -> :error 76 | end 77 | end 78 | 79 | def dump!(aggregation) when is_atom(aggregation) do 80 | case dump(aggregation) do 81 | {:ok, aggregation_int} -> aggregation_int 82 | :error -> raise ArgumentError, message: "#{inspect(aggregation)} is not a valid aggregation" 83 | end 84 | end 85 | 86 | @impl true 87 | def load(aggregation_int) when is_integer(aggregation_int) do 88 | case aggregation_int do 89 | @interface_aggregation_individual -> {:ok, :individual} 90 | @interface_aggregation_object -> {:ok, :object} 91 | _ -> :error 92 | end 93 | end 94 | 95 | def to_int(aggregation) when is_atom(aggregation) do 96 | dump!(aggregation) 97 | end 98 | 99 | def from_int(int) when is_integer(int) do 100 | cast!(int) 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/astarte_core/mapping/database_retention_policy.ex: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2017-2024 SECO Mind Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.Mapping.DatabaseRetentionPolicy do 20 | use Ecto.Type 21 | 22 | @type t :: :no_ttl | :use_ttl 23 | 24 | @mapping_policy_no_ttl 1 25 | @mapping_policy_use_ttl 2 26 | @valid_atoms [ 27 | :no_ttl, 28 | :use_ttl 29 | ] 30 | 31 | @impl true 32 | def type, do: :integer 33 | 34 | @impl true 35 | def cast(nil), do: {:ok, nil} 36 | 37 | def cast(atom) when is_atom(atom) do 38 | if Enum.member?(@valid_atoms, atom) do 39 | {:ok, atom} 40 | else 41 | :error 42 | end 43 | end 44 | 45 | def cast(string) when is_binary(string) do 46 | case string do 47 | "no_ttl" -> {:ok, :no_ttl} 48 | "use_ttl" -> {:ok, :use_ttl} 49 | _ -> :error 50 | end 51 | end 52 | 53 | def cast(int) when is_integer(int) do 54 | load(int) 55 | end 56 | 57 | def cast(_), do: :error 58 | 59 | def cast!(value) do 60 | case cast(value) do 61 | {:ok, policy} -> 62 | policy 63 | 64 | :error -> 65 | raise ArgumentError, 66 | message: "#{inspect(value)} is not a valid database retention policy representation" 67 | end 68 | end 69 | 70 | @impl true 71 | def dump(policy) when is_atom(policy) do 72 | case policy do 73 | :no_ttl -> {:ok, @mapping_policy_no_ttl} 74 | :use_ttl -> {:ok, @mapping_policy_use_ttl} 75 | _ -> :error 76 | end 77 | end 78 | 79 | def dump!(policy) when is_atom(policy) do 80 | case dump(policy) do 81 | {:ok, policy_int} -> 82 | policy_int 83 | 84 | :error -> 85 | raise ArgumentError, 86 | message: "#{inspect(policy)} is not a valid database retention policy" 87 | end 88 | end 89 | 90 | @impl true 91 | def load(policy_int) when is_integer(policy_int) do 92 | case policy_int do 93 | @mapping_policy_no_ttl -> {:ok, :no_ttl} 94 | @mapping_policy_use_ttl -> {:ok, :use_ttl} 95 | _ -> :error 96 | end 97 | end 98 | 99 | def to_int(database_retention_policy) when is_atom(database_retention_policy) do 100 | dump!(database_retention_policy) 101 | end 102 | 103 | def from_int(int) when is_integer(int) do 104 | cast!(int) 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_events/simple_event.pb.ex: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.SimpleEvents.SimpleEvent do 2 | @moduledoc false 3 | 4 | use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 5 | 6 | def fully_qualified_name, do: "SimpleEvent" 7 | 8 | oneof :event, 0 9 | 10 | field :version, 1, type: :int32, deprecated: true 11 | field :simple_trigger_id, 2, proto3_optional: true, type: :bytes, json_name: "simpleTriggerId" 12 | field :parent_trigger_id, 3, proto3_optional: true, type: :bytes, json_name: "parentTriggerId" 13 | field :realm, 4, proto3_optional: true, type: :string 14 | field :device_id, 5, proto3_optional: true, type: :string, json_name: "deviceId" 15 | field :timestamp, 18, proto3_optional: true, type: :int64 16 | 17 | field :device_connected_event, 6, 18 | type: Astarte.Core.Triggers.SimpleEvents.DeviceConnectedEvent, 19 | json_name: "deviceConnectedEvent", 20 | oneof: 0 21 | 22 | field :device_disconnected_event, 7, 23 | type: Astarte.Core.Triggers.SimpleEvents.DeviceDisconnectedEvent, 24 | json_name: "deviceDisconnectedEvent", 25 | oneof: 0 26 | 27 | field :incoming_data_event, 8, 28 | type: Astarte.Core.Triggers.SimpleEvents.IncomingDataEvent, 29 | json_name: "incomingDataEvent", 30 | oneof: 0 31 | 32 | field :value_change_event, 9, 33 | type: Astarte.Core.Triggers.SimpleEvents.ValueChangeEvent, 34 | json_name: "valueChangeEvent", 35 | oneof: 0 36 | 37 | field :value_change_applied_event, 10, 38 | type: Astarte.Core.Triggers.SimpleEvents.ValueChangeAppliedEvent, 39 | json_name: "valueChangeAppliedEvent", 40 | oneof: 0 41 | 42 | field :path_created_event, 11, 43 | type: Astarte.Core.Triggers.SimpleEvents.PathCreatedEvent, 44 | json_name: "pathCreatedEvent", 45 | oneof: 0 46 | 47 | field :path_removed_event, 12, 48 | type: Astarte.Core.Triggers.SimpleEvents.PathRemovedEvent, 49 | json_name: "pathRemovedEvent", 50 | oneof: 0 51 | 52 | field :value_stored_event, 13, 53 | type: Astarte.Core.Triggers.SimpleEvents.ValueStoredEvent, 54 | json_name: "valueStoredEvent", 55 | oneof: 0 56 | 57 | field :incoming_introspection_event, 14, 58 | type: Astarte.Core.Triggers.SimpleEvents.IncomingIntrospectionEvent, 59 | json_name: "incomingIntrospectionEvent", 60 | oneof: 0 61 | 62 | field :interface_added_event, 15, 63 | type: Astarte.Core.Triggers.SimpleEvents.InterfaceAddedEvent, 64 | json_name: "interfaceAddedEvent", 65 | oneof: 0 66 | 67 | field :interface_removed_event, 16, 68 | type: Astarte.Core.Triggers.SimpleEvents.InterfaceRemovedEvent, 69 | json_name: "interfaceRemovedEvent", 70 | oneof: 0 71 | 72 | field :interface_minor_updated_event, 17, 73 | type: Astarte.Core.Triggers.SimpleEvents.InterfaceMinorUpdatedEvent, 74 | json_name: "interfaceMinorUpdatedEvent", 75 | oneof: 0 76 | 77 | field :device_error_event, 19, 78 | type: Astarte.Core.Triggers.SimpleEvents.DeviceErrorEvent, 79 | json_name: "deviceErrorEvent", 80 | oneof: 0 81 | end 82 | -------------------------------------------------------------------------------- /lib/astarte_core/interface/ownership.ex: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2017-2024 SECO Mind Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.Interface.Ownership do 20 | use Ecto.Type 21 | 22 | @type t :: :device | :server 23 | 24 | @interface_ownership_device 1 25 | @interface_ownership_server 2 26 | @valid_atoms [ 27 | :device, 28 | :server 29 | ] 30 | 31 | @impl true 32 | def type, do: :integer 33 | 34 | @impl true 35 | def cast(nil), do: {:ok, nil} 36 | 37 | def cast(atom) when is_atom(atom) do 38 | if Enum.member?(@valid_atoms, atom) do 39 | {:ok, atom} 40 | else 41 | :error 42 | end 43 | end 44 | 45 | def cast(string) when is_binary(string) do 46 | case string do 47 | "device" -> 48 | {:ok, :device} 49 | 50 | "server" -> 51 | {:ok, :server} 52 | 53 | # deprecated names 54 | "producer" -> 55 | {:ok, :device} 56 | 57 | "consumer" -> 58 | {:ok, :server} 59 | 60 | _ -> 61 | :error 62 | end 63 | end 64 | 65 | def cast(int) when is_integer(int) do 66 | load(int) 67 | end 68 | 69 | def cast(_), do: :error 70 | 71 | def cast!(value) do 72 | case cast(value) do 73 | {:ok, ownership} -> 74 | ownership 75 | 76 | :error -> 77 | raise ArgumentError, message: "#{inspect(value)} is not a valid ownership representation" 78 | end 79 | end 80 | 81 | @impl true 82 | def dump(ownership) when is_atom(ownership) do 83 | case ownership do 84 | :device -> {:ok, @interface_ownership_device} 85 | :server -> {:ok, @interface_ownership_server} 86 | _ -> :error 87 | end 88 | end 89 | 90 | def dump!(ownership) when is_atom(ownership) do 91 | case dump(ownership) do 92 | {:ok, ownership_int} -> ownership_int 93 | :error -> raise ArgumentError, message: "#{inspect(ownership)} is not a valid ownership" 94 | end 95 | end 96 | 97 | @impl true 98 | def load(ownership_int) when is_integer(ownership_int) do 99 | case ownership_int do 100 | @interface_ownership_device -> {:ok, :device} 101 | @interface_ownership_server -> {:ok, :server} 102 | _ -> :error 103 | end 104 | end 105 | 106 | def to_int(ownership) when is_atom(ownership) do 107 | dump!(ownership) 108 | end 109 | 110 | def from_int(int) when is_integer(int) do 111 | cast!(int) 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/astarte_core/mapping/retention.ex: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2017-2024 SECO Mind Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.Mapping.Retention do 20 | use Ecto.Type 21 | 22 | @type t :: :discard | :volatile | :stored 23 | 24 | @mapping_retention_discard 1 25 | @mapping_retention_volatile 2 26 | @mapping_retention_stored 3 27 | @valid_atoms [ 28 | :discard, 29 | :volatile, 30 | :stored 31 | ] 32 | 33 | @impl true 34 | def type, do: :integer 35 | 36 | @impl true 37 | def cast(nil), do: {:ok, nil} 38 | 39 | def cast(atom) when is_atom(atom) do 40 | if Enum.member?(@valid_atoms, atom) do 41 | {:ok, atom} 42 | else 43 | :error 44 | end 45 | end 46 | 47 | def cast(string) when is_binary(string) do 48 | case string do 49 | "discard" -> {:ok, :discard} 50 | "volatile" -> {:ok, :volatile} 51 | "stored" -> {:ok, :stored} 52 | _ -> :error 53 | end 54 | end 55 | 56 | def cast(int) when is_integer(int) do 57 | load(int) 58 | end 59 | 60 | def cast(_), do: :error 61 | 62 | def cast!(value) do 63 | case cast(value) do 64 | {:ok, retention} -> 65 | retention 66 | 67 | :error -> 68 | raise ArgumentError, message: "#{inspect(value)} is not a valid retention representation" 69 | end 70 | end 71 | 72 | @impl true 73 | def dump(retention) when is_atom(retention) do 74 | case retention do 75 | :discard -> {:ok, @mapping_retention_discard} 76 | :volatile -> {:ok, @mapping_retention_volatile} 77 | :stored -> {:ok, @mapping_retention_stored} 78 | _ -> :error 79 | end 80 | end 81 | 82 | def dump!(retention) when is_atom(retention) do 83 | case dump(retention) do 84 | {:ok, retention_int} -> retention_int 85 | :error -> raise ArgumentError, message: "#{inspect(retention)} is not a valid retention" 86 | end 87 | end 88 | 89 | @impl true 90 | def load(retention_int) when is_integer(retention_int) do 91 | case retention_int do 92 | @mapping_retention_discard -> {:ok, :discard} 93 | @mapping_retention_volatile -> {:ok, :volatile} 94 | @mapping_retention_stored -> {:ok, :stored} 95 | _ -> :error 96 | end 97 | end 98 | 99 | def to_int(retention) when is_atom(retention) do 100 | dump!(retention) 101 | end 102 | 103 | def from_int(int) when is_integer(int) do 104 | cast!(int) 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/astarte_core/mapping/reliability.ex: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2017-2024 SECO Mind Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.Mapping.Reliability do 20 | use Ecto.Type 21 | 22 | @type t :: :unreliable | :guaranteed | :unique 23 | 24 | @mapping_reliability_unreliable 1 25 | @mapping_reliability_guaranteed 2 26 | @mapping_reliability_unique 3 27 | @valid_atoms [ 28 | :unreliable, 29 | :guaranteed, 30 | :unique 31 | ] 32 | 33 | @impl true 34 | def type, do: :integer 35 | 36 | @impl true 37 | def cast(nil), do: {:ok, nil} 38 | 39 | def cast(atom) when is_atom(atom) do 40 | if Enum.member?(@valid_atoms, atom) do 41 | {:ok, atom} 42 | else 43 | :error 44 | end 45 | end 46 | 47 | def cast(string) when is_binary(string) do 48 | case string do 49 | "unreliable" -> {:ok, :unreliable} 50 | "guaranteed" -> {:ok, :guaranteed} 51 | "unique" -> {:ok, :unique} 52 | _ -> :error 53 | end 54 | end 55 | 56 | def cast(int) when is_integer(int) do 57 | load(int) 58 | end 59 | 60 | def cast(_), do: :error 61 | 62 | def cast!(reliability) do 63 | case cast(reliability) do 64 | {:ok, reliability} -> 65 | reliability 66 | 67 | :error -> 68 | raise ArgumentError, 69 | message: "#{inspect(reliability)} is not a valid reliability representation" 70 | end 71 | end 72 | 73 | @impl true 74 | def dump(reliability) when is_atom(reliability) do 75 | case reliability do 76 | :unreliable -> {:ok, @mapping_reliability_unreliable} 77 | :guaranteed -> {:ok, @mapping_reliability_guaranteed} 78 | :unique -> {:ok, @mapping_reliability_unique} 79 | _ -> :error 80 | end 81 | end 82 | 83 | def dump!(reliability) when is_atom(reliability) do 84 | case dump(reliability) do 85 | {:ok, reliability_int} -> reliability_int 86 | :error -> raise ArgumentError, message: "#{inspect(reliability)} is not a valid reliability" 87 | end 88 | end 89 | 90 | @impl true 91 | def load(reliability_int) when is_integer(reliability_int) do 92 | case reliability_int do 93 | @mapping_reliability_unreliable -> {:ok, :unreliable} 94 | @mapping_reliability_guaranteed -> {:ok, :guaranteed} 95 | @mapping_reliability_unique -> {:ok, :unique} 96 | _ -> :error 97 | end 98 | end 99 | 100 | def to_int(reliability) when is_atom(reliability) do 101 | dump!(reliability) 102 | end 103 | 104 | def from_int(int) when is_integer(int) do 105 | cast!(int) 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/policy/error_type.ex: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2022 SECO Mind srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.Triggers.Policy.ErrorType do 20 | use Ecto.Type 21 | alias Astarte.Core.Triggers.Policy 22 | 23 | @type t :: %{} 24 | 25 | # a Json 26 | def type, do: :map 27 | 28 | def cast(error_type) when is_binary(error_type) do 29 | EctoMorph.generate_changeset(%{keyword: error_type}, Policy.ErrorKeyword) 30 | |> Policy.ErrorKeyword.validate() 31 | |> EctoMorph.into_struct() 32 | |> case do 33 | {:ok, struct} -> {:ok, struct} 34 | # returning changeset.errors ensures the error 35 | # goes on the parent changeset. 36 | {:error, changeset} -> {:error, changeset.errors} 37 | end 38 | end 39 | 40 | def cast(%{"keyword" => error_type}) do 41 | EctoMorph.generate_changeset(%{keyword: error_type}, Policy.ErrorKeyword) 42 | |> Policy.ErrorKeyword.validate() 43 | |> EctoMorph.into_struct() 44 | |> case do 45 | {:ok, struct} -> {:ok, struct} 46 | # returning changeset.errors ensures the error 47 | # goes on the parent changeset. 48 | {:error, changeset} -> {:error, changeset.errors} 49 | end 50 | end 51 | 52 | def cast(error_type) when is_list(error_type) do 53 | EctoMorph.generate_changeset(%{error_codes: error_type}, Policy.ErrorRange) 54 | |> Policy.ErrorRange.validate() 55 | |> EctoMorph.into_struct() 56 | |> case do 57 | {:ok, struct} -> {:ok, struct} 58 | # returning changeset.errors ensures the error 59 | # goes on the parent changeset. 60 | {:error, changeset} -> {:error, changeset.errors} 61 | end 62 | end 63 | 64 | def cast(%{"error_codes" => error_type}) do 65 | EctoMorph.generate_changeset(%{error_codes: error_type}, Policy.ErrorRange) 66 | |> Policy.ErrorRange.validate() 67 | |> EctoMorph.into_struct() 68 | |> case do 69 | {:ok, struct} -> {:ok, struct} 70 | # returning changeset.errors ensures the error 71 | # goes on the parent changeset. 72 | {:error, changeset} -> {:error, changeset.errors} 73 | end 74 | end 75 | 76 | def dump(error_type) when is_binary(error_type) do 77 | EctoMorph.cast_to_struct(%{keyword: error_type}, Policy.ErrorKeyword) 78 | end 79 | 80 | def dump(error_type) when is_list(error_type) do 81 | EctoMorph.cast_to_struct(%{error_codes: error_type}, Policy.ErrorRange) 82 | end 83 | 84 | def load(error_type) when is_binary(error_type) do 85 | EctoMorph.cast_to_struct(%{keyword: error_type}, Policy.ErrorKeyword) 86 | end 87 | 88 | def load(error_type) when is_list(error_type) do 89 | EctoMorph.cast_to_struct(%{error_codes: error_type}, Policy.ErrorRange) 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /specs/policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema", 3 | "title": "Astarte Delivery Policy Schema", 4 | "description": "This schema describes how an Astarte delivery policy should be declared.", 5 | "type": "object", 6 | "properties": { 7 | "policy_name": { 8 | "type": "string", 9 | "pattern": "^(?!@).+$", 10 | "minLength": 1, 11 | "maxLength": 128, 12 | "description": "The name of the delivery policy. This has to be an unique name, shorther than 128 characters." 13 | }, 14 | "handlers": { 15 | "type": "array", 16 | "items": { 17 | "$ref": "#/$defs/handler" 18 | }, 19 | "minItems": 0, 20 | "maxItems": 200, 21 | "uniqueItems": true, 22 | "description": "An handler refers to one or more delivery errors and describes the retry strategy Astarte should take when it/they occur." 23 | }, 24 | "retry_times": { 25 | "type": "integer", 26 | "description": "The minimum amount of times Astarte will try to send an event (first excluded)." 27 | }, 28 | "maximum_capacity": { 29 | "type": "integer", 30 | "description": "The maximum amount of events that can be stored in a queue." 31 | }, 32 | "event_ttl": { 33 | "type": "integer", 34 | "description": "The lifetime of a message, in seconds." 35 | } 36 | }, 37 | "required": [ 38 | "policy_name", 39 | "handlers", 40 | "maximum_capacity" 41 | ], 42 | "$defs": { 43 | "handler": { 44 | "type": "object", 45 | "properties": { 46 | "on": { 47 | "anyOf": [ 48 | { 49 | "type": "array", 50 | "items": { 51 | "type": "number", 52 | "minimum": 400, 53 | "maximum": 599, 54 | "description": "An HTTP error code." 55 | } 56 | }, 57 | { 58 | "const": "client_error", 59 | "description": "Matches HTTP client error codes (400-499)." 60 | }, 61 | { 62 | "const": "server_error", 63 | "description": "Matches HTTP client error codes (500-599)." 64 | }, 65 | { 66 | "const": "any_error", 67 | "description": "Matches HTTP client and server error codes (400-599)." 68 | } 69 | ], 70 | "description": "A class of HTTP errors to which the handler applies." 71 | }, 72 | "strategy": { 73 | "type": "string", 74 | "enum": [ 75 | "discard", 76 | "retry" 77 | ], 78 | "description": "Identifies how errors are handled." 79 | } 80 | }, 81 | "required": [ 82 | "on", 83 | "strategy" 84 | ] 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /.github/workflows/build-workflow.yaml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | # Run when pushing to stable branches 5 | push: 6 | branches: 7 | - 'master' 8 | - 'release-*' 9 | # Run on branch/tag creation 10 | create: 11 | # Run on pull requests 12 | pull_request: 13 | 14 | env: 15 | elixir_version: "1.15.7" 16 | otp_version: "26.1" 17 | 18 | jobs: 19 | test-dialyzer: 20 | name: Check Dialyzer 21 | runs-on: ubuntu-22.04 22 | env: 23 | MIX_ENV: ci 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: actions/cache@v4 27 | with: 28 | path: deps 29 | key: ${{ runner.os }}-${{ env.elixir_version }}-${{ env.otp_version }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} 30 | - uses: actions/cache@v4 31 | with: 32 | path: _build 33 | key: ${{ runner.os }}-${{ env.elixir_version }}-${{ env.otp_version }}-_build-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}-${{ github.sha }} 34 | restore-keys: | 35 | ${{ runner.os }}-${{ env.elixir_version }}-${{ env.otp_version }}-_build-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} 36 | - id: plt_cache 37 | uses: actions/cache@v4 38 | with: 39 | path: dialyzer_cache 40 | key: ${{ runner.os }}-${{ env.elixir_version }}-${{ env.otp_version }}-dialyzer_cache-${{ github.sha }} 41 | restore-keys: | 42 | ${{ runner.os }}-${{ env.elixir_version }}-${{ env.otp_version }}-dialyzer_cache- 43 | - uses: erlef/setup-beam@v1.18 44 | with: 45 | otp-version: ${{ env.otp_version }} 46 | elixir-version: ${{ env.elixir_version }} 47 | - name: Install Dependencies 48 | run: mix deps.get 49 | - name: Create PLTs dir 50 | if: ${{steps.plt_cache.outputs.cache-hit != 'true'}} 51 | run: mkdir -p dialyzer_cache && mix dialyzer --plt 52 | - name: Run dialyzer 53 | # FIXME: This should be set to fail when dialyzer issues are fixed 54 | run: mix dialyzer || exit 0 55 | 56 | test-coverage: 57 | name: Build and Test 58 | runs-on: ubuntu-22.04 59 | # Wait for Dialyzer to give it a go before building 60 | needs: 61 | - test-dialyzer 62 | env: 63 | MIX_ENV: test 64 | steps: 65 | - uses: actions/checkout@v4 66 | - uses: actions/cache@v4 67 | with: 68 | path: deps 69 | key: ${{ runner.os }}-${{ env.elixir_version }}-${{ env.otp_version }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} 70 | - uses: actions/cache@v4 71 | with: 72 | path: _build 73 | key: ${{ runner.os }}-${{ env.elixir_version }}-${{ env.otp_version }}-_build-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}-${{ github.sha }} 74 | restore-keys: | 75 | ${{ runner.os }}-${{ env.elixir_version }}-${{ env.otp_version }}-_build-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} 76 | - uses: erlef/setup-beam@v1.18 77 | with: 78 | otp-version: ${{ env.otp_version }} 79 | elixir-version: ${{ env.elixir_version }} 80 | - name: Install Dependencies 81 | run: mix deps.get 82 | - name: Check formatting 83 | run: mix format --check-formatted 84 | - name: Compile 85 | run: mix compile 86 | - name: Test and Coverage 87 | run: mix coveralls.json --exclude wip -o coverage_results 88 | - uses: codecov/codecov-action@v5 89 | name: Upload Coverage Results to CodeCov 90 | with: 91 | token: ${{ secrets.CODECOV_TOKEN }} 92 | -------------------------------------------------------------------------------- /lib/astarte_core/device.ex: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2017 Ispirata Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.Device do 20 | @moduledoc """ 21 | Utility functions to deal with Astarte devices 22 | """ 23 | 24 | @type device_id :: <<_::128>> 25 | @type encoded_device_id :: String.t() 26 | 27 | @doc """ 28 | Decodes a Base64 url encoded device id and returns it as a 128-bit binary (usable as uuid). 29 | 30 | By default, it will fail with `{:error, :extended_id_not_allowed}` if the size of the encoded device_id is > 128 bit. 31 | You can pass `allow_extended_id: true` as second argument to allow longer device ids (the returned binary will still be 128 bit long, but the function will not return an error and will instead drop the extended id). 32 | 33 | Returns `{:ok, device_id}` or `{:error, reason}`. 34 | """ 35 | @spec decode_device_id(encoded_device_id :: encoded_device_id(), opts :: options) :: 36 | {:ok, device_id :: device_id()} | {:error, atom()} 37 | when options: [option], 38 | option: {:allow_extended_id, boolean()} 39 | def decode_device_id(encoded_device_id, opts \\ []) 40 | when is_binary(encoded_device_id) and is_list(opts) do 41 | allow_extended = Keyword.get(opts, :allow_extended_id, false) 42 | 43 | with {:ok, device_id, extended_id} <- decode_extended_device_id(encoded_device_id) do 44 | if not allow_extended and byte_size(extended_id) > 0 do 45 | {:error, :extended_id_not_allowed} 46 | else 47 | {:ok, device_id} 48 | end 49 | end 50 | end 51 | 52 | @doc """ 53 | Decodes an extended Base64 url encoded device id. 54 | 55 | Returns `{:ok, device_id, extended_id}` (where `device_id` is a binary with the first 128 bits of the decoded id and `extended_id` the rest of the decoded binary) or `{:error, reason}`. 56 | """ 57 | @spec decode_extended_device_id(encoded_device_id :: encoded_device_id()) :: 58 | {:ok, device_id :: device_id(), extended_id :: binary()} | {:error, atom()} 59 | def decode_extended_device_id(encoded_device_id) when is_binary(encoded_device_id) do 60 | with {:ok, decoded} <- Base.url_decode64(encoded_device_id, padding: false), 61 | <> <- decoded do 62 | {:ok, device_id, extended_id} 63 | else 64 | _ -> 65 | {:error, :invalid_device_id} 66 | end 67 | end 68 | 69 | @doc """ 70 | Encodes a device id with the standard encoding (Base64 url encoding, no padding). The device id must be exactly 16 bytes (128 bits) long. 71 | 72 | Returns the encoded device id. 73 | """ 74 | @spec encode_device_id(device_id :: device_id()) :: encoded_device_id :: encoded_device_id() 75 | def encode_device_id(device_id) when is_binary(device_id) and byte_size(device_id) == 16 do 76 | Base.url_encode64(device_id, padding: false) 77 | end 78 | 79 | @doc """ 80 | Generate a random Astarte device id. 81 | 82 | The generated device id is also a valid UUID v4. 83 | """ 84 | @spec random_device_id :: device_id :: device_id() 85 | def random_device_id do 86 | <> = :crypto.strong_rand_bytes(16) 87 | <> 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /specs/interface.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema", 3 | "title": "Astarte Interface Schema", 4 | "description" : "This schema describes how an Astarte interface should be declared", 5 | "type": "object", 6 | "properties": { 7 | "interface_name": { 8 | "type": "string", 9 | "pattern": "^([a-zA-Z][a-zA-Z0-9]*.([a-zA-Z0-9][a-zA-Z0-9-]*.)*)?[a-zA-Z][a-zA-Z0-9]*$", 10 | "minLength": 1, 11 | "maxLength": 128, 12 | "description": "The name of the interface. This has to be an unique, alphanumeric reverse internet domain name, shorther than 128 characters." 13 | }, 14 | "version_major": { 15 | "type": "integer", 16 | "description": "A Major version qualifier for this interface. Interfaces with the same id and different version_major number are deemed incompatible. It is then acceptable to redefine any property of the interface when changing the major version number." 17 | }, 18 | "version_minor": { 19 | "type": "integer", 20 | "description": "A Minor version qualifier for this interface. Interfaces with the same id and major version number and different version_minor number are deemed compatible between each other. When changing the minor number, it is then only possible to insert further mappings. Any other modification might lead to incompatibilities and undefined behavior." 21 | }, 22 | "type": { 23 | "type": "string", 24 | "enum": ["datastream", "properties"], 25 | "description": "Identifies the type of this Interface. Currently two types are supported: datastream and properties. datastream should be used when dealing with streams of non-persistent data, where a single path receives updates and there's no concept of state. properties, instead, are meant to be an actual state and as such they have only a change history, and are retained." 26 | }, 27 | "ownership": { 28 | "type": "string", 29 | "enum": ["device", "server"], 30 | "description": "Identifies the ownership of the interface. Interfaces are meant to be unidirectional, and this property defines who's sending or receiving data. device means the device/gateway is sending data to Astarte, server means the device/gateway is receiving data from Astarte. Bidirectional mode is not supported, you should instantiate another interface for that." 31 | }, 32 | "aggregation": { 33 | "type": "string", 34 | "enum": ["individual", "object"], 35 | "default": "individual", 36 | "description": "Identifies the aggregation of the mappings of the interface. Individual means every mapping changes state or streams data independently, whereas an object aggregation treats the interface as an object, making all the mappings changes interdependent. Choosing the right aggregation might drastically improve performances." 37 | }, 38 | "description": { 39 | "type": "string", 40 | "description": "An optional description of the interface." 41 | }, 42 | "doc": { 43 | "type": "string", 44 | "description": "A string containing documentation that will be injected in the generated client code." 45 | }, 46 | "mappings": { 47 | "type": "array", 48 | "description": "Mappings define the endpoint of the interface, where actual data is stored/streamed. They are defined as relative URLs (e.g. /my/path) and can be parametrized (e.g.: /%{myparam}/path). A valid interface must have no mappings clash, which means that every mapping must resolve to a unique path or collection of paths (including parametrization). Every mapping acquires type, quality and aggregation of the interface.", 49 | "items": {"$ref": "mapping.json"}, 50 | "minItems": 1, 51 | "maxItems": 1024, 52 | "uniqueItems": true 53 | } 54 | }, 55 | "required": ["interface_name", "version_minor", "version_major", "type", "ownership", "mappings"] 56 | } 57 | -------------------------------------------------------------------------------- /lib/astarte_core/storage_type.ex: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2017-2024 SECO Mind Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.StorageType do 20 | use Ecto.Type 21 | 22 | @type t :: 23 | :multi_interface_individual_properties_dbtable 24 | | :multi_interface_individual_datastream_dbtable 25 | | :one_individual_properties_dbtable 26 | | :one_individual_datastream_dbtable 27 | | :one_object_datastream_dbtable 28 | 29 | @multi_interface_individual_properties_dbtable 1 30 | @multi_interface_individual_datastream_dbtable 2 31 | @one_individual_properties_dbtable 3 32 | @one_individual_datastream_dbtable 4 33 | @one_object_datastream_dbtable 5 34 | @valid_atoms [ 35 | :multi_interface_individual_properties_dbtable, 36 | :multi_interface_individual_datastream_dbtable, 37 | :one_individual_properties_dbtable, 38 | :one_individual_datastream_dbtable, 39 | :one_object_datastream_dbtable 40 | ] 41 | 42 | @impl true 43 | def type, do: :integer 44 | 45 | @impl true 46 | def cast(nil), do: {:ok, nil} 47 | 48 | def cast(atom) when is_atom(atom) do 49 | if Enum.member?(@valid_atoms, atom) do 50 | {:ok, atom} 51 | else 52 | :error 53 | end 54 | end 55 | 56 | def cast(int) when is_integer(int) do 57 | load(int) 58 | end 59 | 60 | def cast(_), do: :error 61 | 62 | def cast!(value) do 63 | case cast(value) do 64 | {:ok, value} -> 65 | value 66 | 67 | :error -> 68 | raise ArgumentError, 69 | message: "#{inspect(value)} is not a valid storage type representation" 70 | end 71 | end 72 | 73 | @impl true 74 | def dump(storage_type) when is_atom(storage_type) do 75 | case storage_type do 76 | :multi_interface_individual_properties_dbtable -> 77 | {:ok, @multi_interface_individual_properties_dbtable} 78 | 79 | :multi_interface_individual_datastream_dbtable -> 80 | {:ok, @multi_interface_individual_datastream_dbtable} 81 | 82 | :one_individual_properties_dbtable -> 83 | {:ok, @one_individual_properties_dbtable} 84 | 85 | :one_individual_datastream_dbtable -> 86 | {:ok, @one_individual_datastream_dbtable} 87 | 88 | :one_object_datastream_dbtable -> 89 | {:ok, @one_object_datastream_dbtable} 90 | 91 | _ -> 92 | :error 93 | end 94 | end 95 | 96 | def dump!(storage_type) when is_atom(storage_type) do 97 | case dump(storage_type) do 98 | {:ok, storage_type_int} -> 99 | storage_type_int 100 | 101 | :error -> 102 | raise ArgumentError, message: "#{inspect(storage_type)} is not a valid storage type" 103 | end 104 | end 105 | 106 | @impl true 107 | def load(storage_type_int) when is_integer(storage_type_int) do 108 | case storage_type_int do 109 | @multi_interface_individual_properties_dbtable -> 110 | {:ok, :multi_interface_individual_properties_dbtable} 111 | 112 | @multi_interface_individual_datastream_dbtable -> 113 | {:ok, :multi_interface_individual_datastream_dbtable} 114 | 115 | @one_individual_properties_dbtable -> 116 | {:ok, :one_individual_properties_dbtable} 117 | 118 | @one_individual_datastream_dbtable -> 119 | {:ok, :one_individual_datastream_dbtable} 120 | 121 | @one_object_datastream_dbtable -> 122 | {:ok, :one_object_datastream_dbtable} 123 | 124 | _ -> 125 | :error 126 | end 127 | end 128 | 129 | def to_int(storage_type) when is_atom(storage_type) do 130 | dump!(storage_type) 131 | end 132 | 133 | def from_int(int) when is_integer(int) do 134 | cast!(int) 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /test/astarte_core/interface_descriptor_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.InterfaceDescriptorTest do 2 | use ExUnit.Case 3 | 4 | alias Astarte.Core.CQLUtils 5 | alias Astarte.Core.InterfaceDescriptor 6 | alias Astarte.Core.Interface.Type 7 | alias Astarte.Core.Interface.Ownership 8 | alias Astarte.Core.Interface.Aggregation 9 | alias Astarte.Core.StorageType 10 | 11 | @interface_fixture_name "com.ispirata.Hemera.Test" 12 | @interface_fixture_maj 1 13 | @interface_fixture_min 2 14 | @interface_fixture_type :properties 15 | @interface_fixture_type_as_int Type.to_int(@interface_fixture_type) 16 | @interface_fixture_ownership :server 17 | @interface_fixture_ownership_as_int Ownership.to_int(@interface_fixture_ownership) 18 | @interface_fixture_aggregation :individual 19 | @interface_fixture_aggregation_as_int Aggregation.to_int(@interface_fixture_aggregation) 20 | @interface_fixture_storage "storage" 21 | @interface_fixture_storage_type :multi_interface_individual_properties_dbtable 22 | @interface_fixture_storage_type_as_int StorageType.to_int(@interface_fixture_storage_type) 23 | 24 | @interface_descriptor_fixture %InterfaceDescriptor{ 25 | name: @interface_fixture_name, 26 | major_version: @interface_fixture_maj, 27 | minor_version: @interface_fixture_min, 28 | type: @interface_fixture_type, 29 | ownership: @interface_fixture_ownership, 30 | aggregation: @interface_fixture_aggregation, 31 | storage: @interface_fixture_storage, 32 | storage_type: @interface_fixture_storage_type, 33 | automaton: {%{}, %{}}, 34 | interface_id: CQLUtils.interface_id(@interface_fixture_name, @interface_fixture_maj) 35 | } 36 | 37 | test "keyword list result deserialization" do 38 | descriptor_as_keyword_list = [ 39 | name: @interface_fixture_name, 40 | major_version: @interface_fixture_maj, 41 | minor_version: @interface_fixture_min, 42 | type: @interface_fixture_type_as_int, 43 | ownership: @interface_fixture_ownership_as_int, 44 | aggregation: @interface_fixture_aggregation_as_int, 45 | storage: @interface_fixture_storage, 46 | storage_type: @interface_fixture_storage_type_as_int, 47 | automaton_transitions: :erlang.term_to_binary(%{}), 48 | automaton_accepting_states: :erlang.term_to_binary(%{}), 49 | interface_id: CQLUtils.interface_id(@interface_fixture_name, @interface_fixture_maj) 50 | ] 51 | 52 | assert InterfaceDescriptor.from_db_result!(descriptor_as_keyword_list) == 53 | @interface_descriptor_fixture 54 | end 55 | 56 | test "keyword list deserialization fails if keys are missing" do 57 | descriptor_as_keyword_list_no_aggr = [ 58 | name: @interface_fixture_name, 59 | major_version: @interface_fixture_maj, 60 | minor_version: @interface_fixture_min, 61 | type: @interface_fixture_type_as_int, 62 | ownership: @interface_fixture_ownership_as_int 63 | # Missing aggregation 64 | ] 65 | 66 | assert_raise ArgumentError, fn -> 67 | InterfaceDescriptor.from_db_result!(descriptor_as_keyword_list_no_aggr) 68 | end 69 | end 70 | 71 | test "map result deserialization" do 72 | descriptor_as_map = %{ 73 | name: @interface_fixture_name, 74 | major_version: @interface_fixture_maj, 75 | minor_version: @interface_fixture_min, 76 | type: @interface_fixture_type_as_int, 77 | ownership: @interface_fixture_ownership_as_int, 78 | aggregation: @interface_fixture_aggregation_as_int, 79 | storage: @interface_fixture_storage, 80 | storage_type: @interface_fixture_storage_type_as_int, 81 | automaton_transitions: :erlang.term_to_binary(%{}), 82 | automaton_accepting_states: :erlang.term_to_binary(%{}), 83 | interface_id: CQLUtils.interface_id(@interface_fixture_name, @interface_fixture_maj) 84 | } 85 | 86 | assert InterfaceDescriptor.from_db_result!(descriptor_as_map) == @interface_descriptor_fixture 87 | end 88 | 89 | test "map deserialization fails if keys are missing" do 90 | descriptor_as_map_no_name = %{ 91 | # Missing name 92 | major_version: @interface_fixture_maj, 93 | minor_version: @interface_fixture_min, 94 | type: @interface_fixture_type_as_int, 95 | ownership: @interface_fixture_ownership_as_int, 96 | aggregation: @interface_fixture_aggregation_as_int 97 | } 98 | 99 | assert_raise ArgumentError, fn -> 100 | InterfaceDescriptor.from_db_result!(descriptor_as_map_no_name) 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/astarte_core/interface_descriptor.ex: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2017-2024 SECO Mind Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.InterfaceDescriptor do 20 | use TypedStruct 21 | 22 | alias Astarte.Core.Interface 23 | alias Astarte.Core.Interface.Aggregation 24 | alias Astarte.Core.Interface.Ownership 25 | alias Astarte.Core.Interface.Type 26 | alias Astarte.Core.InterfaceDescriptor 27 | alias Astarte.Core.StorageType 28 | 29 | typedstruct do 30 | field :name, String.t(), default: "" 31 | field :major_version, non_neg_integer(), default: 0 32 | field :minor_version, non_neg_integer(), default: 0 33 | field :type, Type.t() 34 | field :ownership, Ownership.t() 35 | field :aggregation, Aggregation.t() 36 | field :interface_id, :binary 37 | field :automaton, {%{}, %{}} 38 | field :storage, String.t() 39 | field :storage_type, StorageType.t() 40 | end 41 | 42 | @doc """ 43 | Deserializes an `%InterfaceDescriptor{}` from `db_result`. 44 | `db_result` can be a keyword list or a map. 45 | 46 | Returns the `{:ok, %InterfaceDescriptor{}}` on success, 47 | `{:error, :invalid_interface_descriptor_data}` on failure. 48 | """ 49 | def from_db_result(db_result) when not is_map(db_result) do 50 | db_result 51 | |> Enum.into(%{}) 52 | |> from_db_result() 53 | end 54 | 55 | def from_db_result(db_result) do 56 | with %{ 57 | name: name, 58 | major_version: major_version, 59 | minor_version: minor_version, 60 | type: type, 61 | ownership: ownership, 62 | aggregation: aggregation, 63 | automaton_accepting_states: automaton_accepting_states, 64 | automaton_transitions: automaton_transitions, 65 | storage: storage, 66 | storage_type: storage_type, 67 | interface_id: interface_id 68 | } <- db_result do 69 | interface_descriptor = %InterfaceDescriptor{ 70 | name: name, 71 | major_version: major_version, 72 | minor_version: minor_version, 73 | type: Type.cast!(type), 74 | ownership: Ownership.cast!(ownership), 75 | aggregation: Aggregation.cast!(aggregation), 76 | storage: storage, 77 | storage_type: StorageType.cast!(storage_type), 78 | automaton: 79 | {:erlang.binary_to_term(automaton_transitions), 80 | :erlang.binary_to_term(automaton_accepting_states)}, 81 | interface_id: interface_id 82 | } 83 | 84 | {:ok, interface_descriptor} 85 | else 86 | _ -> 87 | {:error, :invalid_interface_descriptor_data} 88 | end 89 | end 90 | 91 | @doc """ 92 | Deserializes an `%InterfaceDescriptor{}` from `db_result`. 93 | `db_result` can be a keyword list or a map. 94 | 95 | Returns the `%InterfaceDescriptor{}` on success, 96 | raises on failure 97 | """ 98 | def from_db_result!(db_result) do 99 | with {:ok, interface_descriptor} <- from_db_result(db_result) do 100 | interface_descriptor 101 | else 102 | _ -> 103 | raise ArgumentError 104 | end 105 | end 106 | 107 | @doc """ 108 | Builds an `%InterfaceDescriptor{}` starting from an `%Interface{}` 109 | 110 | Returns the `%InterfaceDescriptor{}` 111 | """ 112 | def from_interface(%Interface{} = interface) do 113 | %Interface{ 114 | interface_id: interface_id, 115 | name: name, 116 | major_version: major_version, 117 | minor_version: minor_version, 118 | type: type, 119 | ownership: ownership, 120 | aggregation: aggregation 121 | } = interface 122 | 123 | %InterfaceDescriptor{ 124 | interface_id: interface_id, 125 | name: name, 126 | major_version: major_version, 127 | minor_version: minor_version, 128 | type: type, 129 | ownership: ownership, 130 | aggregation: aggregation 131 | } 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /specs/mapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema", 3 | "title": "Astarte Mapping Schema", 4 | "type": "object", 5 | "description": "Identifies a mapping for an interface. A mapping must consist at least of an endpoint and a type.", 6 | "properties": { 7 | "endpoint": { 8 | "type": "string", 9 | "pattern": "^(/(%{([a-zA-Z_][a-zA-Z0-9_]*)}|[a-zA-Z_][a-zA-Z0-9_]*)){1,64}$", 10 | "minLength": 2, 11 | "maxLength": 256, 12 | "description": "The template of the path. This is a UNIX-like path (e.g. /my/path) and can be parametrized. Parameters are in the %{name} form, and can be used to create interfaces which represent dictionaries of mappings. When the interface aggregation is object, an object is composed by all the mappings for one specific parameter combination. /timestamp is a reserved path for timestamps, so every mapping on a datastream must not have any endpoint that ends with /timestamp." 13 | }, 14 | "type": { 15 | "type": "string", 16 | "enum": ["double", "integer", "boolean", "longinteger", 17 | "string", "binaryblob", "datetime", 18 | "doublearray", "integerarray", "booleanarray", "longintegerarray", 19 | "stringarray", "binaryblobarray", "datetimearray"], 20 | "description": "Defines the type of the mapping." 21 | }, 22 | "reliability": { 23 | "type": "string", 24 | "enum": ["unreliable", "guaranteed", "unique"], 25 | "default": "unreliable", 26 | "description": "Useful only with datastream. Defines whether the sent data should be considered delivered when the transport successfully sends the data (unreliable), when we know that the data has been received at least once (guaranteed) or when we know that the data has been received exactly once (unique). unreliable by default. When using reliable data, consider you might incur in additional resource usage on both the transport and the device's end." 27 | }, 28 | "retention": { 29 | "type": "string", 30 | "enum": ["discard", "volatile", "stored"], 31 | "default": "discard", 32 | "description": "Useful only with datastream. Defines whether the sent data should be discarded if the transport is temporarily uncapable of delivering it (discard) or should be kept in a cache in memory (volatile) or on disk (stored), and guaranteed to be delivered in the timeframe defined by the expiry. discard by default." 33 | }, 34 | "expiry": { 35 | "type": "integer", 36 | "default": 0, 37 | "minimum": 0, 38 | "description": "Useful when retention is stored. Defines after how many seconds a specific data entry should be kept before giving up and erasing it from the persistent cache. A value <= 0 means the persistent cache never expires, and is the default." 39 | }, 40 | "database_retention_policy": { 41 | "type": "string", 42 | "enum": ["no_ttl", "use_ttl"], 43 | "default": "no_ttl", 44 | "description": "Useful only with datastream. Defines whether data should expire from the database after a given interval. Valid values are: no_ttl and use_ttl." 45 | }, 46 | "database_retention_ttl": { 47 | "type": "integer", 48 | "minimum": 60, 49 | "maximum": 630719999, 50 | "description": "Useful when database_retention_policy is use_ttl. Defines how many seconds a specific data entry should be kept before erasing it from the database." 51 | }, 52 | "allow_unset": { 53 | "type": "boolean", 54 | "default": false, 55 | "description": "Used only with properties. Used with producers, it generates a method to unset the property. Used with consumers, it generates code to call an unset method when an empty payload is received." 56 | }, 57 | "explicit_timestamp": { 58 | "type": "boolean", 59 | "default": false, 60 | "description": "Allow to set a custom timestamp, otherwise a timestamp is added when the message is received. If true explicit timestamp will also be used for sorting. This feature is only supported on datastreams." 61 | }, 62 | "description": { 63 | "type": "string", 64 | "description": "An optional description of the mapping." 65 | }, 66 | "doc": { 67 | "type": "string", 68 | "description": "A string containing documentation that will be injected in the generated client code." 69 | } 70 | }, 71 | "required": ["endpoint", "type"] 72 | } 73 | -------------------------------------------------------------------------------- /test/astarte_core/mapping/value_type_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Mapping.ValueTypeTest do 2 | use ExUnit.Case 3 | alias Astarte.Core.Mapping.ValueType 4 | 5 | test "valid values are accepted" do 6 | assert ValueType.validate_value(:double, 1.1) == :ok 7 | assert ValueType.validate_value(:double, 1.0) == :ok 8 | assert ValueType.validate_value(:double, 1) == :ok 9 | assert ValueType.validate_value(:double, 0xCAFECAFECAFE) == :ok 10 | 11 | assert ValueType.validate_value(:integer, 15) == :ok 12 | assert ValueType.validate_value(:integer, 0x7FFFFFFF) == :ok 13 | 14 | assert ValueType.validate_value(:longinteger, 0xCAFECAFECAFE) == :ok 15 | 16 | assert ValueType.validate_value(:string, "Astarte です") == :ok 17 | 18 | assert ValueType.validate_value(:boolean, true) == :ok 19 | assert ValueType.validate_value(:boolean, false) == :ok 20 | 21 | assert ValueType.validate_value(:binaryblob, <<0, 1, 2, 3, 4>>) == :ok 22 | 23 | assert ValueType.validate_value(:binaryblob, {0, <<0, 1, 2, 3, 4>>}) == :ok 24 | 25 | assert ValueType.validate_value(:binaryblob, %Cyanide.Binary{ 26 | subtype: :generic, 27 | data: <<0, 1, 2, 3, 4>> 28 | }) == :ok 29 | 30 | assert ValueType.validate_value(:datetime, 1_538_131_554_304) == :ok 31 | assert ValueType.validate_value(:datetime, DateTime.utc_now()) == :ok 32 | 33 | assert ValueType.validate_value(:doublearray, [1.0, 1.1, 1.2, 2]) == :ok 34 | assert ValueType.validate_value(:integerarray, [0, 1, 2, 3, 4, 5]) == :ok 35 | assert ValueType.validate_value(:longintegerarray, [0, 1, 2, 3, 4, 5]) == :ok 36 | assert ValueType.validate_value(:stringarray, ["Hello", "World"]) == :ok 37 | assert ValueType.validate_value(:booleanarray, [true, false]) == :ok 38 | assert ValueType.validate_value(:binaryblobarray, ["Hello", <<0, 1, 2>>]) == :ok 39 | 40 | assert ValueType.validate_value(:datetimearray, [1_538_131_554_304, 1_538_131_554_305]) == :ok 41 | end 42 | 43 | test "invalid values are not accepted" do 44 | assert ValueType.validate_value(:double, true) == {:error, :unexpected_value_type} 45 | assert ValueType.validate_value(:double, "1.0") == {:error, :unexpected_value_type} 46 | 47 | assert ValueType.validate_value(:integer, 2.7) == {:error, :unexpected_value_type} 48 | 49 | assert ValueType.validate_value(:integer, 0xCAFECAFECAFE) == {:error, :unexpected_value_type} 50 | 51 | assert ValueType.validate_value(:longinteger, 1.1) == {:error, :unexpected_value_type} 52 | 53 | assert ValueType.validate_value(:longinteger, 0xCAFECAFECAFECAFECAFECAFE) == 54 | {:error, :unexpected_value_type} 55 | 56 | assert ValueType.validate_value(:string, <<0xFFFF::16>>) == {:error, :unexpected_value_type} 57 | 58 | assert ValueType.validate_value(:string, :not_a_string) == {:error, :unexpected_value_type} 59 | 60 | assert ValueType.validate_value(:boolean, 5) == {:error, :unexpected_value_type} 61 | 62 | assert ValueType.validate_value(:boolean, :not_boolean) == {:error, :unexpected_value_type} 63 | 64 | assert ValueType.validate_value(:boolean, nil) == {:error, :unexpected_value_type} 65 | assert ValueType.validate_value(:boolean, "true") == {:error, :unexpected_value_type} 66 | 67 | assert ValueType.validate_value(:binaryblob, 9) == {:error, :unexpected_value_type} 68 | 69 | longbin = 70 | Stream.cycle([<<42>>]) 71 | |> Enum.take(65537) 72 | |> IO.iodata_to_binary() 73 | 74 | assert ValueType.validate_value(:binaryblob, {0, longbin}) == 75 | {:error, :value_size_exceeded} 76 | 77 | assert ValueType.validate_value(:binaryblob, %Cyanide.Binary{subtype: :generic, data: longbin}) == 78 | {:error, :value_size_exceeded} 79 | 80 | assert ValueType.validate_value(:datetime, 22.3) == {:error, :unexpected_value_type} 81 | assert ValueType.validate_value(:datetime, :not_a_date) == {:error, :unexpected_value_type} 82 | 83 | assert ValueType.validate_value(:doublearray, [1.0, :a, 1.2, 2]) == 84 | {:error, :unexpected_value_type} 85 | 86 | assert ValueType.validate_value(:integerarray, [0, 1, 2.1, 3, 4, 5]) == 87 | {:error, :unexpected_value_type} 88 | 89 | assert ValueType.validate_value(:longintegerarray, [0, 1, 2.4, 3, 4, 5]) == 90 | {:error, :unexpected_value_type} 91 | 92 | assert ValueType.validate_value(:stringarray, ["Hello", 5]) == 93 | {:error, :unexpected_value_type} 94 | 95 | assert ValueType.validate_value(:booleanarray, [true, nil]) == 96 | {:error, :unexpected_value_type} 97 | 98 | assert ValueType.validate_value(:binaryblobarray, ["Hello", 4]) == 99 | {:error, :unexpected_value_type} 100 | 101 | assert ValueType.validate_value(:datetimearray, [1_538_131_554_304, false]) == 102 | {:error, :unexpected_value_type} 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/policy/handler.ex: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2022 SECO Mind srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.Triggers.Policy.Handler do 20 | use TypedEctoSchema 21 | import Ecto.Changeset 22 | alias Astarte.Core.Triggers.Policy 23 | alias Astarte.Core.Triggers.Policy.Handler 24 | alias Astarte.Core.Triggers.PolicyProtobuf.ErrorKeyword, as: ErrorKeywordProto 25 | alias Astarte.Core.Triggers.PolicyProtobuf.ErrorRange, as: ErrorRangeProto 26 | alias Astarte.Core.Triggers.PolicyProtobuf.Handler, as: HandlerProto 27 | 28 | @required_fields [ 29 | :on, 30 | :strategy 31 | ] 32 | 33 | @error_keyword_string_to_atom %{ 34 | "any_error" => :ANY_ERROR, 35 | "client_error" => :CLIENT_ERROR, 36 | "server_error" => :SERVER_ERROR 37 | } 38 | @error_keyword_atom_to_string %{ 39 | :ANY_ERROR => "any_error", 40 | :CLIENT_ERROR => "client_error", 41 | :SERVER_ERROR => "server_error" 42 | } 43 | 44 | @strategy_string_to_atom %{ 45 | "discard" => :DISCARD, 46 | "retry" => :RETRY 47 | } 48 | 49 | @strategy_atom_to_string %{ 50 | :DISCARD => "discard", 51 | :RETRY => "retry" 52 | } 53 | 54 | @derive Jason.Encoder 55 | @primary_key false 56 | typed_embedded_schema do 57 | field :on, Policy.ErrorType 58 | field :strategy, :string, default: "discard" 59 | end 60 | 61 | def changeset(%Handler{} = handler, params \\ %{}) do 62 | handler 63 | |> cast(params, @required_fields) 64 | |> validate_required(@required_fields) 65 | |> validate_inclusion(:strategy, Map.keys(@strategy_string_to_atom)) 66 | end 67 | 68 | def error_set(%Handler{on: error_type}) do 69 | values = 70 | case error_type do 71 | %Policy.ErrorKeyword{keyword: "any_error"} -> 400..599 72 | %Policy.ErrorKeyword{keyword: "client_error"} -> 400..499 73 | %Policy.ErrorKeyword{keyword: "server_error"} -> 500..599 74 | %Policy.ErrorRange{error_codes: errs} -> errs 75 | _ -> [] 76 | end 77 | 78 | MapSet.new(values) 79 | end 80 | 81 | def includes_any?(%Handler{on: error_type}, errors) do 82 | Enum.any?(errors, &includes?(error_type, &1)) 83 | end 84 | 85 | def includes?(%Handler{on: error_type}, error) do 86 | case {error_type, error} do 87 | {%Policy.ErrorKeyword{keyword: "any_error"}, e} when e >= 400 and e <= 599 -> 88 | true 89 | 90 | {%Policy.ErrorKeyword{keyword: "client_error"}, e} when e >= 400 and e <= 499 -> 91 | true 92 | 93 | {%Policy.ErrorKeyword{keyword: "server_error"}, e} when e >= 500 and e <= 599 -> 94 | true 95 | 96 | {%Policy.ErrorRange{error_codes: codes}, e} when e >= 400 and e <= 599 -> 97 | Enum.member?(codes, error) 98 | 99 | _ -> 100 | false 101 | end 102 | end 103 | 104 | def discards?(%Handler{strategy: "discard"}) do 105 | true 106 | end 107 | 108 | def discards?(%Handler{}) do 109 | false 110 | end 111 | 112 | def to_handler_proto(%Handler{} = handler) do 113 | %Handler{ 114 | on: error_type, 115 | strategy: strategy 116 | } = handler 117 | 118 | HandlerProto.new( 119 | strategy: Map.get(@strategy_string_to_atom, strategy), 120 | on: error_type_to_tagged_error_tuple(error_type) 121 | ) 122 | end 123 | 124 | def from_handler_proto(%HandlerProto{} = handler_proto) do 125 | %HandlerProto{ 126 | on: tagged_error_tuple, 127 | strategy: strategy 128 | } = handler_proto 129 | 130 | %Handler{ 131 | on: tagged_error_tuple_to_error_type(tagged_error_tuple), 132 | strategy: Map.get(@strategy_atom_to_string, strategy) 133 | } 134 | end 135 | 136 | defp error_type_to_tagged_error_tuple(%Policy.ErrorKeyword{keyword: keyword}) do 137 | {:error_keyword, 138 | ErrorKeywordProto.new(keyword: Map.get(@error_keyword_string_to_atom, keyword))} 139 | end 140 | 141 | defp error_type_to_tagged_error_tuple(%Policy.ErrorRange{error_codes: codes}) do 142 | {:error_range, ErrorRangeProto.new(error_codes: codes)} 143 | end 144 | 145 | defp tagged_error_tuple_to_error_type({_, %ErrorKeywordProto{keyword: keyword}}) do 146 | keyword_string = Map.get(@error_keyword_atom_to_string, keyword) 147 | %Policy.ErrorKeyword{keyword: keyword_string} 148 | end 149 | 150 | defp tagged_error_tuple_to_error_type({_, %ErrorRangeProto{error_codes: error_codes}}) do 151 | %Policy.ErrorRange{error_codes: error_codes} 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [1.2.0] - 2024-07-01 10 | 11 | ## [1.2.0-rc.0] - 2024-05-28 12 | ### Added 13 | - Function for translating realm to keyspace, to support multiple Astarte 14 | instances sharing the same database 15 | - Added device capabilities 16 | - Added device capability `purge_property_compression_format` 17 | 18 | ### Changed 19 | - Bump Elixir to 1.15.7. 20 | - Bump Erlang/OTP to 26.1. 21 | - IncomingIntrospectionEvent holds now a interface-name -> {major, minor} map 22 | instead of the plain introspection string. 23 | 24 | ## [1.1.1] - 2023-10-03 25 | ### Fixed 26 | - Handle Cyanide 2.0 binaries correctly. Fix #95. 27 | - Correctly encode binaryblobarrays in JSON payload of Astarte events. 28 | 29 | ## [1.1.0] - 2023-06-20 30 | ### Fixed 31 | - Forward ported changes from 1.0.5 (Do not allow mappings where `database_retention_policy`...) 32 | 33 | ## [1.1.0-rc.0] - 2023-06-08 34 | ### Changed 35 | - Bump Elixir and Erlang to 1.14.5 and 25.3.2, respectively. 36 | 37 | ## [1.1.0-alpha.0] - 2022-11-14 38 | ### Changed 39 | - Extend interface mappings charset to support name prefixed with underscore 40 | - Introspection triggers are part of device triggers. Expose an API closer to other triggers. 41 | 42 | ## [1.0.6] - 2024-04-18 43 | 44 | ## [1.0.5] - 2023-09-25 45 | ### Fixed 46 | - Do not allow mappings where `database_retention_policy` is 47 | `use_ttl` but no ttl is set. Fix #84. 48 | 49 | ## [1.0.4] - 2022-09-26 50 | ### Added 51 | - Add delivery policies to triggers. 52 | 53 | ## [1.0.3] - 2022-07-04 54 | 55 | ## [1.0.2] - 2022-03-29 56 | 57 | ## [1.0.1] - 2021-12-16 58 | ### Added 59 | - Handle array values when decoding simple events 60 | 61 | ### Fixed 62 | - Don't treat structs as object aggregations when decoding simple events 63 | 64 | ## [1.0.0] - 2021-06-28 65 | 66 | ## [1.0.0-rc.0] - 2021-05-05 67 | 68 | ## [1.0.0-beta.2] - 2021-03-23 69 | ### Changed 70 | - Update dependencies and Elixir version to 1.11 71 | - If `database_retention_policy` is set to `:no_ttl`, `database_retention_ttl` must not be set. (See #51) 72 | 73 | ### Fixed 74 | - Correctly handle SimpleEvents JSON encoding even when they contain an object aggregation with a 75 | binaryblob value. 76 | 77 | ## [1.0.0-beta.1] - 2021-02-11 78 | ### Fixed 79 | - Return an error instead of crashing when the endpoint is not present within a mapping. 80 | 81 | ## [1.0.0-alpha.1] - 2020-06-18 82 | ### Added 83 | - Add `exchange` to `AMQPTriggerTarget` proto. This will allow to send events to any user defined 84 | AMQP exchange (see [#351](https://github.com/astarte-platform/astarte/issues/351)). 85 | - Add additional options to `AMQPTriggerTarget` such as `priority`, `expiration` and `persistent`. 86 | - Add support for device-specific and group-specific triggers. 87 | - Add `DeviceErrorEvent` to `SimpleEvents`, allowing to react to a device error. 88 | 89 | ### Changed 90 | - It is now possible to omit the `device_id` in a `device_trigger`. This is equivalent to passing 91 | `*` as `device_id`. The old behaviour is still supported. 92 | 93 | ## [0.11.4] - 2021-01-25 94 | ### Fixed 95 | - Correctly handle binaryblob value deserialization in events. 96 | 97 | ## [0.11.3] - 2020-09-24 98 | 99 | ## [0.11.2] - 2020-08-14 100 | ### Changed 101 | - Test against Elixir 1.8.2. 102 | 103 | ## [0.11.1] - 2020-05-18 104 | 105 | ## [0.11.0] - 2020-04-06 106 | 107 | ## [0.11.0-rc.1] - 2020-03-25 108 | 109 | ## [0.11.0-rc.0] - 2020-02-26 110 | ### Changed 111 | - Add support for aggregated server owned interfaces. 112 | 113 | ### Fixed 114 | - Correctly handle parametric endpoints regardless of the ordering, so that overlapping endpoints are always refused. (See #2) 115 | 116 | ## [0.11.0-beta.2] - 2020-01-24 117 | ### Changed 118 | - Restrict the use of `*` as `interface_name` only to `incoming_data` data triggers. 119 | - Allow hyphens in `interface_name`. Both the top level domain and the last domain component 120 | must not contain hyphens. ([#7](https://github.com/astarte-platform/astarte_core/issues/7)) 121 | 122 | ### Fixed 123 | - Handle empty `bson_value` in `Triggers.SimpleEvents.Encoder`, avoiding crashes when an empty bson 124 | value is sent as event (e.g. unset). 125 | 126 | ## [0.11.0-beta.1] - 2019-12-24 127 | ### Changed 128 | - `astarte`, `system` and all names starting with `system_` are now reserved realm names. 129 | - Add `database_retention_policy` and `database_retention_ttl` mapping attributes. 130 | 131 | ## [0.10.2] - 2019-12-09 132 | ### Added 133 | - Add timestamp field to SimpleEvent protobuf. 134 | 135 | ## [0.10.1] - 2019-10-02 136 | ### Fixed 137 | - Fix interface validation: object aggregated properties interfaces are not valid. 138 | - Fix interface validation: server owned object aggregated interfaces are not yet supported, hence not valid. 139 | 140 | ## [0.10.0] - 2019-04-16 141 | 142 | ## [0.10.0-rc.0] - 2019-04-03 143 | ### Fixed 144 | - Fix endpoint placeholder regex used in Mapping.normalize_endpoint. 145 | - Fix overlapping endpoints detection, it was allowing some corner case overlappings. 146 | 147 | ## [0.10.0-beta.3] - 2018-12-19 148 | ### Changed 149 | - Interface name `device` is reserved now. 150 | 151 | ### Fixed 152 | - Correctly support Bson.Bin struct. 153 | - False positive overlapping endpoints were detected, EndpointsAutomaton now handles them as valid. 154 | - Correctly serialize triggers on the special "*" interface and device. 155 | 156 | ## [0.10.0-beta.2] - 2018-10-19 157 | ### Added 158 | - Add limit to 64K for string and blobs, 1024 items for arrays. 159 | - Add value validation code for any Astarte type. 160 | 161 | ## [0.10.0-beta.1] - 2018-08-10 162 | ### Added 163 | - First Astarte release. 164 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/simple_triggers_protobuf/utils.ex: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2017 Ispirata Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.Triggers.SimpleTriggersProtobuf.Utils do 20 | alias Astarte.Core.CQLUtils 21 | alias Astarte.Core.Mapping 22 | alias Astarte.Core.Triggers.DataTrigger 23 | 24 | alias Astarte.Core.Triggers.SimpleTriggersProtobuf.DataTrigger, 25 | as: SimpleTriggersProtobufDataTrigger 26 | 27 | alias Astarte.Core.Triggers.SimpleTriggersProtobuf.TriggerTargetContainer 28 | alias Astarte.Core.Triggers.SimpleTriggersProtobuf.SimpleTriggerContainer 29 | alias Astarte.Core.Triggers.SimpleTriggersProtobuf.AMQPTriggerTarget 30 | 31 | @any_device_object_id <<140, 77, 4, 17, 75, 202, 11, 92, 131, 72, 15, 167, 65, 149, 191, 244>> 32 | @any_interface_object_id <<247, 238, 60, 243, 184, 175, 236, 43, 25, 242, 126, 91, 253, 141, 17, 33 | 119>> 34 | @groups_namespace <<36, 234, 86, 36, 135, 212, 64, 186, 187, 188, 84, 47, 123, 78, 154, 123>> 35 | @device_object_type_int 1 36 | @interface_object_type_int 2 37 | @any_interface_object_type_int 3 38 | @any_device_object_type_int 4 39 | @group_object_type_int 5 40 | @group_and_interface_object_type_int 6 41 | @device_and_interface_object_type_int 7 42 | @group_and_any_interface_object_type_int 8 43 | @device_and_any_interface_object_type_int 9 44 | 45 | def any_interface_object_id do 46 | @any_interface_object_id 47 | end 48 | 49 | def any_device_object_id do 50 | @any_device_object_id 51 | end 52 | 53 | def object_type_to_int!(:device), do: @device_object_type_int 54 | def object_type_to_int!(:interface), do: @interface_object_type_int 55 | def object_type_to_int!(:any_device), do: @any_device_object_type_int 56 | def object_type_to_int!(:any_interface), do: @any_interface_object_type_int 57 | def object_type_to_int!(:group), do: @group_object_type_int 58 | def object_type_to_int!(:group_and_interface), do: @group_and_interface_object_type_int 59 | def object_type_to_int!(:device_and_interface), do: @device_and_interface_object_type_int 60 | def object_type_to_int!(:group_and_any_interface), do: @group_and_any_interface_object_type_int 61 | 62 | def object_type_to_int!(:device_and_any_interface), 63 | do: @device_and_any_interface_object_type_int 64 | 65 | def deserialize_trigger_target(payload) do 66 | %TriggerTargetContainer{ 67 | trigger_target: {_target_type, target} 68 | } = TriggerTargetContainer.decode(payload) 69 | 70 | %AMQPTriggerTarget{static_headers: static_headers_map} = target 71 | 72 | # Use a list of pairs instead of a map be compatible with old (exprotobuf) decoding 73 | %{target | static_headers: Enum.into(static_headers_map, [])} 74 | end 75 | 76 | def deserialize_simple_trigger(payload) do 77 | %SimpleTriggerContainer{ 78 | simple_trigger: {simple_trigger_type, simple_trigger} 79 | } = SimpleTriggerContainer.decode(payload) 80 | 81 | {simple_trigger_type, simple_trigger} 82 | end 83 | 84 | def get_interface_id_or_any(interface_name, interface_major) do 85 | if interface_name == "*" do 86 | :any_interface 87 | else 88 | CQLUtils.interface_id(interface_name, interface_major) 89 | end 90 | end 91 | 92 | def get_group_object_id(group_name) when is_binary(group_name) do 93 | UUID.uuid5(@groups_namespace, group_name, :raw) 94 | end 95 | 96 | def get_device_and_any_interface_object_id(device_id) when is_binary(device_id) do 97 | UUID.uuid5(@any_device_object_id, device_id, :raw) 98 | end 99 | 100 | def get_group_and_any_interface_object_id(group_name) when is_binary(group_name) do 101 | UUID.uuid5(@any_device_object_id, group_name, :raw) 102 | end 103 | 104 | def get_device_and_interface_object_id(device_id, interface_id) 105 | when is_binary(device_id) and is_binary(interface_id) do 106 | UUID.uuid5(interface_id, device_id, :raw) 107 | end 108 | 109 | def get_group_and_interface_object_id(group_name, interface_id) 110 | when is_binary(group_name) and is_binary(interface_id) do 111 | UUID.uuid5(interface_id, group_name, :raw) 112 | end 113 | 114 | def simple_trigger_to_data_trigger(protobuf_data_trigger) do 115 | %SimpleTriggersProtobufDataTrigger{ 116 | interface_name: interface_name, 117 | interface_major: interface_major, 118 | match_path: match_path, 119 | value_match_operator: value_match_operator, 120 | known_value: encoded_known_value 121 | } = protobuf_data_trigger 122 | 123 | value_match_operator = 124 | if value_match_operator == :INVALID_OPERATOR do 125 | :ANY 126 | else 127 | value_match_operator 128 | end 129 | 130 | %{"v" => plain_value} = 131 | if encoded_known_value do 132 | Cyanide.decode!(encoded_known_value) 133 | else 134 | %{"v" => nil} 135 | end 136 | 137 | path_match_tokens = 138 | if match_path == "/*" do 139 | :any_endpoint 140 | else 141 | match_path 142 | |> Mapping.normalize_endpoint() 143 | |> String.split("/") 144 | |> tl() 145 | end 146 | 147 | interface_id_or_any = get_interface_id_or_any(interface_name, interface_major) 148 | 149 | %DataTrigger{ 150 | interface_id: interface_id_or_any, 151 | path_match_tokens: path_match_tokens, 152 | value_match_operator: value_match_operator, 153 | known_value: plain_value, 154 | trigger_targets: nil 155 | } 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /lib/astarte_core/mapping/endpoints_automaton.ex: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2017 Ispirata Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.Mapping.EndpointsAutomaton do 20 | alias Astarte.Core.Mapping 21 | 22 | @doc """ 23 | returns `:ok` and an endpoint for a given `path` using a previously built automata (`{transitions, accepting_states}`). 24 | if path is not complete one or more endpoints will be guessed and `:guessed` followed by a list of endpoints is returned. 25 | """ 26 | def resolve_path(path, {transitions, accepting_states}) do 27 | path_tokens = String.split(path, "/", trim: true) 28 | 29 | states = do_transitions(path_tokens, [0], transitions) 30 | 31 | cond do 32 | states == [] -> 33 | {:error, :not_found} 34 | 35 | length(states) == 1 and accepting_states[hd(states)] != nil -> 36 | {:ok, accepting_states[hd(states)]} 37 | 38 | true -> 39 | states = force_transitions(states, transitions, accepting_states) 40 | 41 | guessed_endpoints = 42 | for state <- states do 43 | accepting_states[state] 44 | end 45 | 46 | {:guessed, guessed_endpoints} 47 | end 48 | end 49 | 50 | @doc """ 51 | builds the automaton for given `mappings`, returns `:ok` followed by the automaton tuple if build succeeded, otherwise `:error` and the reason. 52 | """ 53 | def build(mappings) do 54 | nfa = do_build(mappings) 55 | 56 | if is_valid?(nfa, mappings) do 57 | {:ok, nfa} 58 | else 59 | {:error, :overlapping_mappings} 60 | end 61 | end 62 | 63 | @doc """ 64 | returns true if `nfa` is valid for given `mappings` 65 | """ 66 | def is_valid?(nfa, mappings) do 67 | Enum.all?(mappings, fn mapping -> 68 | resolve_path(mapping.endpoint, nfa) == {:ok, mapping.endpoint} 69 | end) 70 | end 71 | 72 | @doc """ 73 | returns a list of likely invalid endpoints for a certain list of `mappings`. 74 | """ 75 | def lint(mappings) do 76 | nfa = do_build(mappings) 77 | 78 | mappings 79 | |> Enum.filter(fn mapping -> 80 | resolve_path(mapping.endpoint, nfa) != {:ok, mapping.endpoint} 81 | end) 82 | |> Enum.map(fn mapping -> mapping.endpoint end) 83 | end 84 | 85 | defp do_transitions([], current_states, _transitions) do 86 | current_states 87 | end 88 | 89 | defp do_transitions(_tokens, [], _transitions) do 90 | [] 91 | end 92 | 93 | defp do_transitions([token | tail_tokens], current_states, transitions) do 94 | next_states = 95 | List.foldl(current_states, [], fn state, acc -> 96 | if Mapping.is_placeholder?(token) do 97 | all_state_transitions = state_transitions(transitions, state) 98 | 99 | all_state_transitions ++ acc 100 | else 101 | transition_list = Map.get(transitions, {state, token}) |> List.wrap() 102 | epsi_transition_list = Map.get(transitions, {state, ""}) |> List.wrap() 103 | 104 | transition_list ++ epsi_transition_list ++ acc 105 | end 106 | end) 107 | 108 | do_transitions(tail_tokens, next_states, transitions) 109 | end 110 | 111 | defp force_transitions(current_states, transitions, accepting_states) do 112 | next_states = 113 | List.foldl(current_states, [], fn state, acc -> 114 | good_state = 115 | if accepting_states[state] == nil do 116 | state_transitions(transitions, state) 117 | else 118 | [state] 119 | end 120 | 121 | good_state ++ acc 122 | end) 123 | 124 | finished = 125 | Enum.all?(next_states, fn state -> 126 | accepting_states[state] 127 | end) 128 | 129 | if finished do 130 | next_states 131 | else 132 | force_transitions(next_states, transitions, accepting_states) 133 | end 134 | end 135 | 136 | defp state_transitions(transitions, state) do 137 | Enum.reduce(transitions, [], fn 138 | {{^state, _}, next_state}, acc -> 139 | [next_state | acc] 140 | 141 | _transition, acc -> 142 | acc 143 | end) 144 | end 145 | 146 | defp do_build(mappings) do 147 | {transitions, _, accepting_states} = List.foldl(mappings, {%{}, [], %{}}, &parse_endpoint/2) 148 | 149 | {transitions, accepting_states} 150 | end 151 | 152 | def parse_endpoint(mapping, {transitions, states, accepting_states}) do 153 | ["" | path_tokens] = 154 | mapping.endpoint 155 | |> Mapping.normalize_endpoint() 156 | |> String.split("/") 157 | 158 | {states, _, _, transitions} = 159 | List.foldl(path_tokens, {states, 0, "", transitions}, fn token, 160 | {states, previous_state, 161 | partial_endpoint, transitions} -> 162 | new_partial_endpoint = "#{partial_endpoint}/#{token}" 163 | 164 | candidate_previous = 165 | Enum.find_index(states, fn state -> state == new_partial_endpoint end) 166 | 167 | if candidate_previous != nil do 168 | {states, candidate_previous, new_partial_endpoint, transitions} 169 | else 170 | states = states ++ [partial_endpoint] 171 | new_state = length(states) 172 | 173 | {states, new_state, new_partial_endpoint, 174 | Map.put(transitions, {previous_state, token}, new_state)} 175 | end 176 | end) 177 | 178 | accepting_states = Map.put(accepting_states, length(states), mapping.endpoint) 179 | 180 | {transitions, states, accepting_states} 181 | end 182 | end 183 | -------------------------------------------------------------------------------- /lib/astarte_core/triggers/policy/policy.ex: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2022 SECO Mind srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.Triggers.Policy do 20 | use TypedEctoSchema 21 | import Ecto.Changeset 22 | 23 | alias Astarte.Core.Triggers.Policy 24 | alias Astarte.Core.Triggers.Policy.Handler 25 | alias Astarte.Core.Triggers.PolicyProtobuf.Policy, as: PolicyProto 26 | 27 | @policy_name_regex ~r/^(?!@).+$/ 28 | 29 | @required_fields [ 30 | :name, 31 | :error_handlers, 32 | :maximum_capacity 33 | ] 34 | 35 | @permitted_fields [ 36 | :name, 37 | :maximum_capacity, 38 | :retry_times, 39 | :event_ttl, 40 | :prefetch_count 41 | ] 42 | 43 | @derive Jason.Encoder 44 | @primary_key false 45 | typed_embedded_schema do 46 | field :name 47 | field :maximum_capacity, :integer 48 | field :retry_times, :integer 49 | field :event_ttl, :integer 50 | field :prefetch_count, :integer 51 | embeds_many :error_handlers, Policy.Handler 52 | end 53 | 54 | def changeset(%Policy{} = policy, params \\ %{}) do 55 | policy 56 | |> cast(params, @permitted_fields) 57 | |> validate_required(@required_fields) 58 | |> validate_length(:name, max: 128) 59 | |> validate_format(:name, @policy_name_regex) 60 | |> validate_inclusion(:retry_times, 1..100) 61 | |> validate_inclusion(:event_ttl, 1..86_400) 62 | |> validate_inclusion(:maximum_capacity, 1..1_000_000) 63 | |> validate_inclusion(:prefetch_count, 1..300) 64 | |> cast_embed(:error_handlers, with: &Handler.changeset/2, required: true) 65 | |> validate_length(:error_handlers, min: 1, max: 200) 66 | |> validate_all_handlers_on_different_errors() 67 | |> validate_retry_times_compatible() 68 | end 69 | 70 | def valid_name?(name) do 71 | String.match?(name, @policy_name_regex) 72 | end 73 | 74 | defp validate_all_handlers_on_different_errors(changeset) do 75 | handlers = get_field(changeset, :error_handlers, []) 76 | 77 | if disjoint_errors?(handlers) do 78 | changeset 79 | else 80 | add_error(changeset, :error_handlers, "must all handle distinct errors.") 81 | end 82 | end 83 | 84 | defp disjoint_errors?(handlers) do 85 | {_, disjoint?} = 86 | Enum.map(handlers, &Handler.error_set/1) 87 | |> Enum.reduce_while({MapSet.new(), true}, fn error_set, {error_set_acc, _disjoint?} -> 88 | if MapSet.disjoint?(error_set, error_set_acc) do 89 | {:cont, {MapSet.union(error_set, error_set_acc), true}} 90 | else 91 | {:halt, {nil, false}} 92 | end 93 | end) 94 | 95 | disjoint? 96 | end 97 | 98 | defp validate_retry_times_compatible(changeset) do 99 | handlers = get_field(changeset, :error_handlers, []) 100 | retry_times = get_field(changeset, :retry_times) 101 | all_discards = Enum.all?(handlers, &Handler.discards?/1) 102 | 103 | cond do 104 | all_discards and retry_times != nil -> 105 | add_error(changeset, :retry_times, "must not be set if all errors are discarded.") 106 | 107 | not all_discards and retry_times == nil -> 108 | add_error(changeset, :retry_times, "must be set if some events are to be retried.") 109 | 110 | true -> 111 | changeset 112 | end 113 | end 114 | 115 | @doc """ 116 | Creates a `Policy` from a `PolicyProto`. 117 | Returns `{:ok, %Policy{}}` on success, `{:error, :invalid_policy_data}` on failure 118 | """ 119 | def from_policy_proto(%PolicyProto{} = policy_proto) do 120 | with %PolicyProto{ 121 | name: name, 122 | maximum_capacity: maximum_capacity, 123 | retry_times: retry_times, 124 | event_ttl: event_ttl, 125 | prefetch_count: prefetch_count, 126 | error_handlers: error_handlers 127 | } <- policy_proto do 128 | event_ttl = 129 | if event_ttl != 0 do 130 | event_ttl 131 | else 132 | nil 133 | end 134 | 135 | {:ok, 136 | %Policy{ 137 | name: name, 138 | error_handlers: Enum.map(error_handlers, &Handler.from_handler_proto/1), 139 | maximum_capacity: maximum_capacity, 140 | retry_times: retry_times, 141 | event_ttl: event_ttl, 142 | prefetch_count: prefetch_count 143 | }} 144 | else 145 | _ -> {:error, :invalid_policy_data} 146 | end 147 | end 148 | 149 | @doc """ 150 | Creates a `Policy` from a `PolicyProto`. 151 | 152 | Returns the `%Policy{}` on success, 153 | raises on failure 154 | """ 155 | def from_policy_proto!(policy_proto) do 156 | with {:ok, policy} <- from_policy_proto(policy_proto) do 157 | policy 158 | else 159 | _ -> 160 | raise ArgumentError 161 | end 162 | end 163 | 164 | @doc """ 165 | Creates a `PolicyProto` from a `Policy`. 166 | 167 | It is assumed that the `Policy` is valid and constructed using `Policy.changeset` 168 | 169 | Returns a `%PolicyProto{}` 170 | """ 171 | def to_policy_proto(%Policy{} = policy) do 172 | %Policy{ 173 | name: name, 174 | error_handlers: error_handlers, 175 | maximum_capacity: maximum_capacity, 176 | retry_times: retry_times, 177 | event_ttl: event_ttl, 178 | prefetch_count: prefetch_count 179 | } = policy 180 | 181 | %PolicyProto{ 182 | name: name, 183 | maximum_capacity: maximum_capacity, 184 | retry_times: retry_times, 185 | event_ttl: event_ttl || 0, 186 | prefetch_count: prefetch_count, 187 | error_handlers: Enum.map(error_handlers, &Handler.to_handler_proto/1) 188 | } 189 | end 190 | end 191 | -------------------------------------------------------------------------------- /lib/astarte_core/cql_utils.ex: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2017 Ispirata Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.CQLUtils do 20 | @moduledoc """ 21 | This module contains a set of functions that should be used to map Astarte types and concepts to C* 22 | """ 23 | 24 | @interface_descriptor_statement """ 25 | SELECT name, major_version, minor_version, type, ownership, aggregation 26 | FROM interfaces 27 | WHERE name=:name AND major_version=:major_version 28 | """ 29 | 30 | @doc """ 31 | Returns a generated table name that might be used during table creation." 32 | """ 33 | def interface_name_to_table_name(interface_name, major_version) do 34 | safe_interface_name = 35 | interface_name 36 | |> String.downcase() 37 | |> String.replace(".", "_") 38 | |> String.replace("-", "") 39 | 40 | long_table_name = "#{safe_interface_name}_v" <> Integer.to_string(major_version) 41 | long_name_len = String.length(long_table_name) 42 | 43 | if long_name_len >= 45 do 44 | <> = 45 | :crypto.hash(:sha256, long_table_name) 46 | |> Base.encode64() 47 | |> String.replace("+", "_") 48 | |> String.replace("/", "_") 49 | 50 | {_, shorter_name} = String.split_at(long_table_name, long_name_len - 39) 51 | 52 | "a#{prefix}_#{shorter_name}" 53 | else 54 | long_table_name 55 | end 56 | end 57 | 58 | @doc """ 59 | Returns the column name for a certain endpoint that will be used for object interface tables. 60 | """ 61 | def endpoint_to_db_column_name(endpoint_name) when is_binary(endpoint_name) do 62 | long_column_name_suffix = 63 | endpoint_name 64 | |> String.split("/") 65 | |> List.last() 66 | |> String.downcase() 67 | 68 | long_name_len = String.length(long_column_name_suffix) 69 | 70 | if long_name_len >= 44 do 71 | <> = 72 | :crypto.hash(:sha256, long_column_name_suffix) 73 | |> Base.encode64() 74 | |> String.replace(~r/[+\/]/, "_") 75 | 76 | {_, shorter_name} = String.split_at(long_column_name_suffix, long_name_len - 38) 77 | 78 | "v_#{prefix}_#{shorter_name}" 79 | else 80 | "v_#{long_column_name_suffix}" 81 | end 82 | end 83 | 84 | @doc """ 85 | Returns true if a given CQL name can be safely used for a table or a column name 86 | """ 87 | def is_valid_cql_name?(cql_name) when is_binary(cql_name) do 88 | String.match?(cql_name, ~r/^[a-zA-Z]+[a-zA-Z0-9_]*$/) and String.length(cql_name) <= 48 89 | end 90 | 91 | @doc """ 92 | Returns the CQL query statement that should be used to retrieve interface descriptor from the database. 93 | """ 94 | def interface_descriptor_statement() do 95 | @interface_descriptor_statement 96 | end 97 | 98 | @doc """ 99 | Returns a CQL type for a given mapping value type atom 100 | """ 101 | def mapping_value_type_to_db_type(value_type) do 102 | case value_type do 103 | :double -> "double" 104 | :integer -> "int" 105 | :boolean -> "boolean" 106 | :longinteger -> "bigint" 107 | :string -> "varchar" 108 | :binaryblob -> "blob" 109 | :datetime -> "timestamp" 110 | :doublearray -> "list" 111 | :integerarray -> "list" 112 | :booleanarray -> "list" 113 | :longintegerarray -> "list" 114 | :stringarray -> "list" 115 | :binaryblobarray -> "list" 116 | :datetimearray -> "list" 117 | end 118 | end 119 | 120 | @doc """ 121 | Returns table column name that stores a certain type. 122 | """ 123 | def type_to_db_column_name(column_type) do 124 | case column_type do 125 | :double -> "double_value" 126 | :integer -> "integer_value" 127 | :boolean -> "boolean_value" 128 | :longinteger -> "longinteger_value" 129 | :string -> "string_value" 130 | :binaryblob -> "binaryblob_value" 131 | :datetime -> "datetime_value" 132 | :doublearray -> "doublearray_value" 133 | :integerarray -> "integerarray_value" 134 | :booleanarray -> "booleanarray_value" 135 | :longintegerarray -> "longintegerarray_value" 136 | :stringarray -> "stringarray_value" 137 | :binaryblobarray -> "binaryblobarray_value" 138 | :datetimearray -> "datetimearray_value" 139 | end 140 | end 141 | 142 | @doc """ 143 | Returns interface UUID for a certain `interface_name` with a certain `interface_major` 144 | """ 145 | def interface_id(interface_name, interface_major) 146 | when is_binary(interface_name) and is_integer(interface_major) do 147 | iid_string = "iid:#{interface_name}:#{Integer.to_string(interface_major)}" 148 | 149 | <> = :crypto.hash(:sha256, iid_string) 150 | 151 | iid 152 | end 153 | 154 | @doc """ 155 | returns an endpoint UUID for a certain `endpoint` on a certain `interface_name` with a certain `interface_major`. 156 | """ 157 | def endpoint_id(interface_name, interface_major, endpoint) 158 | when is_binary(interface_name) and is_integer(interface_major) and is_binary(endpoint) do 159 | stripped_endpoint = 160 | endpoint 161 | |> String.replace(~r/%{[a-zA-Z0-9]*}/, "") 162 | 163 | eid_string = 164 | "eid:#{interface_name}:#{Integer.to_string(interface_major)}:#{stripped_endpoint}:" 165 | 166 | <> = :crypto.hash(:sha256, eid_string) 167 | 168 | eid 169 | end 170 | 171 | @spec realm_name_to_keyspace_name(nonempty_binary(), binary()) :: nonempty_binary() 172 | def realm_name_to_keyspace_name(realm_name, astarte_instance_id \\ "") 173 | when is_binary(realm_name) do 174 | instance_and_realm = astarte_instance_id <> realm_name 175 | 176 | case String.length(instance_and_realm) do 177 | len when len >= 48 -> Base.url_encode64(instance_and_realm, padding: false) 178 | _ -> instance_and_realm 179 | end 180 | |> String.replace("-", "") 181 | |> String.replace("_", "") 182 | |> String.slice(0..47) 183 | |> String.downcase() 184 | end 185 | end 186 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "certifi": {:hex, :certifi, "2.6.1", "dbab8e5e155a0763eea978c913ca280a6b544bfa115633fa20249c3d396d9493", [:rebar3], [], "hexpm", "524c97b4991b3849dd5c17a631223896272c6b0af446778ba4675a1dff53bb7e"}, 3 | "cyanide": {:hex, :cyanide, "2.0.0", "f97b700b87f9b0679ae812f0c4b7fe35ea6541a4121a096cf10287941b7a6d55", [:mix], [], "hexpm", "7f9748251804c2a2115b539202568e1117ab2f0ae09875853fb89cc94ae19dd1"}, 4 | "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, 5 | "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, 6 | "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, 7 | "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, 8 | "ecto_morph": {:hex, :ecto_morph, "0.1.29", "bc0b915779636bd2d30c54cad6922b3cb40f85b1d4ad59bdffd3c788d9d1f972", [:mix], [{:ecto, ">= 3.0.3", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "814bed72e3d03b278c1dfb3fbc4da37f478a37518ee54f010c1ad9254f1ca0e3"}, 9 | "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"}, 10 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 11 | "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"}, 12 | "excoveralls": {:hex, :excoveralls, "0.18.1", "a6f547570c6b24ec13f122a5634833a063aec49218f6fff27de9df693a15588c", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d65f79db146bb20399f23046015974de0079668b9abb2f5aac074d078da60b8d"}, 13 | "hackney": {:hex, :hackney, "1.17.4", "99da4674592504d3fb0cfef0db84c3ba02b4508bae2dff8c0108baa0d6e0977c", [:rebar3], [{:certifi, "~>2.6.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "de16ff4996556c8548d512f4dbe22dd58a587bf3332e7fd362430a7ef3986b16"}, 14 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 15 | "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, 16 | "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"}, 17 | "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"}, 18 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, 19 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 20 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 21 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 22 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, 23 | "protobuf": {:hex, :protobuf, "0.12.0", "58c0dfea5f929b96b5aa54ec02b7130688f09d2de5ddc521d696eec2a015b223", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "75fa6cbf262062073dd51be44dd0ab940500e18386a6c4e87d5819a58964dc45"}, 24 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, 25 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, 26 | "typed_ecto_schema": {:hex, :typed_ecto_schema, "0.4.1", "a373ca6f693f4de84cde474a67467a9cb9051a8a7f3f615f1e23dc74b75237fa", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "85c6962f79d35bf543dd5659c6adc340fd2480cacc6f25d2cc2933ea6e8fcb3b"}, 27 | "typedstruct": {:hex, :typedstruct, "0.5.3", "d68ae424251a41b81a8d0c485328ab48edbd3858f3565bbdac21b43c056fc9b4", [:make, :mix], [], "hexpm", "b53b8186701417c0b2782bf02a2db5524f879b8488f91d1d83b97d84c2943432"}, 28 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 29 | } 30 | -------------------------------------------------------------------------------- /test/astarte_core/triggers/simple_triggers_protobuf_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.Triggers.SimpleTriggersProtobufTest do 2 | use ExUnit.Case 3 | 4 | describe "payload serialized with ExProtobuf" do 5 | test "still works for AMQPTriggerTarget" do 6 | alias Astarte.Core.Triggers.SimpleTriggersProtobuf.AMQPTriggerTarget 7 | 8 | simple_trigger_id = "c0cd4ff8-1ee1-4162-b654-2697f6af652b" 9 | parent_trigger_id = "825b42ff-6664-4f67-ac64-71a1865acb05" 10 | static_header_key = "important_metadata_connected" 11 | static_header_value = "test_meta_connected" 12 | static_headers = %{static_header_key => static_header_value} 13 | routing_key = "test_routing_key" 14 | 15 | target = %AMQPTriggerTarget{ 16 | version: 1, 17 | simple_trigger_id: simple_trigger_id, 18 | parent_trigger_id: parent_trigger_id, 19 | static_headers: static_headers, 20 | routing_key: routing_key 21 | } 22 | 23 | serialized_target = 24 | <<8, 1, 18, 36, 99, 48, 99, 100, 52, 102, 102, 56, 45, 49, 101, 101, 49, 45, 52, 49, 54, 25 | 50, 45, 98, 54, 53, 52, 45, 50, 54, 57, 55, 102, 54, 97, 102, 54, 53, 50, 98, 26, 36, 26 | 56, 50, 53, 98, 52, 50, 102, 102, 45, 54, 54, 54, 52, 45, 52, 102, 54, 55, 45, 97, 99, 27 | 54, 52, 45, 55, 49, 97, 49, 56, 54, 53, 97, 99, 98, 48, 53, 34, 16, 116, 101, 115, 116, 28 | 95, 114, 111, 117, 116, 105, 110, 103, 95, 107, 101, 121, 42, 51, 10, 28, 105, 109, 112, 29 | 111, 114, 116, 97, 110, 116, 95, 109, 101, 116, 97, 100, 97, 116, 97, 95, 99, 111, 110, 30 | 110, 101, 99, 116, 101, 100, 18, 19, 116, 101, 115, 116, 95, 109, 101, 116, 97, 95, 99, 31 | 111, 110, 110, 101, 99, 116, 101, 100>> 32 | 33 | assert AMQPTriggerTarget.encode(target) == serialized_target 34 | assert AMQPTriggerTarget.decode(serialized_target) == target 35 | end 36 | 37 | test "still works for DataTrigger" do 38 | alias Astarte.Core.Triggers.SimpleTriggersProtobuf.DataTrigger 39 | 40 | trigger = %DataTrigger{ 41 | version: 1, 42 | interface_name: "com.test.SimpleStreamTest", 43 | interface_major: 1, 44 | data_trigger_type: :INCOMING_DATA, 45 | match_path: "/0/value", 46 | value_match_operator: :LESS_THAN, 47 | known_value: Cyanide.encode!(%{v: 100}) 48 | } 49 | 50 | serialized_trigger = 51 | <<8, 1, 16, 1, 26, 25, 99, 111, 109, 46, 116, 101, 115, 116, 46, 83, 105, 109, 112, 108, 52 | 101, 83, 116, 114, 101, 97, 109, 84, 101, 115, 116, 32, 1, 42, 8, 47, 48, 47, 118, 97, 53 | 108, 117, 101, 48, 6, 58, 12, 12, 0, 0, 0, 16, 118, 0, 100, 0, 0, 0, 0>> 54 | 55 | assert DataTrigger.encode(trigger) == serialized_trigger 56 | assert DataTrigger.decode(serialized_trigger) == trigger 57 | end 58 | 59 | test "still works for DeviceTrigger" do 60 | alias Astarte.Core.Triggers.SimpleTriggersProtobuf.DeviceTrigger 61 | 62 | trigger = %DeviceTrigger{ 63 | version: 1, 64 | device_id: "f0VMRgIBAQAAAAAAAAAAAA", 65 | device_event_type: :DEVICE_CONNECTED 66 | } 67 | 68 | serialized_trigger = 69 | <<8, 1, 16, 1, 26, 22, 102, 48, 86, 77, 82, 103, 73, 66, 65, 81, 65, 65, 65, 65, 65, 65, 70 | 65, 65, 65, 65, 65, 65>> 71 | 72 | assert DeviceTrigger.encode(trigger) == serialized_trigger 73 | assert DeviceTrigger.decode(serialized_trigger) == trigger 74 | end 75 | 76 | test "still works for SimpleTriggerContainer" do 77 | alias Astarte.Core.Triggers.SimpleTriggersProtobuf.SimpleTriggerContainer 78 | alias Astarte.Core.Triggers.SimpleTriggersProtobuf.DeviceTrigger 79 | 80 | container = %SimpleTriggerContainer{ 81 | version: 1, 82 | simple_trigger: { 83 | :device_trigger, 84 | %DeviceTrigger{ 85 | version: 1, 86 | device_event_type: :DEVICE_CONNECTED 87 | } 88 | } 89 | } 90 | 91 | serialized_container = <<8, 1, 18, 4, 8, 1, 16, 1>> 92 | 93 | assert SimpleTriggerContainer.encode(container) == serialized_container 94 | assert SimpleTriggerContainer.decode(serialized_container) == container 95 | end 96 | 97 | test "still works for TaggedSimpleTrigger" do 98 | alias Astarte.Core.Triggers.SimpleTriggersProtobuf.TaggedSimpleTrigger 99 | alias Astarte.Core.Triggers.SimpleTriggersProtobuf.SimpleTriggerContainer 100 | alias Astarte.Core.Triggers.SimpleTriggersProtobuf.DeviceTrigger 101 | 102 | trigger = %TaggedSimpleTrigger{ 103 | version: 1, 104 | object_id: "9ac234b0-4767-449c-a581-345c2bafaece", 105 | object_type: 2, 106 | simple_trigger_container: %SimpleTriggerContainer{ 107 | version: 1, 108 | simple_trigger: { 109 | :device_trigger, 110 | %DeviceTrigger{ 111 | version: 1, 112 | device_event_type: :DEVICE_CONNECTED 113 | } 114 | } 115 | } 116 | } 117 | 118 | serialized_trigger = 119 | <<8, 1, 18, 36, 57, 97, 99, 50, 51, 52, 98, 48, 45, 52, 55, 54, 55, 45, 52, 52, 57, 99, 120 | 45, 97, 53, 56, 49, 45, 51, 52, 53, 99, 50, 98, 97, 102, 97, 101, 99, 101, 24, 2, 34, 8, 121 | 8, 1, 18, 4, 8, 1, 16, 1>> 122 | 123 | assert TaggedSimpleTrigger.encode(trigger) == serialized_trigger 124 | assert TaggedSimpleTrigger.decode(serialized_trigger) == trigger 125 | end 126 | 127 | test "still works for TriggerTargetContainer" do 128 | alias Astarte.Core.Triggers.SimpleTriggersProtobuf.TriggerTargetContainer 129 | alias Astarte.Core.Triggers.SimpleTriggersProtobuf.AMQPTriggerTarget 130 | 131 | simple_trigger_id = "c0cd4ff8-1ee1-4162-b654-2697f6af652b" 132 | parent_trigger_id = "825b42ff-6664-4f67-ac64-71a1865acb05" 133 | static_header_key = "important_metadata_connected" 134 | static_header_value = "test_meta_connected" 135 | static_headers = %{static_header_key => static_header_value} 136 | routing_key = "test_routing_key" 137 | 138 | trigger = %TriggerTargetContainer{ 139 | version: 1, 140 | trigger_target: { 141 | :amqp_trigger_target, 142 | %AMQPTriggerTarget{ 143 | version: 1, 144 | simple_trigger_id: simple_trigger_id, 145 | parent_trigger_id: parent_trigger_id, 146 | static_headers: static_headers, 147 | routing_key: routing_key 148 | } 149 | } 150 | } 151 | 152 | serialized_trigger = 153 | <<8, 1, 18, 149, 1, 8, 1, 18, 36, 99, 48, 99, 100, 52, 102, 102, 56, 45, 49, 101, 101, 49, 154 | 45, 52, 49, 54, 50, 45, 98, 54, 53, 52, 45, 50, 54, 57, 55, 102, 54, 97, 102, 54, 53, 155 | 50, 98, 26, 36, 56, 50, 53, 98, 52, 50, 102, 102, 45, 54, 54, 54, 52, 45, 52, 102, 54, 156 | 55, 45, 97, 99, 54, 52, 45, 55, 49, 97, 49, 56, 54, 53, 97, 99, 98, 48, 53, 34, 16, 116, 157 | 101, 115, 116, 95, 114, 111, 117, 116, 105, 110, 103, 95, 107, 101, 121, 42, 51, 10, 28, 158 | 105, 109, 112, 111, 114, 116, 97, 110, 116, 95, 109, 101, 116, 97, 100, 97, 116, 97, 95, 159 | 99, 111, 110, 110, 101, 99, 116, 101, 100, 18, 19, 116, 101, 115, 116, 95, 109, 101, 160 | 116, 97, 95, 99, 111, 110, 110, 101, 99, 116, 101, 100>> 161 | 162 | assert TriggerTargetContainer.encode(trigger) == serialized_trigger 163 | assert TriggerTargetContainer.decode(serialized_trigger) == trigger 164 | end 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /test/astarte_core/mapping_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Astarte.Core.MappingTest do 2 | use ExUnit.Case 3 | 4 | alias Astarte.Core.CQLUtils 5 | alias Astarte.Core.Mapping 6 | 7 | test "mapping with no type fails" do 8 | opts = opts_fixture() 9 | 10 | params = %{ 11 | "endpoint" => "/valid" 12 | } 13 | 14 | assert %Ecto.Changeset{valid?: false, errors: [type: _]} = 15 | Mapping.changeset(%Mapping{}, params, opts) 16 | end 17 | 18 | test "mapping with invalid type fails" do 19 | opts = opts_fixture() 20 | 21 | params = %{ 22 | "endpoint" => "/valid", 23 | "type" => "invalid" 24 | } 25 | 26 | assert %Ecto.Changeset{valid?: false, errors: [type: _]} = 27 | Mapping.changeset(%Mapping{}, params, opts) 28 | end 29 | 30 | test "mapping with invalid endpoint fails" do 31 | opts = opts_fixture() 32 | 33 | params = %{ 34 | "endpoint" => "//this/is/almost/%{ok}", 35 | "type" => "string" 36 | } 37 | 38 | assert %Ecto.Changeset{valid?: false, errors: [endpoint: _]} = 39 | Mapping.changeset(%Mapping{}, params, opts) 40 | end 41 | 42 | test "mapping with invalid retention fails" do 43 | opts = opts_fixture() 44 | 45 | params = %{ 46 | "endpoint" => "/valid", 47 | "type" => "string", 48 | "retention" => "invalid" 49 | } 50 | 51 | assert %Ecto.Changeset{valid?: false, errors: [retention: _]} = 52 | Mapping.changeset(%Mapping{}, params, opts) 53 | end 54 | 55 | test "mapping with invalid reliability fails" do 56 | opts = opts_fixture() 57 | 58 | params = %{ 59 | "endpoint" => "/valid", 60 | "type" => "string", 61 | "reliability" => "invalid" 62 | } 63 | 64 | assert %Ecto.Changeset{valid?: false, errors: [reliability: _]} = 65 | Mapping.changeset(%Mapping{}, params, opts) 66 | end 67 | 68 | test "valid mapping" do 69 | opts = opts_fixture() 70 | 71 | params = %{ 72 | "endpoint" => "/this/is/%{ok}", 73 | "type" => "integer", 74 | "retention" => "stored", 75 | "reliability" => "guaranteed", 76 | "expiry" => 60, 77 | "database_retention_policy" => "use_ttl", 78 | "database_retention_ttl" => 60, 79 | "doc" => "The doc.", 80 | "description" => "The description." 81 | } 82 | 83 | assert %Ecto.Changeset{valid?: true} = changeset = Mapping.changeset(%Mapping{}, params, opts) 84 | assert {:ok, mapping} = Ecto.Changeset.apply_action(changeset, :insert) 85 | 86 | assert %Mapping{ 87 | endpoint: "/this/is/%{ok}", 88 | value_type: :integer, 89 | retention: :stored, 90 | reliability: :guaranteed, 91 | expiry: 60, 92 | doc: "The doc.", 93 | description: "The description." 94 | } = mapping 95 | end 96 | 97 | test "legacy naming" do 98 | opts = opts_fixture() 99 | 100 | params = %{ 101 | "path" => "/this/is/%{ok}", 102 | "type" => "double" 103 | } 104 | 105 | assert %Ecto.Changeset{valid?: true} = changeset = Mapping.changeset(%Mapping{}, params, opts) 106 | assert {:ok, mapping} = Ecto.Changeset.apply_action(changeset, :insert) 107 | 108 | assert %Mapping{ 109 | endpoint: "/this/is/%{ok}", 110 | value_type: :double 111 | } = mapping 112 | end 113 | 114 | test "defaults" do 115 | opts = opts_fixture() 116 | 117 | params = %{ 118 | "endpoint" => "/this/is/%{ok}", 119 | "type" => "datetime" 120 | } 121 | 122 | assert %Ecto.Changeset{valid?: true} = changeset = Mapping.changeset(%Mapping{}, params, opts) 123 | assert {:ok, mapping} = Ecto.Changeset.apply_action(changeset, :insert) 124 | 125 | assert %Mapping{ 126 | endpoint: "/this/is/%{ok}", 127 | value_type: :datetime, 128 | retention: :discard, 129 | reliability: :unreliable, 130 | expiry: 0, 131 | database_retention_policy: :no_ttl, 132 | database_retention_ttl: nil, 133 | allow_unset: false 134 | } = mapping 135 | end 136 | 137 | test "mapping with no_ttl policy and valid database_retention_ttl set fails" do 138 | opts = opts_fixture() 139 | 140 | params = %{ 141 | "endpoint" => "/valid", 142 | "type" => "string", 143 | "reliability" => "guaranteed", 144 | "database_retention_policy" => "no_ttl", 145 | "database_retention_ttl" => 80 146 | } 147 | 148 | assert %Ecto.Changeset{valid?: false, errors: [database_retention_policy: _]} = 149 | Mapping.changeset(%Mapping{}, params, opts) 150 | end 151 | 152 | test "mapping with no_ttl policy and invalid database_retention_ttl set fails" do 153 | opts = opts_fixture() 154 | 155 | params = %{ 156 | "endpoint" => "/valid", 157 | "type" => "string", 158 | "reliability" => "guaranteed", 159 | "database_retention_policy" => "no_ttl", 160 | "database_retention_ttl" => 0 161 | } 162 | 163 | assert %Ecto.Changeset{ 164 | valid?: false, 165 | errors: [ 166 | {:database_retention_policy, _}, 167 | {:database_retention_ttl, _} 168 | ] 169 | } = Mapping.changeset(%Mapping{}, params, opts) 170 | end 171 | 172 | test "mapping with no_ttl policy and no database_retention_ttl succeeds" do 173 | opts = opts_fixture() 174 | 175 | params = %{ 176 | "endpoint" => "/valid", 177 | "type" => "string", 178 | "reliability" => "guaranteed", 179 | "database_retention_policy" => "no_ttl" 180 | } 181 | 182 | assert %Ecto.Changeset{valid?: true} = Mapping.changeset(%Mapping{}, params, opts) 183 | end 184 | 185 | test "mapping with use_ttl policy and no database_retention_ttl fails" do 186 | opts = opts_fixture() 187 | 188 | params = %{ 189 | "endpoint" => "/invalid", 190 | "type" => "string", 191 | "reliability" => "guaranteed", 192 | "database_retention_policy" => "use_ttl" 193 | } 194 | 195 | assert %Ecto.Changeset{ 196 | valid?: false, 197 | errors: [ 198 | {:database_retention_ttl, _} 199 | ] 200 | } = Mapping.changeset(%Mapping{}, params, opts) 201 | end 202 | 203 | test "mapping from legacy database result" do 204 | legacy_result = [ 205 | endpoint: "/test", 206 | # integer 207 | value_type: 3, 208 | # unique 209 | reliability: 3, 210 | # stored 211 | retention: 3, 212 | expiry: 0, 213 | database_retention_policy: nil, 214 | database_retention_ttl: nil, 215 | allow_unset: false, 216 | explicit_timestamp: true, 217 | endpoint_id: <<24, 101, 36, 39, 201, 240, 51, 175, 45, 122, 166, 194, 132, 91, 176, 154>>, 218 | interface_id: <<3, 203, 231, 42, 212, 254, 30, 12, 159, 114, 187, 196, 29, 30, 5, 219>> 219 | ] 220 | 221 | expected_mapping = %Mapping{ 222 | endpoint: "/test", 223 | value_type: :integer, 224 | reliability: :unique, 225 | retention: :stored, 226 | expiry: 0, 227 | database_retention_policy: :no_ttl, 228 | database_retention_ttl: nil, 229 | allow_unset: false, 230 | explicit_timestamp: true, 231 | description: nil, 232 | doc: nil, 233 | endpoint_id: <<24, 101, 36, 39, 201, 240, 51, 175, 45, 122, 166, 194, 132, 91, 176, 154>>, 234 | interface_id: <<3, 203, 231, 42, 212, 254, 30, 12, 159, 114, 187, 196, 29, 30, 5, 219>> 235 | } 236 | 237 | assert Mapping.from_db_result!(legacy_result) == expected_mapping 238 | end 239 | 240 | defp opts_fixture do 241 | interface_name = "com.Name" 242 | interface_major = 1 243 | interface_id = CQLUtils.interface_id(interface_name, interface_major) 244 | 245 | [interface_name: interface_name, interface_major: interface_major, interface_id: interface_id] 246 | end 247 | end 248 | -------------------------------------------------------------------------------- /lib/astarte_core/mapping/value_type.ex: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of Astarte. 3 | # 4 | # Copyright 2017-2024 SECO Mind Srl 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | defmodule Astarte.Core.Mapping.ValueType do 20 | use Ecto.Type 21 | 22 | @type t :: 23 | :double 24 | | :integer 25 | | :boolean 26 | | :longinteger 27 | | :string 28 | | :binaryblob 29 | | :datetime 30 | | :doublearray 31 | | :integerarray 32 | | :booleanarray 33 | | :longintegerarray 34 | | :stringarray 35 | | :binaryblobarray 36 | | :datetimearray 37 | 38 | @mapping_value_type_double 1 39 | @mapping_value_type_doublearray 2 40 | @mapping_value_type_integer 3 41 | @mapping_value_type_integerarray 4 42 | @mapping_value_type_longinteger 5 43 | @mapping_value_type_longintegerarray 6 44 | @mapping_value_type_string 7 45 | @mapping_value_type_stringarray 8 46 | @mapping_value_type_boolean 9 47 | @mapping_value_type_booleanarray 10 48 | @mapping_value_type_binaryblob 11 49 | @mapping_value_type_binaryblobarray 12 50 | @mapping_value_type_datetime 13 51 | @mapping_value_type_datetimearray 14 52 | @valid_atoms [ 53 | :double, 54 | :integer, 55 | :boolean, 56 | :longinteger, 57 | :string, 58 | :binaryblob, 59 | :datetime, 60 | :doublearray, 61 | :integerarray, 62 | :booleanarray, 63 | :longintegerarray, 64 | :stringarray, 65 | :binaryblobarray, 66 | :datetimearray 67 | ] 68 | 69 | # The following limits are really conservative, 70 | # it is always easier to increase them in future releases 71 | @blob_size 65536 72 | @list_len 1024 73 | @string_size 65536 74 | 75 | @impl true 76 | def type, do: :integer 77 | 78 | @impl true 79 | def cast(nil), do: {:ok, nil} 80 | 81 | def cast(atom) when is_atom(atom) do 82 | if Enum.member?(@valid_atoms, atom) do 83 | {:ok, atom} 84 | else 85 | :error 86 | end 87 | end 88 | 89 | def cast(string) when is_binary(string) do 90 | case string do 91 | "double" -> {:ok, :double} 92 | "integer" -> {:ok, :integer} 93 | "boolean" -> {:ok, :boolean} 94 | "longinteger" -> {:ok, :longinteger} 95 | "string" -> {:ok, :string} 96 | "binaryblob" -> {:ok, :binaryblob} 97 | "datetime" -> {:ok, :datetime} 98 | "doublearray" -> {:ok, :doublearray} 99 | "integerarray" -> {:ok, :integerarray} 100 | "booleanarray" -> {:ok, :booleanarray} 101 | "longintegerarray" -> {:ok, :longintegerarray} 102 | "stringarray" -> {:ok, :stringarray} 103 | "binaryblobarray" -> {:ok, :binaryblobarray} 104 | "datetimearray" -> {:ok, :datetimearray} 105 | _ -> :error 106 | end 107 | end 108 | 109 | def cast(int) when is_integer(int) do 110 | load(int) 111 | end 112 | 113 | def cast(_), do: :error 114 | 115 | def cast!(value) do 116 | case cast(value) do 117 | {:ok, value_type} -> 118 | value_type 119 | 120 | :error -> 121 | raise ArgumentError, message: "#{inspect(value)} is not a valid value type representation" 122 | end 123 | end 124 | 125 | @impl true 126 | def dump(value_type) when is_atom(value_type) do 127 | case value_type do 128 | :double -> {:ok, @mapping_value_type_double} 129 | :integer -> {:ok, @mapping_value_type_integer} 130 | :boolean -> {:ok, @mapping_value_type_boolean} 131 | :longinteger -> {:ok, @mapping_value_type_longinteger} 132 | :string -> {:ok, @mapping_value_type_string} 133 | :binaryblob -> {:ok, @mapping_value_type_binaryblob} 134 | :datetime -> {:ok, @mapping_value_type_datetime} 135 | :doublearray -> {:ok, @mapping_value_type_doublearray} 136 | :integerarray -> {:ok, @mapping_value_type_integerarray} 137 | :booleanarray -> {:ok, @mapping_value_type_booleanarray} 138 | :longintegerarray -> {:ok, @mapping_value_type_longintegerarray} 139 | :stringarray -> {:ok, @mapping_value_type_stringarray} 140 | :binaryblobarray -> {:ok, @mapping_value_type_binaryblobarray} 141 | :datetimearray -> {:ok, @mapping_value_type_datetimearray} 142 | _ -> :error 143 | end 144 | end 145 | 146 | def dump!(value_type) when is_atom(value_type) do 147 | case dump(value_type) do 148 | {:ok, value_type_int} -> value_type_int 149 | :error -> raise ArgumentError, message: "#{inspect(value_type)} is not a valid value type" 150 | end 151 | end 152 | 153 | @impl true 154 | def load(value_type_int) when is_integer(value_type_int) do 155 | case value_type_int do 156 | @mapping_value_type_double -> {:ok, :double} 157 | @mapping_value_type_integer -> {:ok, :integer} 158 | @mapping_value_type_boolean -> {:ok, :boolean} 159 | @mapping_value_type_longinteger -> {:ok, :longinteger} 160 | @mapping_value_type_string -> {:ok, :string} 161 | @mapping_value_type_binaryblob -> {:ok, :binaryblob} 162 | @mapping_value_type_datetime -> {:ok, :datetime} 163 | @mapping_value_type_doublearray -> {:ok, :doublearray} 164 | @mapping_value_type_integerarray -> {:ok, :integerarray} 165 | @mapping_value_type_booleanarray -> {:ok, :booleanarray} 166 | @mapping_value_type_longintegerarray -> {:ok, :longintegerarray} 167 | @mapping_value_type_stringarray -> {:ok, :stringarray} 168 | @mapping_value_type_binaryblobarray -> {:ok, :binaryblobarray} 169 | @mapping_value_type_datetimearray -> {:ok, :datetimearray} 170 | _ -> :error 171 | end 172 | end 173 | 174 | def to_int(value_type) when is_atom(value_type) do 175 | dump!(value_type) 176 | end 177 | 178 | def from_int(int) when is_integer(int) do 179 | cast!(int) 180 | end 181 | 182 | def validate_value(expected_type, value) do 183 | case {value, expected_type} do 184 | {v, :double} when is_number(v) -> 185 | :ok 186 | 187 | {v, :integer} when is_integer(v) and abs(v) <= 0x7FFFFFFF -> 188 | :ok 189 | 190 | {v, :boolean} when is_boolean(v) -> 191 | :ok 192 | 193 | {v, :longinteger} when is_integer(v) and abs(v) <= 0x7FFFFFFFFFFFFFFF -> 194 | :ok 195 | 196 | {v, :string} when is_binary(v) -> 197 | cond do 198 | String.valid?(v) == false -> 199 | {:error, :unexpected_value_type} 200 | 201 | byte_size(v) > @string_size -> 202 | {:error, :value_size_exceeded} 203 | 204 | true -> 205 | :ok 206 | end 207 | 208 | {v, :binaryblob} when is_binary(v) -> 209 | if byte_size(v) > @blob_size do 210 | {:error, :value_size_exceeded} 211 | else 212 | :ok 213 | end 214 | 215 | {%Cyanide.Binary{subtype: _subtype, data: bin}, :binaryblob} when is_binary(bin) -> 216 | if byte_size(bin) > @blob_size do 217 | {:error, :value_size_exceeded} 218 | else 219 | :ok 220 | end 221 | 222 | {{_subtype, bin}, :binaryblob} when is_binary(bin) -> 223 | if byte_size(bin) > @blob_size do 224 | {:error, :value_size_exceeded} 225 | else 226 | :ok 227 | end 228 | 229 | {%DateTime{} = _v, :datetime} -> 230 | :ok 231 | 232 | {v, :datetime} when is_integer(v) -> 233 | :ok 234 | 235 | {v, :doublearray} when is_list(v) -> 236 | validate_array_value(:double, v) 237 | 238 | {v, :integerarray} when is_list(v) -> 239 | validate_array_value(:integer, v) 240 | 241 | {v, :booleanarray} when is_list(v) -> 242 | validate_array_value(:boolean, v) 243 | 244 | {v, :longintegerarray} when is_list(v) -> 245 | validate_array_value(:longinteger, v) 246 | 247 | {v, :stringarray} when is_list(v) -> 248 | validate_array_value(:string, v) 249 | 250 | {v, :binaryblobarray} when is_list(v) -> 251 | validate_array_value(:binaryblob, v) 252 | 253 | {v, :datetimearray} when is_list(v) -> 254 | validate_array_value(:datetime, v) 255 | 256 | _ -> 257 | {:error, :unexpected_value_type} 258 | end 259 | end 260 | 261 | defp validate_array_value(type, values) do 262 | cond do 263 | length(values) > @list_len -> 264 | {:error, :value_size_exceeded} 265 | 266 | Enum.all?(values, fn item -> validate_value(type, item) == :ok end) == false -> 267 | {:error, :unexpected_value_type} 268 | 269 | true -> 270 | :ok 271 | end 272 | end 273 | end 274 | -------------------------------------------------------------------------------- /test/astarte_core/cql_utils_test.exs: -------------------------------------------------------------------------------- 1 | defmodule CQLUtilsTest do 2 | use ExUnit.Case 3 | alias Astarte.Core.CQLUtils 4 | alias Astarte.Core.Realm 5 | 6 | test "interface name to table name" do 7 | assert CQLUtils.interface_name_to_table_name("com.ispirata.Hemera.DeviceLog", 1) == 8 | "com_ispirata_hemera_devicelog_v1" 9 | 10 | assert CQLUtils.interface_name_to_table_name("test", 0) == "test_v0" 11 | end 12 | 13 | test "endpoint name to object interface column name" do 14 | assert CQLUtils.endpoint_to_db_column_name("/testEndpoint") == "v_testendpoint" 15 | assert CQLUtils.endpoint_to_db_column_name("%{p0}/%{p1}/testEndpoint2") == "v_testendpoint2" 16 | 17 | assert CQLUtils.endpoint_to_db_column_name("/this_is_a_quite_long_endpoint_name_thatis43") == 18 | "v_this_is_a_quite_long_endpoint_name_thatis43" 19 | 20 | assert CQLUtils.endpoint_to_db_column_name("/this_is_a_quite_long_endpoint_name_that_is44") == 21 | "v_1XOzAu_s_a_quite_long_endpoint_name_that_is44" 22 | 23 | assert CQLUtils.endpoint_to_db_column_name( 24 | "/this_is_a_quite_long_endpoint_name_that_is_more_than_characters_48" 25 | ) == "v_o82S8J_t_name_that_is_more_than_characters_48" 26 | end 27 | 28 | test "is valid CQL name" do 29 | assert CQLUtils.is_valid_cql_name?("0I_II_II_L") == false 30 | assert CQLUtils.is_valid_cql_name?("I_II_II_L_0") == true 31 | assert CQLUtils.is_valid_cql_name?("I_II_II_L_ù") == false 32 | assert CQLUtils.is_valid_cql_name?("ù_I_II_II_L") == false 33 | assert CQLUtils.is_valid_cql_name?("_I_II_II_L_ù") == false 34 | assert CQLUtils.is_valid_cql_name?("") == false 35 | assert CQLUtils.is_valid_cql_name?("v_testendpoint") == true 36 | assert CQLUtils.is_valid_cql_name?("v_testendpoint2") == true 37 | assert CQLUtils.is_valid_cql_name?("v_this_is_a_quite_long_endpoint_name_thatis43") == true 38 | assert CQLUtils.is_valid_cql_name?("v_1XOzAu_s_a_quite_long_endpoint_name_that_is44") == true 39 | assert CQLUtils.is_valid_cql_name?("v_o82S8J_t_name_that_is_more_than_characters_48") == true 40 | assert CQLUtils.is_valid_cql_name?("v_o82S8J_t_name_that_is_more_than_characters_48a") == true 41 | 42 | assert CQLUtils.is_valid_cql_name?("v_o82S8J_t_name_that_is_more_than_characters_48ab") == 43 | false 44 | end 45 | 46 | test "mapping value type to db column type" do 47 | assert CQLUtils.mapping_value_type_to_db_type(:double) == "double" 48 | assert CQLUtils.mapping_value_type_to_db_type(:integer) == "int" 49 | assert CQLUtils.mapping_value_type_to_db_type(:boolean) == "boolean" 50 | assert CQLUtils.mapping_value_type_to_db_type(:longinteger) == "bigint" 51 | assert CQLUtils.mapping_value_type_to_db_type(:string) == "varchar" 52 | assert CQLUtils.mapping_value_type_to_db_type(:binaryblob) == "blob" 53 | assert CQLUtils.mapping_value_type_to_db_type(:datetime) == "timestamp" 54 | assert CQLUtils.mapping_value_type_to_db_type(:doublearray) == "list" 55 | assert CQLUtils.mapping_value_type_to_db_type(:integerarray) == "list" 56 | assert CQLUtils.mapping_value_type_to_db_type(:booleanarray) == "list" 57 | assert CQLUtils.mapping_value_type_to_db_type(:longintegerarray) == "list" 58 | assert CQLUtils.mapping_value_type_to_db_type(:stringarray) == "list" 59 | assert CQLUtils.mapping_value_type_to_db_type(:binaryblobarray) == "list" 60 | assert CQLUtils.mapping_value_type_to_db_type(:datetimearray) == "list" 61 | 62 | assert_raise CaseClauseError, fn -> CQLUtils.mapping_value_type_to_db_type("integer") end 63 | assert_raise CaseClauseError, fn -> CQLUtils.mapping_value_type_to_db_type(:date) end 64 | assert_raise CaseClauseError, fn -> CQLUtils.mapping_value_type_to_db_type(:time) end 65 | assert_raise CaseClauseError, fn -> CQLUtils.mapping_value_type_to_db_type(:int64) end 66 | assert_raise CaseClauseError, fn -> CQLUtils.mapping_value_type_to_db_type(:timestamp) end 67 | assert_raise CaseClauseError, fn -> CQLUtils.mapping_value_type_to_db_type(:float) end 68 | end 69 | 70 | test "mapping value type to individual interface column name" do 71 | assert CQLUtils.type_to_db_column_name(:double) == "double_value" 72 | assert CQLUtils.type_to_db_column_name(:integer) == "integer_value" 73 | assert CQLUtils.type_to_db_column_name(:boolean) == "boolean_value" 74 | assert CQLUtils.type_to_db_column_name(:longinteger) == "longinteger_value" 75 | assert CQLUtils.type_to_db_column_name(:string) == "string_value" 76 | assert CQLUtils.type_to_db_column_name(:binaryblob) == "binaryblob_value" 77 | assert CQLUtils.type_to_db_column_name(:datetime) == "datetime_value" 78 | assert CQLUtils.type_to_db_column_name(:doublearray) == "doublearray_value" 79 | assert CQLUtils.type_to_db_column_name(:integerarray) == "integerarray_value" 80 | assert CQLUtils.type_to_db_column_name(:booleanarray) == "booleanarray_value" 81 | assert CQLUtils.type_to_db_column_name(:longintegerarray) == "longintegerarray_value" 82 | assert CQLUtils.type_to_db_column_name(:stringarray) == "stringarray_value" 83 | assert CQLUtils.type_to_db_column_name(:binaryblobarray) == "binaryblobarray_value" 84 | assert CQLUtils.type_to_db_column_name(:datetimearray) == "datetimearray_value" 85 | 86 | assert_raise CaseClauseError, fn -> CQLUtils.type_to_db_column_name("integer") end 87 | assert_raise CaseClauseError, fn -> CQLUtils.type_to_db_column_name(:date) end 88 | assert_raise CaseClauseError, fn -> CQLUtils.type_to_db_column_name(:time) end 89 | assert_raise CaseClauseError, fn -> CQLUtils.type_to_db_column_name(:int64) end 90 | assert_raise CaseClauseError, fn -> CQLUtils.type_to_db_column_name(:timestamp) end 91 | assert_raise CaseClauseError, fn -> CQLUtils.type_to_db_column_name(:float) end 92 | end 93 | 94 | test "interface id generation" do 95 | assert CQLUtils.interface_id("com.foo", 2) == CQLUtils.interface_id("com.foo", 2) 96 | assert CQLUtils.interface_id("com.test", 0) != CQLUtils.interface_id("com.test", 1) 97 | assert CQLUtils.interface_id("com.test1", 0) != CQLUtils.interface_id("com.test", 10) 98 | assert CQLUtils.interface_id("a", 1) != CQLUtils.interface_id("b", 1) 99 | 100 | assert CQLUtils.interface_id("org.astarte-platform.MyInterface", 1) != 101 | CQLUtils.interface_id("org.astarte-platform.myinterface", 1) 102 | 103 | assert CQLUtils.interface_id("This.Is.A.Test", 1) != 104 | CQLUtils.interface_id("this.is.a.test", 1) 105 | 106 | assert CQLUtils.interface_id("org.astarte-platform.MyInterface", 1) != 107 | CQLUtils.interface_id("org.astarte-platform.MyInterface", 0) 108 | 109 | assert CQLUtils.interface_id("astarte.is.cool", 10) == 110 | <<209, 245, 26, 90, 177, 111, 236, 137, 134, 53, 237, 97, 134, 247, 21, 254>> 111 | end 112 | 113 | test "endpoint id generation" do 114 | assert Astarte.Core.CQLUtils.endpoint_id("com.foo", 2, "/test/foo") == 115 | Astarte.Core.CQLUtils.endpoint_id("com.foo", 2, "/test/foo") 116 | 117 | assert Astarte.Core.CQLUtils.endpoint_id("com.foo", 2, "/test/Foo") != 118 | Astarte.Core.CQLUtils.endpoint_id("com.foo", 2, "/test/foo") 119 | 120 | assert Astarte.Core.CQLUtils.endpoint_id("org.astarte-platform.MyInterface", 0, "/test/foo") != 121 | Astarte.Core.CQLUtils.endpoint_id("org.astarte-platform.myinterface", 0, "/test/foo") 122 | 123 | assert Astarte.Core.CQLUtils.endpoint_id("Test", 10, "/test") != 124 | Astarte.Core.CQLUtils.endpoint_id("test", 10, "/test") 125 | 126 | assert Astarte.Core.CQLUtils.endpoint_id("com.foo", 2, "/test/foo") != 127 | Astarte.Core.CQLUtils.endpoint_id("com.foo", 3, "/test/foo") 128 | 129 | assert Astarte.Core.CQLUtils.endpoint_id("com.foo", 1, "/test/foo") != 130 | Astarte.Core.CQLUtils.endpoint_id("com.bar", 1, "/test/foo") 131 | 132 | assert Astarte.Core.CQLUtils.endpoint_id("com.foo", 1, "/test/foo") == 133 | <<47, 163, 1, 227, 139, 231, 222, 201, 41, 57, 24, 82, 234, 76, 61, 4>> 134 | end 135 | 136 | test "endpoint id generation with normalization" do 137 | assert Astarte.Core.CQLUtils.endpoint_id("com.foo", 2, "/a/%{something}") == 138 | Astarte.Core.CQLUtils.endpoint_id("com.foo", 2, "/a/%{different}") 139 | 140 | assert Astarte.Core.CQLUtils.endpoint_id("com.foo", 2, "/a/%{something}") == 141 | Astarte.Core.CQLUtils.endpoint_id("com.foo", 2, "/a/%{SomeThing}") 142 | 143 | assert Astarte.Core.CQLUtils.endpoint_id("com.foo", 2, "/a/%{something}") != 144 | Astarte.Core.CQLUtils.endpoint_id("com.foo", 2, "/b/%{SomeThing}") 145 | 146 | assert Astarte.Core.CQLUtils.endpoint_id("com.foo", 10, "/a/%{something}/foo") == 147 | Astarte.Core.CQLUtils.endpoint_id("com.foo", 10, "/a//foo") 148 | 149 | assert Astarte.Core.CQLUtils.endpoint_id("com.foo", 2, "/a/%{something}/foo") != 150 | Astarte.Core.CQLUtils.endpoint_id("com.foo", 2, "/a/%{something}/bar") 151 | end 152 | 153 | describe "Realm name to keyspace name translation" do 154 | test "works with a short realm name that does not get encoded" do 155 | assert CQLUtils.realm_name_to_keyspace_name("example", "atestinstance") == 156 | "atestinstanceexample" 157 | 158 | assert Realm.valid_name?(CQLUtils.realm_name_to_keyspace_name("example", "atestinstance")) == 159 | true 160 | end 161 | 162 | test "works with a long realm name that does get encoded" do 163 | assert CQLUtils.realm_name_to_keyspace_name( 164 | "averyveryverylongrealmnamejustforthistest", 165 | "atestinstance" 166 | ) == 167 | "yxrlc3rpbnn0yw5jzwf2zxj5dmvyexzlcnlsb25ncmvhbg1u" 168 | 169 | assert Realm.valid_name?( 170 | CQLUtils.realm_name_to_keyspace_name( 171 | "averyveryverylongrealmnamejustforthistest", 172 | "atestinstance" 173 | ) 174 | ) == true 175 | end 176 | end 177 | end 178 | --------------------------------------------------------------------------------