Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate to new validation plugin #194

Merged
merged 27 commits into from
Mar 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b92c055
api: Use new validation options
Mar 7, 2019
6760a04
api: Add/fix validations
Mar 15, 2019
cfc05a9
api: Fix populators
Mar 15, 2019
3a4b0f4
api: Regenerate
Mar 7, 2019
c5775d2
api: Validate Set/Update requests using fieldmask
Mar 13, 2019
d50235d
api: Remove bounds validation on raw_payload
johanstokking Mar 14, 2019
85f388d
make: Update protoc image version
Mar 7, 2019
1ac7663
dev: Vendor validator dependencies
Mar 7, 2019
833ddeb
all: Migrate to new validation method signature
Mar 7, 2019
2faaa36
util: Support ValidateFields in validator middleware
Mar 6, 2019
3f214ab
util: Fix PingSlotChannel MAC command frequency encoding
Mar 8, 2019
0c52e16
util: Fix crypto package
Mar 12, 2019
26a2358
ns: Fix panics in application queue
Mar 6, 2019
1f559d4
ns: Harmonize tests, adapt to message validation
Mar 6, 2019
8d91552
js: Harmonize tests, adapt to message validation
Mar 7, 2019
5a8a37b
js: Ensure App/Dev IDs are set in registries
Mar 7, 2019
e899ff5
ns: Fix panic on message drops
Mar 8, 2019
a82632f
ns: Fix BeaconFreqReq MAC encoding
Mar 8, 2019
f1abfd4
ns: Adapt to API changes
Mar 9, 2019
7f5ab3e
js: Make device primary key uid and secondary key EUIs
johanstokking Mar 8, 2019
2c7fa8d
ns: Fix argument order
johanstokking Mar 8, 2019
240601e
js: Make SetByEUI atomic in Redis
Mar 9, 2019
97789d2
util: Do not change given uplink message when getting identifiers
johanstokking Mar 14, 2019
1df8842
util: Adapt to API changes
Mar 15, 2019
6c170e1
dev: Regenerate messages
Mar 12, 2019
1a04640
ns: Fix decoding of uplinks
Mar 15, 2019
7d0d128
js: Remove redundant checks
Mar 15, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .make/protos/go.make
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@
GO_PROTO_TYPES := any duration empty field_mask struct timestamp wrappers
GO_PROTO_TYPE_CONVERSIONS = $(subst $(SPACE),$(COMMA),$(foreach type,$(GO_PROTO_TYPES),Mgoogle/protobuf/$(type).proto=github.com/gogo/protobuf/types))
GO_PROTOC_FLAGS ?= \
--fieldmask_out=$(PROTOC_OUT) \
--fieldmask_out=lang=gogo,$(GO_PROTO_TYPE_CONVERSIONS):$(PROTOC_OUT) \
--gogottn_out=plugins=grpc,$(GO_PROTO_TYPE_CONVERSIONS):$(PROTOC_OUT) \
--grpc-gateway_out=$(GO_PROTO_TYPE_CONVERSIONS):$(PROTOC_OUT) \
--govalidators_out=gogoimport=true:$(PROTOC_OUT)
--grpc-gateway_out=$(GO_PROTO_TYPE_CONVERSIONS):$(PROTOC_OUT)

