Skip to content

Commit

Permalink
feat: write json and string methods for producer and consumer configs (
Browse files Browse the repository at this point in the history
  • Loading branch information
oktaykcr committed Aug 24, 2024
1 parent a7389e8 commit 5e312be
Show file tree
Hide file tree
Showing 9 changed files with 113 additions and 50 deletions.
1 change: 0 additions & 1 deletion balancer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ func TestGetBalancerRoundRobinh(t *testing.T) {
}

func TestGetBalancerString(t *testing.T) {

tests := []struct {
name string
balancer Balancer
Expand Down
30 changes: 17 additions & 13 deletions consumer_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,43 +66,47 @@ type ConsumerConfig struct {
MetricPrefix string
}

func (cfg RetryConfiguration) Json() string {
return fmt.Sprintf(`{"Brokers": ["%s"], "Topic": %q, "StartTimeCron": %q, "WorkDuration": %q, "MaxRetry": %d, "VerifyTopicOnStartup": %t, "Rack": %q}`,
func (cfg RetryConfiguration) JSON() string {
return fmt.Sprintf(`{"Brokers": ["%s"], "Topic": %q, "StartTimeCron": %q, "WorkDuration": %q, `+
`"MaxRetry": %d, "VerifyTopicOnStartup": %t, "Rack": %q}`,
strings.Join(cfg.Brokers, "\", \""), cfg.Topic, cfg.StartTimeCron,
cfg.WorkDuration, cfg.MaxRetry, cfg.VerifyTopicOnStartup, cfg.Rack)
}

func (cfg *BatchConfiguration) Json() string {
func (cfg *BatchConfiguration) JSON() string {
if cfg == nil {
return "{}"
}
return fmt.Sprintf(`{"MessageGroupLimit": %d}`, cfg.MessageGroupLimit)
}

func (cfg ReaderConfig) Json() string {
return fmt.Sprintf(`{"Brokers": ["%s"], "GroupId": %q, "GroupTopics": ["%s"], "MaxWait": %q, "CommitInterval": %q, "StartOffset": %q}`,
func (cfg ReaderConfig) JSON() string {
return fmt.Sprintf(`{"Brokers": ["%s"], "GroupId": %q, "GroupTopics": ["%s"], `+
`"MaxWait": %q, "CommitInterval": %q, "StartOffset": %q}`,
strings.Join(cfg.Brokers, "\", \""), cfg.GroupID, strings.Join(cfg.GroupTopics, "\", \""),
cfg.MaxWait, cfg.CommitInterval, kcronsumer.ToStringOffset(cfg.StartOffset))
}

func (cfg *ConsumerConfig) Json() string {
func (cfg *ConsumerConfig) JSON() string {
if cfg == nil {
return "{}"
}
return fmt.Sprintf(`{"ClientID": %q, "Reader": %s, "BatchConfiguration": %s, "MessageGroupDuration": %q, "TransactionalRetry": %t, "Concurrency": %d, "RetryEnabled": %t, "RetryConfiguration": %s, "VerifyTopicOnStartup": %t, "Rack": %q, "SASL": %s, "TLS": %s}`,
cfg.ClientID, cfg.Reader.Json(), cfg.BatchConfiguration.Json(),
return fmt.Sprintf(`{"ClientID": %q, "Reader": %s, "BatchConfiguration": %s, "MessageGroupDuration": %q, `+
`"TransactionalRetry": %t, "Concurrency": %d, "RetryEnabled": %t, "RetryConfiguration": %s, `+
`"VerifyTopicOnStartup": %t, "Rack": %q, "SASL": %s, "TLS": %s}`,
cfg.ClientID, cfg.Reader.JSON(), cfg.BatchConfiguration.JSON(),
cfg.MessageGroupDuration, *cfg.TransactionalRetry, cfg.Concurrency,
cfg.RetryEnabled, cfg.RetryConfiguration.Json(), cfg.VerifyTopicOnStartup,
cfg.Rack, cfg.SASL.Json(), cfg.TLS.Json())
cfg.RetryEnabled, cfg.RetryConfiguration.JSON(), cfg.VerifyTopicOnStartup,
cfg.Rack, cfg.SASL.JSON(), cfg.TLS.JSON())
}

func (cfg *ConsumerConfig) JsonPretty() string {
return jsonPretty(cfg.Json())
func (cfg *ConsumerConfig) JSONPretty() string {
return jsonPretty(cfg.JSON())
}

func (cfg *ConsumerConfig) String() string {
re := regexp.MustCompile(`"(\w+)"\s*:`)
modifiedString := re.ReplaceAllString(cfg.Json(), `$1:`)
modifiedString := re.ReplaceAllString(cfg.JSON(), `$1:`)
modifiedString = modifiedString[1 : len(modifiedString)-1]
return modifiedString
}
Expand Down
69 changes: 56 additions & 13 deletions consumer_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,33 +228,46 @@ func Test_jsonPretty(t *testing.T) {
}
}

func TestConsumerConfig_Json(t *testing.T) {
func TestConsumerConfig_JSON(t *testing.T) {
t.Run("Should_Convert_Nil_Config_To_Json", func(t *testing.T) {
// Given
var config *ConsumerConfig
expected := "{}"
// When
result := config.Json()
result := config.JSON()
// Then
if result != expected {
t.Fatal("result must be equal to expected")
}
})
t.Run("Should_Convert_To_Json", func(t *testing.T) {
// Given
expected := "{\"ClientID\": \"test-consumer-client-id\", \"Reader\": {\"Brokers\": [\"broker-1.test.com\", \"broker-2.test.com\"], \"GroupId\": \"test-consumer.0\", \"GroupTopics\": [\"test-updated.0\"], \"MaxWait\": \"2s\", \"CommitInterval\": \"1s\", \"StartOffset\": \"earliest\"}, \"BatchConfiguration\": {\"MessageGroupLimit\": 100}, \"MessageGroupDuration\": \"20ns\", \"TransactionalRetry\": false, \"Concurrency\": 10, \"RetryEnabled\": true, \"RetryConfiguration\": {\"Brokers\": [\"broker-1.test.com\", \"broker-2.test.com\"], \"Topic\": \"test-exception.0\", \"StartTimeCron\": \"*/2 * * * *\", \"WorkDuration\": \"1m0s\", \"MaxRetry\": 3, \"VerifyTopicOnStartup\": true, \"Rack\": \"\"}, \"VerifyTopicOnStartup\": true, \"Rack\": \"stage\", \"SASL\": {\"Mechanism\": \"scram\", \"Username\": \"user\", \"Password\": \"pass\"}, \"TLS\": {\"RootCAPath\": \"resources/ca\", \"IntermediateCAPath\": \"resources/intCa\"}}"
expected := "{\"ClientID\": \"test-consumer-client-id\", \"Reader\": {\"Brokers\": [\"broker-1.test.com\", \"broker-2.test.com\"], " +
"\"GroupId\": \"test-consumer.0\", \"GroupTopics\": [\"test-updated.0\"], \"MaxWait\": \"2s\", " +
"\"CommitInterval\": \"1s\", \"StartOffset\": \"earliest\"}, \"BatchConfiguration\": {\"MessageGroupLimit\": 100}, " +
"\"MessageGroupDuration\": \"20ns\", \"TransactionalRetry\": false, \"Concurrency\": 10, \"RetryEnabled\": true, " +
"\"RetryConfiguration\": {\"Brokers\": [\"broker-1.test.com\", \"broker-2.test.com\"], \"Topic\": \"test-exception.0\", " +
"\"StartTimeCron\": \"*/2 * * * *\", \"WorkDuration\": \"1m0s\", \"MaxRetry\": 3, \"VerifyTopicOnStartup\": true, \"Rack\": \"\"}, " +
"\"VerifyTopicOnStartup\": true, \"Rack\": \"stage\", " +
"\"SASL\": {\"Mechanism\": \"scram\", \"Username\": \"user\", \"Password\": \"pass\"}, " +
"\"TLS\": {\"RootCAPath\": \"resources/ca\", \"IntermediateCAPath\": \"resources/intCa\"}}"
// When
result := getConsumerConfigExample().Json()
result := getConsumerConfigExample().JSON()
// Then
if result != expected {
t.Fatal("result must be equal to expected")
}
})
t.Run("Should_Convert_To_Json_Without_Inner_Object", func(t *testing.T) {
// Given
expected := "{\"ClientID\": \"test-consumer-client-id\", \"Reader\": {\"Brokers\": [\"\"], \"GroupId\": \"\", \"GroupTopics\": [\"\"], \"MaxWait\": \"0s\", \"CommitInterval\": \"0s\", \"StartOffset\": \"earliest\"}, \"BatchConfiguration\": {}, \"MessageGroupDuration\": \"20ns\", \"TransactionalRetry\": false, \"Concurrency\": 10, \"RetryEnabled\": true, \"RetryConfiguration\": {\"Brokers\": [\"\"], \"Topic\": \"\", \"StartTimeCron\": \"\", \"WorkDuration\": \"0s\", \"MaxRetry\": 0, \"VerifyTopicOnStartup\": false, \"Rack\": \"\"}, \"VerifyTopicOnStartup\": true, \"Rack\": \"stage\", \"SASL\": {}, \"TLS\": {}}"
expected := "{\"ClientID\": \"test-consumer-client-id\", \"Reader\": {\"Brokers\": [\"\"], \"GroupId\": \"\", " +
"\"GroupTopics\": [\"\"], \"MaxWait\": \"0s\", \"CommitInterval\": \"0s\", \"StartOffset\": \"earliest\"}, " +
"\"BatchConfiguration\": {}, \"MessageGroupDuration\": \"20ns\", \"TransactionalRetry\": false, \"Concurrency\": 10, " +
"\"RetryEnabled\": true, \"RetryConfiguration\": {\"Brokers\": [\"\"], \"Topic\": \"\", \"StartTimeCron\": \"\", " +
"\"WorkDuration\": \"0s\", \"MaxRetry\": 0, \"VerifyTopicOnStartup\": false, \"Rack\": \"\"}, \"VerifyTopicOnStartup\": true, " +
"\"Rack\": \"stage\", \"SASL\": {}, \"TLS\": {}}"
// When
result := getConsumerConfigWithoutInnerObjectExample().Json()
result := getConsumerConfigWithoutInnerObjectExample().JSON()
// Then
if result != expected {
t.Fatal("result must be equal to expected")
Expand All @@ -265,7 +278,14 @@ func TestConsumerConfig_Json(t *testing.T) {
func TestConsumerConfig_String(t *testing.T) {
t.Run("Should_Convert_To_String", func(t *testing.T) {
// Given
expected := "ClientID: \"test-consumer-client-id\", Reader: {Brokers: [\"broker-1.test.com\", \"broker-2.test.com\"], GroupId: \"test-consumer.0\", GroupTopics: [\"test-updated.0\"], MaxWait: \"2s\", CommitInterval: \"1s\", StartOffset: \"earliest\"}, BatchConfiguration: {MessageGroupLimit: 100}, MessageGroupDuration: \"20ns\", TransactionalRetry: false, Concurrency: 10, RetryEnabled: true, RetryConfiguration: {Brokers: [\"broker-1.test.com\", \"broker-2.test.com\"], Topic: \"test-exception.0\", StartTimeCron: \"*/2 * * * *\", WorkDuration: \"1m0s\", MaxRetry: 3, VerifyTopicOnStartup: true, Rack: \"\"}, VerifyTopicOnStartup: true, Rack: \"stage\", SASL: {Mechanism: \"scram\", Username: \"user\", Password: \"pass\"}, TLS: {RootCAPath: \"resources/ca\", IntermediateCAPath: \"resources/intCa\"}"
expected := "ClientID: \"test-consumer-client-id\", Reader: {Brokers: [\"broker-1.test.com\", \"broker-2.test.com\"], " +
"GroupId: \"test-consumer.0\", GroupTopics: [\"test-updated.0\"], MaxWait: \"2s\", CommitInterval: \"1s\", " +
"StartOffset: \"earliest\"}, BatchConfiguration: {MessageGroupLimit: 100}, MessageGroupDuration: \"20ns\", " +
"TransactionalRetry: false, Concurrency: 10, RetryEnabled: true, " +
"RetryConfiguration: {Brokers: [\"broker-1.test.com\", \"broker-2.test.com\"], Topic: \"test-exception.0\", " +
"StartTimeCron: \"*/2 * * * *\", WorkDuration: \"1m0s\", MaxRetry: 3, VerifyTopicOnStartup: true, Rack: \"\"}, " +
"VerifyTopicOnStartup: true, Rack: \"stage\", SASL: {Mechanism: \"scram\", Username: \"user\", Password: \"pass\"}, " +
"TLS: {RootCAPath: \"resources/ca\", IntermediateCAPath: \"resources/intCa\"}"
// When
result := getConsumerConfigExample().String()
// Then
Expand All @@ -275,7 +295,11 @@ func TestConsumerConfig_String(t *testing.T) {
})
t.Run("Should_Convert_To_String_Without_Inner_Object", func(t *testing.T) {
// Given
expected := "ClientID: \"test-consumer-client-id\", Reader: {Brokers: [\"\"], GroupId: \"\", GroupTopics: [\"\"], MaxWait: \"0s\", CommitInterval: \"0s\", StartOffset: \"earliest\"}, BatchConfiguration: {}, MessageGroupDuration: \"20ns\", TransactionalRetry: false, Concurrency: 10, RetryEnabled: true, RetryConfiguration: {Brokers: [\"\"], Topic: \"\", StartTimeCron: \"\", WorkDuration: \"0s\", MaxRetry: 0, VerifyTopicOnStartup: false, Rack: \"\"}, VerifyTopicOnStartup: true, Rack: \"stage\", SASL: {}, TLS: {}"
expected := "ClientID: \"test-consumer-client-id\", Reader: {Brokers: [\"\"], GroupId: \"\", " +
"GroupTopics: [\"\"], MaxWait: \"0s\", CommitInterval: \"0s\", StartOffset: \"earliest\"}, " +
"BatchConfiguration: {}, MessageGroupDuration: \"20ns\", TransactionalRetry: false, Concurrency: 10, " +
"RetryEnabled: true, RetryConfiguration: {Brokers: [\"\"], Topic: \"\", StartTimeCron: \"\", WorkDuration: \"0s\", " +
"MaxRetry: 0, VerifyTopicOnStartup: false, Rack: \"\"}, VerifyTopicOnStartup: true, Rack: \"stage\", SASL: {}, TLS: {}"
// When
result := getConsumerConfigWithoutInnerObjectExample().String()
// Then
Expand All @@ -285,22 +309,41 @@ func TestConsumerConfig_String(t *testing.T) {
})
}

func TestConsumerConfig_JsonPretty(t *testing.T) {
func TestConsumerConfig_JSONPretty(t *testing.T) {
t.Run("Should_Convert_To_Pretty_Json", func(t *testing.T) {
// Given
expected := "{\n\t\"ClientID\": \"test-consumer-client-id\",\n\t\"Reader\": {\n\t\t\"Brokers\": [\n\t\t\t\"broker-1.test.com\",\n\t\t\t\"broker-2.test.com\"\n\t\t],\n\t\t\"GroupId\": \"test-consumer.0\",\n\t\t\"GroupTopics\": [\n\t\t\t\"test-updated.0\"\n\t\t],\n\t\t\"MaxWait\": \"2s\",\n\t\t\"CommitInterval\": \"1s\",\n\t\t\"StartOffset\": \"earliest\"\n\t},\n\t\"BatchConfiguration\": {\n\t\t\"MessageGroupLimit\": 100\n\t},\n\t\"MessageGroupDuration\": \"20ns\",\n\t\"TransactionalRetry\": false,\n\t\"Concurrency\": 10,\n\t\"RetryEnabled\": true,\n\t\"RetryConfiguration\": {\n\t\t\"Brokers\": [\n\t\t\t\"broker-1.test.com\",\n\t\t\t\"broker-2.test.com\"\n\t\t],\n\t\t\"Topic\": \"test-exception.0\",\n\t\t\"StartTimeCron\": \"*/2 * * * *\",\n\t\t\"WorkDuration\": \"1m0s\",\n\t\t\"MaxRetry\": 3,\n\t\t\"VerifyTopicOnStartup\": true,\n\t\t\"Rack\": \"\"\n\t},\n\t\"VerifyTopicOnStartup\": true,\n\t\"Rack\": \"stage\",\n\t\"SASL\": {\n\t\t\"Mechanism\": \"scram\",\n\t\t\"Username\": \"user\",\n\t\t\"Password\": \"pass\"\n\t},\n\t\"TLS\": {\n\t\t\"RootCAPath\": \"resources/ca\",\n\t\t\"IntermediateCAPath\": \"resources/intCa\"\n\t}\n}"
expected := "{\n\t\"ClientID\": \"test-consumer-client-id\",\n\t\"Reader\": {\n\t\t\"" +
"Brokers\": [\n\t\t\t\"broker-1.test.com\",\n\t\t\t\"broker-2.test.com\"\n\t\t],\n\t\t\"" +
"GroupId\": \"test-consumer.0\",\n\t\t\"GroupTopics\": [\n\t\t\t\"test-updated.0\"\n\t\t],\n\t\t\"" +
"MaxWait\": \"2s\",\n\t\t\"CommitInterval\": \"1s\",\n\t\t\"StartOffset\": \"earliest\"\n\t},\n\t\"" +
"BatchConfiguration\": {\n\t\t\"MessageGroupLimit\": 100\n\t},\n\t\"MessageGroupDuration\": \"20ns\",\n\t\"" +
"TransactionalRetry\": false,\n\t\"Concurrency\": 10,\n\t\"RetryEnabled\": true,\n\t\"" +
"RetryConfiguration\": {\n\t\t\"Brokers\": [\n\t\t\t\"broker-1.test.com\",\n\t\t\t\"broker-2.test.com\"\n\t\t],\n\t\t\"" +
"Topic\": \"test-exception.0\",\n\t\t\"StartTimeCron\": \"*/2 * * * *\",\n\t\t\"WorkDuration\": \"1m0s\",\n\t\t\"" +
"MaxRetry\": 3,\n\t\t\"VerifyTopicOnStartup\": true,\n\t\t\"Rack\": \"\"\n\t},\n\t\"" +
"VerifyTopicOnStartup\": true,\n\t\"Rack\": \"stage\",\n\t\"" +
"SASL\": {\n\t\t\"Mechanism\": \"scram\",\n\t\t\"Username\": \"user\",\n\t\t\"Password\": \"pass\"\n\t},\n\t\"" +
"TLS\": {\n\t\t\"RootCAPath\": \"resources/ca\",\n\t\t\"IntermediateCAPath\": \"resources/intCa\"\n\t}\n}"
// When
result := getConsumerConfigExample().JsonPretty()
result := getConsumerConfigExample().JSONPretty()
// Then
if result != expected {
t.Fatal("result must be equal to expected")
}
})
t.Run("Should_Convert_To_Pretty_Json_Without_Inner_Object", func(t *testing.T) {
// Given
expected := "{\n\t\"ClientID\": \"test-consumer-client-id\",\n\t\"Reader\": {\n\t\t\"Brokers\": [\n\t\t\t\"\"\n\t\t],\n\t\t\"GroupId\": \"\",\n\t\t\"GroupTopics\": [\n\t\t\t\"\"\n\t\t],\n\t\t\"MaxWait\": \"0s\",\n\t\t\"CommitInterval\": \"0s\",\n\t\t\"StartOffset\": \"earliest\"\n\t},\n\t\"BatchConfiguration\": {},\n\t\"MessageGroupDuration\": \"20ns\",\n\t\"TransactionalRetry\": false,\n\t\"Concurrency\": 10,\n\t\"RetryEnabled\": true,\n\t\"RetryConfiguration\": {\n\t\t\"Brokers\": [\n\t\t\t\"\"\n\t\t],\n\t\t\"Topic\": \"\",\n\t\t\"StartTimeCron\": \"\",\n\t\t\"WorkDuration\": \"0s\",\n\t\t\"MaxRetry\": 0,\n\t\t\"VerifyTopicOnStartup\": false,\n\t\t\"Rack\": \"\"\n\t},\n\t\"VerifyTopicOnStartup\": true,\n\t\"Rack\": \"stage\",\n\t\"SASL\": {},\n\t\"TLS\": {}\n}"
expected := "{\n\t\"ClientID\": \"test-consumer-client-id\",\n\t\"" +
"Reader\": {\n\t\t\"Brokers\": [\n\t\t\t\"\"\n\t\t],\n\t\t\"GroupId\": \"\",\n\t\t\"" +
"GroupTopics\": [\n\t\t\t\"\"\n\t\t],\n\t\t\"MaxWait\": \"0s\",\n\t\t\"CommitInterval\": \"0s\",\n\t\t\"" +
"StartOffset\": \"earliest\"\n\t},\n\t\"BatchConfiguration\": {},\n\t\"" +
"MessageGroupDuration\": \"20ns\",\n\t\"TransactionalRetry\": false,\n\t\"Concurrency\": 10,\n\t\"" +
"RetryEnabled\": true,\n\t\"RetryConfiguration\": {\n\t\t\"Brokers\": [\n\t\t\t\"\"\n\t\t],\n\t\t\"" +
"Topic\": \"\",\n\t\t\"StartTimeCron\": \"\",\n\t\t\"WorkDuration\": \"0s\",\n\t\t\"MaxRetry\": 0,\n\t\t\"" +
"VerifyTopicOnStartup\": false,\n\t\t\"Rack\": \"\"\n\t},\n\t\"VerifyTopicOnStartup\": true,\n\t\"" +
"Rack\": \"stage\",\n\t\"SASL\": {},\n\t\"TLS\": {}\n}"
// When
result := getConsumerConfigWithoutInnerObjectExample().JsonPretty()
result := getConsumerConfigWithoutInnerObjectExample().JSONPretty()
// Then
if result != expected {
t.Fatal("result must be equal to expected")
Expand Down
3 changes: 2 additions & 1 deletion mechanism.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package kafka

import (
"fmt"

"github.com/segmentio/kafka-go/sasl"
"github.com/segmentio/kafka-go/sasl/plain"
"github.com/segmentio/kafka-go/sasl/scram"
Expand Down Expand Up @@ -39,7 +40,7 @@ func (s *SASLConfig) IsEmpty() bool {
return s == nil
}

func (s *SASLConfig) Json() string {
func (s *SASLConfig) JSON() string {
if s == nil {
return "{}"
}
Expand Down
4 changes: 2 additions & 2 deletions mechanism_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ func TestSASLConfig_Json(t *testing.T) {

expected := "{}"
// When
result := cfg.Json()
result := cfg.JSON()
// Then
if result != expected {
t.Fatal("result must be equal to expected")
Expand All @@ -25,7 +25,7 @@ func TestSASLConfig_Json(t *testing.T) {

expected := "{\"Mechanism\": \"scram\", \"Username\": \"user\", \"Password\": \"pass\"}"
// When
result := cfg.Json()
result := cfg.JSON()
// Then
if result != expected {
t.Fatal("result must be equal to expected")
Expand Down
10 changes: 5 additions & 5 deletions producer_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,21 @@ type ProducerConfig struct {

func (cfg *ProducerConfig) String() string {
re := regexp.MustCompile(`"(\w+)"\s*:`)
modifiedString := re.ReplaceAllString(cfg.Json(), `$1:`)
modifiedString := re.ReplaceAllString(cfg.JSON(), `$1:`)
modifiedString = modifiedString[1 : len(modifiedString)-1]
return modifiedString
}

func (cfg *ProducerConfig) Json() string {
func (cfg *ProducerConfig) JSON() string {
if cfg == nil {
return "{}"
}
return fmt.Sprintf(`{"Writer": %s, "ClientID": %q, "DistributedTracingEnabled": %t, "SASL": %s, "TLS": %s}`,
cfg.Writer.Json(), cfg.ClientID, cfg.DistributedTracingEnabled, cfg.SASL.Json(), cfg.TLS.Json())
cfg.Writer.Json(), cfg.ClientID, cfg.DistributedTracingEnabled, cfg.SASL.JSON(), cfg.TLS.JSON())
}

func (cfg *ProducerConfig) JsonPretty() string {
return jsonPretty(cfg.Json())
func (cfg *ProducerConfig) JSONPretty() string {
return jsonPretty(cfg.JSON())
}

func (cfg *ProducerConfig) newKafkaTransport() (*kafka.Transport, error) {
Expand Down
Loading

0 comments on commit 5e312be

Please sign in to comment.