go.protos: $(wildcard api/*.proto)
$(PROTOC) $(GO_PROTOC_FLAGS) $(API_PROTO_FILES) 2>&1 | grep -vE ' protoc-gen-gogo: WARNING: failed finding publicly imported dependency for \.ttn\.lorawan\.v3\..* used in' || true
Expand Down
2 changes: 1 addition & 1 deletion .make/protos/main.make
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ API_PROTO_FILES = $(PWD)/api/'*.proto'

PROTOC_OUT ?= /out

PROTOC_DOCKER_IMAGE ?= thethingsindustries/protoc:3.0.26
PROTOC_DOCKER_IMAGE ?= thethingsindustries/protoc:3.1.0
PROTOC_DOCKER_ARGS = run --user `id -u` --rm \
--mount type=bind,src=$(PWD)/api,dst=$(PWD)/api \
--mount type=bind,src=$(PWD)/pkg/ttnpb,dst=$(PROTOC_OUT)/go.thethings.network/lorawan-stack/pkg/ttnpb \
Expand Down
16 changes: 8 additions & 8 deletions api/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1333,7 +1333,7 @@ PeerInfo
| ----- | ---- | ----- | ----------- |
| grpc_port | [uint32](#uint32) | | Port on which the gRPC server is exposed. |
| tls | [bool](#bool) | | Indicates whether the gRPC server uses TLS. |
| roles | [PeerInfo.Role](#ttn.lorawan.v3.PeerInfo.Role) | repeated | Roles of the peer () |
| roles | [PeerInfo.Role](#ttn.lorawan.v3.PeerInfo.Role) | repeated | Roles of the peer. |
| tags | [PeerInfo.TagsEntry](#ttn.lorawan.v3.PeerInfo.TagsEntry) | repeated | Tags of the peer |


Expand Down Expand Up @@ -1543,7 +1543,7 @@ SDKs are responsible for combining (if desired) the three.
| last_rj_count_1 | [uint32](#uint32) | | Last Rejoin counter value used (type 1). Stored in Join Server. |
| last_dev_status_received_at | [google.protobuf.Timestamp](#google.protobuf.Timestamp) | | Time when last DevStatus MAC command was received. Stored in Network Server. |
| power_state | [PowerState](#ttn.lorawan.v3.PowerState) | | The power state of the device; whether it is battery-powered or connected to an external power source. Received via the DevStatus MAC command at status_received_at. Stored in Network Server. |
| battery_percentage | [float](#float) | | Latest-known battery percentage of the device. Received via the DevStatus MAC command at last_dev_status_received_at or earlier. Stored in Network Server. |
| battery_percentage | [google.protobuf.FloatValue](#google.protobuf.FloatValue) | | Latest-known battery percentage of the device. Received via the DevStatus MAC command at last_dev_status_received_at or earlier. Stored in Network Server. |
| downlink_margin | [int32](#int32) | | Demodulation signal-to-noise ratio (dB). Received via the DevStatus MAC command at last_dev_status_received_at. Stored in Network Server. |
| recent_adr_uplinks | [UplinkMessage](#ttn.lorawan.v3.UplinkMessage) | repeated | Recent uplink messages with ADR bit set to 1 sorted by time. Stored in Network Server. The field is reset each time an uplink message carrying MACPayload is received with ADR bit set to 0. The number of messages stored is in the range [0,20]; |
| recent_uplinks | [UplinkMessage](#ttn.lorawan.v3.UplinkMessage) | repeated | Recent uplink messages sorted by time. Stored in Network Server. The number of messages stored may depend on configuration. |
Expand Down Expand Up @@ -1784,12 +1784,12 @@ This is used internally by the Network Server and is read only.
| class_b_timeout | [google.protobuf.Duration](#google.protobuf.Duration) | | Maximum delay for the device to answer a MAC request or a confirmed downlink frame. If unset, the default value from Network Server configuration will be used. |
| ping_slot_periodicity | [MACSettings.PingSlotPeriodValue](#ttn.lorawan.v3.MACSettings.PingSlotPeriodValue) | | Periodicity of the class B ping slot. If unset, the default value from Network Server configuration will be used. |
| ping_slot_data_rate_index | [MACSettings.DataRateIndexValue](#ttn.lorawan.v3.MACSettings.DataRateIndexValue) | | Data rate index of the class B ping slot. If unset, the default value from Network Server configuration will be used. |
| ping_slot_frequency | [uint64](#uint64) | | Frequency of the class B ping slot (Hz). If unset, the default value from Network Server configuration will be used. |
| ping_slot_frequency | [google.protobuf.UInt64Value](#google.protobuf.UInt64Value) | | Frequency of the class B ping slot (Hz). If unset, the default value from Network Server configuration will be used. |
| class_c_timeout | [google.protobuf.Duration](#google.protobuf.Duration) | | Maximum delay for the device to answer a MAC request or a confirmed downlink frame. If unset, the default value from Network Server configuration will be used. |
| rx1_delay | [MACSettings.RxDelayValue](#ttn.lorawan.v3.MACSettings.RxDelayValue) | | Class A Rx1 delay. If unset, the default value from Network Server configuration or regional parameters specification will be used. |
| rx1_data_rate_offset | [google.protobuf.UInt32Value](#google.protobuf.UInt32Value) | | Rx1 data rate offset. If unset, the default value from Network Server configuration will be used. |
| rx2_data_rate_index | [MACSettings.DataRateIndexValue](#ttn.lorawan.v3.MACSettings.DataRateIndexValue) | | Data rate index for Rx2. If unset, the default value from Network Server configuration or regional parameters specification will be used. |
| rx2_frequency | [uint64](#uint64) | | Frequency for Rx2 (Hz). If unset, the default value from Network Server configuration or regional parameters specification will be used. |
| rx2_frequency | [google.protobuf.UInt64Value](#google.protobuf.UInt64Value) | | Frequency for Rx2 (Hz). If unset, the default value from Network Server configuration or regional parameters specification will be used. |
| factory_preset_frequencies | [uint64](#uint64) | repeated | List of factory-preset frequencies. If unset, the default value from Network Server configuration or regional parameters specification will be used. |
| max_duty_cycle | [MACSettings.AggregatedDutyCycleValue](#ttn.lorawan.v3.MACSettings.AggregatedDutyCycleValue) | | Maximum uplink duty cycle (of all channels). |
| supports_32_bit_f_cnt | [google.protobuf.BoolValue](#google.protobuf.BoolValue) | | Whether the device supports 32-bit frame counters. If unset, the default value from Network Server configuration will be used. |
Expand All @@ -1801,7 +1801,7 @@ This is used internally by the Network Server and is read only.
| desired_rx1_delay | [MACSettings.RxDelayValue](#ttn.lorawan.v3.MACSettings.RxDelayValue) | | The Rx1 delay Network Server should configure device to use via MAC commands or Join-Accept. If unset, the default value from Network Server configuration or regional parameters specification will be used. |
| desired_rx1_data_rate_offset | [google.protobuf.UInt32Value](#google.protobuf.UInt32Value) | | The Rx1 data rate offset Network Server should configure device to use via MAC commands or Join-Accept. If unset, the default value from Network Server configuration will be used. |
| desired_rx2_data_rate_index | [MACSettings.DataRateIndexValue](#ttn.lorawan.v3.MACSettings.DataRateIndexValue) | | The Rx2 data rate index Network Server should configure device to use via MAC commands or Join-Accept. If unset, the default value from frequency plan, Network Server configuration or regional parameters specification will be used. |
| desired_rx2_frequency | [uint64](#uint64) | | The Rx2 frequency index Network Server should configure device to use via MAC commands. If unset, the default value from frequency plan, Network Server configuration or regional parameters specification will be used. |
| desired_rx2_frequency | [google.protobuf.UInt64Value](#google.protobuf.UInt64Value) | | The Rx2 frequency index Network Server should configure device to use via MAC commands. If unset, the default value from frequency plan, Network Server configuration or regional parameters specification will be used. |



Expand Down Expand Up @@ -3156,7 +3156,7 @@ OrganizationOrUserIdentifiers contains either organization or user identifiers.
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| payload_request | [CryptoServicePayloadRequest](#ttn.lorawan.v3.CryptoServicePayloadRequest) | | |
| join_request_type | [uint32](#uint32) | | |
| join_request_type | [RejoinType](#ttn.lorawan.v3.RejoinType) | | |
| dev_nonce | [bytes](#bytes) | | |


Expand Down Expand Up @@ -3822,7 +3822,7 @@ Only the components for which the keys were meant, will have the key-encryption-

| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| rejoin_type | [uint32](#uint32) | | |
| rejoin_type | [RejoinType](#ttn.lorawan.v3.RejoinType) | | |
| data_rate_index | [DataRateIndex](#ttn.lorawan.v3.DataRateIndex) | | |
| max_retries | [uint32](#uint32) | | |
| period_exponent | [RejoinPeriodExponent](#ttn.lorawan.v3.RejoinPeriodExponent) | | Exponent e that configures the rejoin period = 32 * 2^e + rand(0,32) seconds. |
Expand Down Expand Up @@ -5752,7 +5752,7 @@ where the user or organization is collaborator on.
| ----- | ---- | ----- | ----------- |
| frequency | [uint64](#uint64) | | Frequency (Hz). |
| radio | [uint32](#uint32) | | |
| bandwidth | [uint32](#uint32) | | |
| bandwidth | [uint32](#uint32) | | Bandwidth (Hz). |
| spreading_factor | [uint32](#uint32) | | |


Expand Down
6 changes: 3 additions & 3 deletions api/api.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -4472,7 +4472,8 @@
},
"bandwidth": {
"type": "integer",
"format": "int64"
"format": "int64",
"description": "Bandwidth (Hz)."
},
"spreading_factor": {
"type": "integer",
Expand Down Expand Up @@ -4619,8 +4620,7 @@
"type": "object",
"properties": {
"rejoin_type": {
"type": "integer",
"format": "int64"
"$ref": "#/definitions/v3RejoinType"
},
"data_rate_index": {
"$ref": "#/definitions/v3DataRateIndex"
Expand Down
21 changes: 11 additions & 10 deletions api/application.proto
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
syntax = "proto3";

import "github.com/gogo/protobuf/gogoproto/gogo.proto";
import "github.com/lyft/protoc-gen-validate/validate/validate.proto";
import "google/api/annotations.proto";
import "google/protobuf/field_mask.proto";
import "google/protobuf/timestamp.proto";
Expand All @@ -28,7 +29,7 @@ option go_package = "go.thethings.network/lorawan-stack/pkg/ttnpb";

// Application is the message that defines an Application in the network.
message Application {
ApplicationIdentifiers ids = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
ApplicationIdentifiers ids = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false, (validate.rules).message.required = true];
google.protobuf.Timestamp created_at = 2 [(gogoproto.nullable) = false, (gogoproto.stdtime) = true];
google.protobuf.Timestamp updated_at = 3 [(gogoproto.nullable) = false, (gogoproto.stdtime) = true];

Expand All @@ -43,7 +44,7 @@ message Applications {
}

message GetApplicationRequest {
ApplicationIdentifiers application_ids = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
ApplicationIdentifiers application_ids = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false, (validate.rules).message.required = true];
google.protobuf.FieldMask field_mask = 2 [(gogoproto.nullable) = false];
}

Expand All @@ -64,28 +65,28 @@ message ListApplicationsRequest {
}

message CreateApplicationRequest {
Application application = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
Application application = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false, (validate.rules).message.required = true];
// Collaborator to grant all rights on the newly created application.
OrganizationOrUserIdentifiers collaborator = 2 [(gogoproto.nullable) = false];
OrganizationOrUserIdentifiers collaborator = 2 [(gogoproto.nullable) = false, (validate.rules).message.required = true];
}

message UpdateApplicationRequest {
Application application = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
Application application = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false, (validate.rules).message.required = true];
google.protobuf.FieldMask field_mask = 2 [(gogoproto.nullable) = false];
}

message CreateApplicationAPIKeyRequest {
ApplicationIdentifiers application_ids = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
ApplicationIdentifiers application_ids = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false, (validate.rules).message.required = true];
string name = 2;
repeated Right rights = 3;
}

message UpdateApplicationAPIKeyRequest {
ApplicationIdentifiers application_ids = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
APIKey api_key = 2 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
ApplicationIdentifiers application_ids = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false, (validate.rules).message.required = true];
APIKey api_key = 2 [(gogoproto.embed) = true, (gogoproto.nullable) = false, (validate.rules).message.required = true];
}

message SetApplicationCollaboratorRequest {
ApplicationIdentifiers application_ids = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
Collaborator collaborator = 2 [(gogoproto.nullable) = false];
ApplicationIdentifiers application_ids = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false, (validate.rules).message.required = true];
Collaborator collaborator = 2 [(gogoproto.nullable) = false, (validate.rules).message.required = true];
}
14 changes: 7 additions & 7 deletions api/applicationserver.proto
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
syntax = "proto3";

import "github.com/gogo/protobuf/gogoproto/gogo.proto";
import "github.com/mwitkow/go-proto-validators/validator.proto";
import "github.com/lyft/protoc-gen-validate/validate/validate.proto";
import "google/api/annotations.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/field_mask.proto";
Expand All @@ -33,26 +33,26 @@ message ApplicationLink {
// The typical format of the address is "host:port". If the port is omitted,
// the normal port inference (with DNS lookup, otherwise defaults) is used.
// Leave empty when linking to a cluster Network Server.
string network_server_address = 1 [(validator.field) = {regex: "^(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*(?:[A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])(?::[0-9]{1,5})?$|^$"}];
string api_key = 2 [(gogoproto.customname) = "APIKey", (validator.field) = {string_not_empty: true}];
string network_server_address = 1 [(validate.rules).string.pattern = "^(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*(?:[A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])(?::[0-9]{1,5})?$|^$"];
string api_key = 2 [(gogoproto.customname) = "APIKey", (validate.rules).string.min_len = 1];
MessagePayloadFormatters default_formatters = 3;
}

message GetApplicationLinkRequest {
ApplicationIdentifiers application_ids = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
ApplicationIdentifiers application_ids = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false, (validate.rules).message.required = true];
google.protobuf.FieldMask field_mask = 2 [(gogoproto.nullable) = false];
}

message SetApplicationLinkRequest {
ApplicationIdentifiers application_ids = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
ApplicationLink link = 2 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
ApplicationIdentifiers application_ids = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false, (validate.rules).message.required = true];
ApplicationLink link = 2 [(gogoproto.embed) = true, (gogoproto.nullable) = false, (validate.rules).message.required = true];
google.protobuf.FieldMask field_mask = 3 [(gogoproto.nullable) = false];
}

// Link stats as monitored by the Application Server.
message ApplicationLinkStats {
google.protobuf.Timestamp linked_at = 1 [(gogoproto.stdtime) = true];
string network_server_address = 2;
string network_server_address = 2 [(validate.rules).string.pattern = "^(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*(?:[A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])(?::[0-9]{1,5})?$|^$"];
// Timestamp when the last upstream message has been received from a Network Server.
// This can be a join-accept, uplink message or downlink message event.
google.protobuf.Timestamp last_up_received_at = 3 [(gogoproto.stdtime) = true];
Expand Down
14 changes: 7 additions & 7 deletions api/applicationserver_web.proto
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
syntax = "proto3";

import "github.com/gogo/protobuf/gogoproto/gogo.proto";
import "github.com/mwitkow/go-proto-validators/validator.proto";
import "github.com/lyft/protoc-gen-validate/validate/validate.proto";
import "google/api/annotations.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/field_mask.proto";
Expand All @@ -27,12 +27,12 @@ package ttn.lorawan.v3;
option go_package = "go.thethings.network/lorawan-stack/pkg/ttnpb";

message ApplicationWebhookIdentifiers {
ApplicationIdentifiers application_ids = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
string webhook_id = 2 [(gogoproto.customname) = "WebhookID", (validator.field) = {regex: "^[a-z0-9](?:[-]?[a-z0-9]){2,}$" , length_lt: 37}];
ApplicationIdentifiers application_ids = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false, (validate.rules).message.required = true];
string webhook_id = 2 [(gogoproto.customname) = "WebhookID", (validate.rules).string = {pattern: "^[a-z0-9](?:[-]?[a-z0-9]){2,}$" , max_len: 36}];
}

message ApplicationWebhook {
ApplicationWebhookIdentifiers ids = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
ApplicationWebhookIdentifiers ids = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false, (validate.rules).message.required = true];
google.protobuf.Timestamp created_at = 2 [(gogoproto.nullable) = false, (gogoproto.stdtime) = true];
google.protobuf.Timestamp updated_at = 3 [(gogoproto.nullable) = false, (gogoproto.stdtime) = true];

Expand Down Expand Up @@ -68,17 +68,17 @@ message ApplicationWebhookFormats {
}

message GetApplicationWebhookRequest {
ApplicationWebhookIdentifiers ids = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
ApplicationWebhookIdentifiers ids = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false, (validate.rules).message.required = true];
google.protobuf.FieldMask field_mask = 2 [(gogoproto.nullable) = false];
}

message ListApplicationWebhooksRequest {
ApplicationIdentifiers application_ids = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
ApplicationIdentifiers application_ids = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false, (validate.rules).message.required = true];
google.protobuf.FieldMask field_mask = 2 [(gogoproto.nullable) = false];
}

message SetApplicationWebhookRequest {
ApplicationWebhook webhook = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false];
ApplicationWebhook webhook = 1 [(gogoproto.embed) = true, (gogoproto.nullable) = false, (validate.rules).message.required = true];
google.protobuf.FieldMask field_mask = 2 [(gogoproto.nullable) = false];
}

Expand Down
Loading