Repository: corazawaf/coraza
Pull request #608 (2023-02-03 01:33:37)

Further simplify collections APIs
- Only interfaces are exposed, all implementations and factories moved to internal
- Remove int-converting methods from single collection, they seem too unrelated to collections and aren't hard for a user to do. String renamed to Get to better match being part of a collection
- Separates Collection from Keyed collection. Previously, we had FindString type methods implemented even on single collections, delegating to FindAll, and similar for views. This is hard to reason about - now the types are precise. Code that needs to handle generically, like ours, can still use a type check for reasonably simple code while the collections themselves become more self-explanatory
- Moves AnchoredVar to internal type keyValue

codecov-commenter: # [Codecov](https://codecov.io/gh/corazawaf/coraza/pull/608?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) Report
Base: **77.64**% // Head: **77.69**% // Increases project coverage by **`+0.04%`** :tada:
> Coverage data is based on head [(`c31c198`)](https://codecov.io/gh/corazawaf/coraza/pull/608?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) compared to base [(`f308794`)](https://codecov.io/gh/corazawaf/coraza/commit/f308794456893678d8d7eeeb83b3d210dace04f3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf).
> Patch coverage: 78.57% of modified lines in pull request are covered.

> :exclamation: Current head c31c198 differs from pull request most recent head cc5a2d6. Consider uploading reports for the commit cc5a2d6 to get more accurate results

:mega: This organization is not using Codecov’s [GitHub App Integration](https://github.com/apps/codecov). We recommend you install it so Codecov can continue to function properly for your repositories. [Learn more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf)

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           v3/dev     #608      +/-   ##
==========================================
+ Coverage   77.64%   77.69%   +0.04%     
==========================================
  Files         144      144              
  Lines        7449     7469      +20     
==========================================
+ Hits         5784     5803      +19     
+ Misses       1435     1434       -1     
- Partials      230      232       +2     
```

| Flag | Coverage Δ | |
|---|---|---|
| default | `73.32% <78.27%> (+0.05%)` | :arrow_up: |
| examples | `28.62% <51.48%> (+<0.01%)` | :arrow_up: |
| ftw | `55.11% <70.23%> (+0.09%)` | :arrow_up: |
| ftw-multiphase | `55.21% <70.23%> (+0.09%)` | :arrow_up: |
| tinygo | `71.28% <77.67%> (+0.06%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/corazawaf/coraza/pull/608?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) | Coverage Δ | |
|---|---|---|
| [actions/ctl.go](https://codecov.io/gh/corazawaf/coraza/pull/608?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-YWN0aW9ucy9jdGwuZ28=) | `71.72% <ø> (ø)` | |
| [macro/macro.go](https://codecov.io/gh/corazawaf/coraza/pull/608?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-bWFjcm8vbWFjcm8uZ28=) | `85.71% <60.00%> (-4.77%)` | :arrow_down: |
| [internal/corazawaf/transaction.go](https://codecov.io/gh/corazawaf/coraza/pull/608?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvY29yYXphd2FmL3RyYW5zYWN0aW9uLmdv) | `79.92% <66.47%> (-0.26%)` | :arrow_down: |
| [internal/collections/named.go](https://codecov.io/gh/corazawaf/coraza/pull/608?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvY29sbGVjdGlvbnMvbmFtZWQuZ28=) | `89.28% <71.42%> (ø)` | |
| [internal/collections/map.go](https://codecov.io/gh/corazawaf/coraza/pull/608?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvY29sbGVjdGlvbnMvbWFwLmdv) | `90.29% <90.29%> (ø)` | |
| [internal/collections/concat.go](https://codecov.io/gh/corazawaf/coraza/pull/608?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvY29sbGVjdGlvbnMvY29uY2F0Lmdv) | `100.00% <100.00%> (ø)` | |
| [internal/collections/single.go](https://codecov.io/gh/corazawaf/coraza/pull/608?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvY29sbGVjdGlvbnMvc2luZ2xlLmdv) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/corazawaf/coraza/pull/608?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf).


Patch:
diff --git a/actions/ctl.go b/actions/ctl.go
index 7cfceca..55847fc 100644
--- a/actions/ctl.go
+++ b/actions/ctl.go
@@ -168,7 +168,7 @@ func (a *ctlFn) Evaluate(r rules.RuleMetadata, txS rules.TransactionState) {
 	case ctlHashEnforcement:
 		// Not supported yet
 	case ctlDebugLogLevel:
-		// lvl, _ := strconv.Atoi(a.Value)
+		// lvl, _ := strconv.Atoi(a.value)
 		// TODO
 		// We cannot update the log level, it would affect the whole waf instance...
 		// tx.WAF.SetLogLevel(lvl)
diff --git a/collection/collection.go b/collection/collection.go
index ce094a6..15464ad 100644
--- a/collection/collection.go
+++ b/collection/collection.go
@@ -14,18 +14,62 @@ import (
 // to store slices of data for keys
 // Important: CollectionMaps ARE NOT concurrent safe
 type Collection interface {
+	// FindAll returns matches for all the items in this Collection.
+	FindAll() []types.MatchData
+
+	// Name returns the name for the current CollectionMap
+	Name() string
+
+	// Reset the current Collection
+	Reset()
+}
+
+// Single is a Collection with a single element.
+type Single interface {
+	Collection
+
+	// Get returns the value of this Single
+	Get() string
+
+	// Set sets the value of this Single
+	Set(string)
+}
+
+// Keyed is a Collection with elements that can be selected by key.
+type Keyed interface {
+	Collection
+
+	// Get returns a slice of strings for a key
+	Get(key string) []string
+
 	// FindRegex returns a slice of MatchData for the regex
 	FindRegex(key *regexp.Regexp) []types.MatchData
 
 	// FindString returns a slice of MatchData for the string
 	FindString(key string) []types.MatchData
+}
 
-	// FindString returns a slice of MatchData for the string
-	FindAll() []types.MatchData
+// Map are used to store VARIABLE data
+// for transactions, this data structured is designed
+// to store slices of data for keys
+// Important: CollectionMaps ARE NOT concurrent safe
+type Map interface {
+	Keyed
 
-	// Name returns the name for the current CollectionMap
-	Name() string
+	// Add a value to some key
+	Add(key string, value string)
 
-	// Reset the current CollectionMap
-	Reset()
+	// Set will replace the key's value with this slice
+	Set(key string, values []string)
+
+	// SetIndex will place the value under the index
+	// If the index is higher than the current size of the CollectionMap
+	// it will be appended
+	SetIndex(key string, index int, value string)
+
+	// Remove deletes the key from the CollectionMap
+	Remove(key string)
+
+	// Data returns all the data in the CollectionMap
+	Data() map[string][]string
 }
diff --git a/collection/collection_map.go b/collection/collection_map.go
deleted file mode 100644
index 13d2b92..0000000
--- a/collection/collection_map.go
+++ /dev/null
@@ -1,214 +0,0 @@
-// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package collection
-
-import (
-	"regexp"
-	"strings"
-
-	"github.com/corazawaf/coraza/v3/internal/corazarules"
-	"github.com/corazawaf/coraza/v3/types"
-	"github.com/corazawaf/coraza/v3/types/variables"
-)
-
-// Map are used to store VARIABLE data
-// for transactions, this data structured is designed
-// to store slices of data for keys
-// Important: CollectionMaps ARE NOT concurrent safe
-type Map interface {
-	// Get returns a slice of strings for a key
-	Get(key string) []string
-
-	// FindRegex returns a slice of MatchData for the regex
-	FindRegex(key *regexp.Regexp) []types.MatchData
-
-	// FindString returns a slice of MatchData for the string
-	FindString(key string) []types.MatchData
-
-	// FindAll returns all the contained elements
-	FindAll() []types.MatchData
-
-	// Add a value to some key
-	Add(key string, value string)
-
-	// Set will replace the key's value with this slice
-	Set(key string, values []string)
-
-	// SetIndex will place the value under the index
-	// If the index is higher than the current size of the CollectionMap
-	// it will be appended
-	SetIndex(key string, index int, value string)
-
-	// Remove deletes the key from the CollectionMap
-	Remove(key string)
-
-	// Name returns the name for the current CollectionMap
-	Name() string
-
-	// Reset the current CollectionMap
-	Reset()
-
-	// Data returns all the data in the CollectionMap
-	Data() map[string][]string
-}
-
-// Map are used to store VARIABLE data
-// for transactions, this data structured is designed
-// to store slices of data for keys
-// Important: CollectionMaps ARE NOT concurrent safe
-type defaultMap struct {
-	data     map[string][]types.AnchoredVar
-	name     string
-	variable variables.RuleVariable
-}
-
-// Get returns a slice of strings for a key
-func (c *defaultMap) Get(key string) []string {
-	if len(c.data) == 0 {
-		return nil
-	}
-	keyL := strings.ToLower(key)
-	var values []string
-	for _, a := range c.data[keyL] {
-		values = append(values, a.Value)
-	}
-	return values
-}
-
-// FindRegex returns a slice of MatchData for the regex
-func (c *defaultMap) FindRegex(key *regexp.Regexp) []types.MatchData {
-	var result []types.MatchData
-	for k, data := range c.data {
-		if key.MatchString(k) {
-			for _, d := range data {
-				result = append(result, &corazarules.MatchData{
-					VariableName_: c.name,
-					Variable_:     c.variable,
-					Key_:          d.Name,
-					Value_:        d.Value,
-				})
-			}
-		}
-	}
-	return result
-}
-
-// FindString returns a slice of MatchData for the string
-func (c *defaultMap) FindString(key string) []types.MatchData {
-	var result []types.MatchData
-	if key == "" {
-		return c.FindAll()
-	}
-	if len(c.data) == 0 {
-		return nil
-	}
-	keyL := strings.ToLower(key)
-	// if key is not empty
-	if e, ok := c.data[keyL]; ok {
-		for _, aVar := range e {
-			result = append(result, &corazarules.MatchData{
-				VariableName_: c.name,
-				Variable_:     c.variable,
-				Key_:          aVar.Name,
-				Value_:        aVar.Value,
-			})
-		}
-	}
-	return result
-}
-
-// FindAll returns all the contained elements
-func (c *defaultMap) FindAll() []types.MatchData {
-	var result []types.MatchData
-	for _, data := range c.data {
-		for _, d := range data {
-			result = append(result, &corazarules.MatchData{
-				VariableName_: c.name,
-				Variable_:     c.variable,
-				Key_:          d.Name,
-				Value_:        d.Value,
-			})
-		}
-	}
-	return result
-}
-
-// Add a value to some key
-func (c *defaultMap) Add(key string, value string) {
-	keyL := strings.ToLower(key)
-	aVal := types.AnchoredVar{Name: key, Value: value}
-	c.data[keyL] = append(c.data[keyL], aVal)
-}
-
-// Set will replace the key's value with this slice
-// internally converts [] string to []types.AnchoredVar
-func (c *defaultMap) Set(key string, values []string) {
-	keyL := strings.ToLower(key)
-	c.data[keyL] = make([]types.AnchoredVar, 0, len(values))
-	for _, v := range values {
-		c.data[keyL] = append(c.data[keyL], types.AnchoredVar{Name: key, Value: v})
-	}
-}
-
-// SetIndex will place the value under the index
-// If the index is higher than the current size of the CollectionMap
-// it will be appended
-func (c *defaultMap) SetIndex(key string, index int, value string) {
-	keyL := strings.ToLower(key)
-	values := c.data[keyL]
-	av := types.AnchoredVar{Name: key, Value: value}
-
-	switch {
-	case len(values) == 0:
-		c.data[keyL] = []types.AnchoredVar{av}
-	case len(values) <= index:
-		c.data[keyL] = append(c.data[keyL], av)
-	default:
-		c.data[keyL][index] = av
-	}
-}
-
-// Remove deletes the key from the CollectionMap
-func (c *defaultMap) Remove(key string) {
-	if len(c.data) == 0 {
-		return
-	}
-	keyL := strings.ToLower(key)
-	delete(c.data, keyL)
-}
-
-// Name returns the name for the current CollectionMap
-func (c *defaultMap) Name() string {
-	return c.name
-}
-
-// Reset the current CollectionMap
-func (c *defaultMap) Reset() {
-	for k := range c.data {
-		delete(c.data, k)
-	}
-}
-
-// Data returns all the data in the CollectionMap
-func (c *defaultMap) Data() map[string][]string {
-	result := map[string][]string{}
-	for k, v := range c.data {
-		result[k] = make([]string, 0, len(v))
-		for _, a := range v {
-			result[k] = append(result[k], a.Value)
-		}
-	}
-	return result
-}
-
-var _ Collection = &defaultMap{}
-
-// NewMap returns a collection of key->[]values
-func NewMap(variable variables.RuleVariable) Map {
-	return &defaultMap{
-		name:     variable.Name(),
-		variable: variable,
-		data:     map[string][]types.AnchoredVar{},
-	}
-}
diff --git a/collection/collection_simple.go b/collection/collection_simple.go
deleted file mode 100644
index a7044f9..0000000
--- a/collection/collection_simple.go
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package collection
-
-import (
-	"regexp"
-	"strconv"
-
-	"github.com/corazawaf/coraza/v3/internal/corazarules"
-	"github.com/corazawaf/coraza/v3/types"
-	"github.com/corazawaf/coraza/v3/types/variables"
-)
-
-// Simple are used to store VARIABLE data
-// for transactions, this data structured is designed
-// to store slices of data for keys
-// Important: CollectionSimples ARE NOT concurrent safe
-type Simple struct {
-	data     string
-	name     string
-	variable variables.RuleVariable
-}
-
-// FindRegex returns a slice of MatchData for the regex
-func (c *Simple) FindRegex(key *regexp.Regexp) []types.MatchData {
-	return c.FindAll()
-}
-
-// FindString returns a slice of MatchData for the string
-func (c *Simple) FindString(key string) []types.MatchData {
-	return c.FindAll()
-}
-
-// FindAll returns a single MatchData for the current data
-func (c *Simple) FindAll() []types.MatchData {
-	return []types.MatchData{
-		&corazarules.MatchData{
-			VariableName_: c.name,
-			Variable_:     c.variable,
-			Value_:        c.data,
-		},
-	}
-}
-
-// String returns the first string occurrence of a key
-func (c *Simple) String() string {
-	return c.data
-}
-
-// Int64 returns the first int64 occurrence of a key
-func (c *Simple) Int64() int64 {
-	return int64(c.Int())
-}
-
-// Int returns the first int occurrence of a key
-func (c *Simple) Int() int {
-	r, _ := strconv.ParseInt(c.data, 10, 32)
-	return int(r)
-}
-
-// Set will replace the key's value with this slice
-// internally converts [] string to []types.AnchoredVar
-func (c *Simple) Set(value string) {
-	c.data = value
-}
-
-// Name returns the name for the current CollectionSimple
-func (c *Simple) Name() string {
-	return c.name
-}
-
-// Reset the current CollectionSimple
-func (c *Simple) Reset() {
-	c.data = ""
-}
-
-var _ Collection = &Simple{}
-
-// NewSimple creates a new CollectionSimple
-func NewSimple(variable variables.RuleVariable) *Simple {
-	return &Simple{
-		variable: variable,
-		name:     variable.Name(),
-	}
-}
diff --git a/internal/collections/concat.go b/internal/collections/concat.go
index cb48e80..ff6f459 100644
--- a/internal/collections/concat.go
+++ b/internal/collections/concat.go
@@ -5,12 +5,14 @@ package collections
 
 import (
 	"regexp"
+	"strings"
 
 	"github.com/corazawaf/coraza/v3/collection"
 	"github.com/corazawaf/coraza/v3/types"
 	"github.com/corazawaf/coraza/v3/types/variables"
 )
 
+// ConcatCollection is a collection view over multiple sollections.
 type ConcatCollection struct {
 	data     []collection.Collection
 	name     string
@@ -27,8 +29,52 @@ func NewConcatCollection(variable variables.RuleVariable, data ...collection.Col
 	}
 }
 
+// FindAll returns all matches for all collections
+func (c *ConcatCollection) FindAll() []types.MatchData {
+	var res []types.MatchData
+	for _, c := range c.data {
+		res = append(res, c.FindAll()...)
+	}
+	return res
+}
+
+// Name returns the name for the current CollectionconcatCollection
+func (c *ConcatCollection) Name() string {
+	return c.name
+}
+
+// Reset the current ConcatMap
+func (c *ConcatCollection) Reset() {
+}
+
+// ConcatKeyed is a collection view over multiple keyed collections.
+type ConcatKeyed struct {
+	data     []collection.Keyed
+	name     string
+	variable variables.RuleVariable
+}
+
+var _ collection.Keyed = &ConcatKeyed{}
+
+func NewConcatKeyed(variable variables.RuleVariable, data ...collection.Keyed) *ConcatKeyed {
+	return &ConcatKeyed{
+		data:     data,
+		name:     variable.Name(),
+		variable: variable,
+	}
+}
+
+func (c *ConcatKeyed) Get(key string) []string {
+	keyL := strings.ToLower(key)
+	var res []string
+	for _, c := range c.data {
+		res = append(res, c.Get(keyL)...)
+	}
+	return res
+}
+
 // FindRegex returns a slice of MatchData for the regex
-func (c *ConcatCollection) FindRegex(key *regexp.Regexp) []types.MatchData {
+func (c *ConcatKeyed) FindRegex(key *regexp.Regexp) []types.MatchData {
 	var res []types.MatchData
 	for _, c := range c.data {
 		res = append(res, c.FindRegex(key)...)
@@ -37,7 +83,7 @@ func (c *ConcatCollection) FindRegex(key *regexp.Regexp) []types.MatchData {
 }
 
 // FindString returns a slice of MatchData for the string
-func (c *ConcatCollection) FindString(key string) []types.MatchData {
+func (c *ConcatKeyed) FindString(key string) []types.MatchData {
 	var res []types.MatchData
 	for _, c := range c.data {
 		res = append(res, c.FindString(key)...)
@@ -46,7 +92,7 @@ func (c *ConcatCollection) FindString(key string) []types.MatchData {
 }
 
 // FindAll returns all matches for all collections
-func (c *ConcatCollection) FindAll() []types.MatchData {
+func (c *ConcatKeyed) FindAll() []types.MatchData {
 	var res []types.MatchData
 	for _, c := range c.data {
 		res = append(res, c.FindAll()...)
@@ -55,10 +101,10 @@ func (c *ConcatCollection) FindAll() []types.MatchData {
 }
 
 // Name returns the name for the current CollectionconcatCollection
-func (c *ConcatCollection) Name() string {
+func (c *ConcatKeyed) Name() string {
 	return c.name
 }
 
-// Reset the current ConcatCollection
-func (c *ConcatCollection) Reset() {
+// Reset the current ConcatMap
+func (c *ConcatKeyed) Reset() {
 }
diff --git a/internal/collections/map.go b/internal/collections/map.go
new file mode 100644
index 0000000..d20b84c
--- /dev/null
+++ b/internal/collections/map.go
@@ -0,0 +1,163 @@
+// Copyright 2023 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package collections
+
+import (
+	"regexp"
+	"strings"
+
+	"github.com/corazawaf/coraza/v3/collection"
+	"github.com/corazawaf/coraza/v3/internal/corazarules"
+	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/types/variables"
+)
+
+// Map is a default collection.Map.
+type Map struct {
+	data     map[string][]keyValue
+	name     string
+	variable variables.RuleVariable
+}
+
+var _ collection.Map = &Map{}
+
+func NewMap(variable variables.RuleVariable) collection.Map {
+	return &Map{
+		name:     variable.Name(),
+		variable: variable,
+		data:     map[string][]keyValue{},
+	}
+}
+
+func (c *Map) Get(key string) []string {
+	if len(c.data) == 0 {
+		return nil
+	}
+	keyL := strings.ToLower(key)
+	var values []string
+	for _, a := range c.data[keyL] {
+		values = append(values, a.value)
+	}
+	return values
+}
+
+func (c *Map) FindRegex(key *regexp.Regexp) []types.MatchData {
+	var result []types.MatchData
+	for k, data := range c.data {
+		if key.MatchString(k) {
+			for _, d := range data {
+				result = append(result, &corazarules.MatchData{
+					VariableName_: c.name,
+					Variable_:     c.variable,
+					Key_:          d.key,
+					Value_:        d.value,
+				})
+			}
+		}
+	}
+	return result
+}
+
+func (c *Map) FindString(key string) []types.MatchData {
+	var result []types.MatchData
+	if key == "" {
+		return c.FindAll()
+	}
+	if len(c.data) == 0 {
+		return nil
+	}
+	keyL := strings.ToLower(key)
+	// if key is not empty
+	if e, ok := c.data[keyL]; ok {
+		for _, aVar := range e {
+			result = append(result, &corazarules.MatchData{
+				VariableName_: c.name,
+				Variable_:     c.variable,
+				Key_:          aVar.key,
+				Value_:        aVar.value,
+			})
+		}
+	}
+	return result
+}
+
+func (c *Map) FindAll() []types.MatchData {
+	var result []types.MatchData
+	for _, data := range c.data {
+		for _, d := range data {
+			result = append(result, &corazarules.MatchData{
+				VariableName_: c.name,
+				Variable_:     c.variable,
+				Key_:          d.key,
+				Value_:        d.value,
+			})
+		}
+	}
+	return result
+}
+
+func (c *Map) Add(key string, value string) {
+	keyL := strings.ToLower(key)
+	aVal := keyValue{key: key, value: value}
+	c.data[keyL] = append(c.data[keyL], aVal)
+}
+
+func (c *Map) Set(key string, values []string) {
+	keyL := strings.ToLower(key)
+	c.data[keyL] = make([]keyValue, 0, len(values))
+	for _, v := range values {
+		c.data[keyL] = append(c.data[keyL], keyValue{key: key, value: v})
+	}
+}
+
+func (c *Map) SetIndex(key string, index int, value string) {
+	keyL := strings.ToLower(key)
+	values := c.data[keyL]
+	av := keyValue{key: key, value: value}
+
+	switch {
+	case len(values) == 0:
+		c.data[keyL] = []keyValue{av}
+	case len(values) <= index:
+		c.data[keyL] = append(c.data[keyL], av)
+	default:
+		c.data[keyL][index] = av
+	}
+}
+
+func (c *Map) Remove(key string) {
+	if len(c.data) == 0 {
+		return
+	}
+	keyL := strings.ToLower(key)
+	delete(c.data, keyL)
+}
+
+func (c *Map) Name() string {
+	return c.name
+}
+
+func (c *Map) Reset() {
+	for k := range c.data {
+		delete(c.data, k)
+	}
+}
+
+func (c *Map) Data() map[string][]string {
+	result := map[string][]string{}
+	for k, v := range c.data {
+		result[k] = make([]string, 0, len(v))
+		for _, a := range v {
+			result[k] = append(result[k], a.value)
+		}
+	}
+	return result
+}
+
+// keyValue stores the case preserved original key and value
+// of the variable
+type keyValue struct {
+	key   string
+	value string
+}
diff --git a/internal/collections/named.go b/internal/collections/named.go
index d1ff88d..fb19d86 100644
--- a/internal/collections/named.go
+++ b/internal/collections/named.go
@@ -22,7 +22,7 @@ var _ collection.Map = &NamedCollection{}
 
 func NewNamedCollection(rv variables.RuleVariable) *NamedCollection {
 	return &NamedCollection{
-		Map: collection.NewMap(rv),
+		Map: NewMap(rv),
 	}
 }
 
@@ -72,7 +72,7 @@ func (c *NamedCollection) Data() map[string][]string {
 }
 
 func (c *NamedCollection) Names(rv variables.RuleVariable) collection.Collection {
-	return &namedCollectionNames{
+	return &NamedCollectionNames{
 		name:       rv.Name(),
 		variable:   rv,
 		collection: c,
@@ -88,21 +88,21 @@ func (c *NamedCollection) addName(key string) {
 	c.names = append(c.names, key)
 }
 
-type namedCollectionNames struct {
+type NamedCollectionNames struct {
 	name       string
 	variable   variables.RuleVariable
 	collection *NamedCollection
 }
 
-func (c *namedCollectionNames) FindRegex(key *regexp.Regexp) []types.MatchData {
+func (c *NamedCollectionNames) FindRegex(key *regexp.Regexp) []types.MatchData {
 	panic("selection operator not supported")
 }
 
-func (c *namedCollectionNames) FindString(key string) []types.MatchData {
+func (c *NamedCollectionNames) FindString(key string) []types.MatchData {
 	panic("selection operator not supported")
 }
 
-func (c *namedCollectionNames) FindAll() []types.MatchData {
+func (c *NamedCollectionNames) FindAll() []types.MatchData {
 	var res []types.MatchData
 	for _, k := range c.collection.names {
 		res = append(res, &corazarules.MatchData{
@@ -114,9 +114,9 @@ func (c *namedCollectionNames) FindAll() []types.MatchData {
 	return res
 }
 
-func (c *namedCollectionNames) Name() string {
+func (c *NamedCollectionNames) Name() string {
 	return c.name
 }
 
-func (c *namedCollectionNames) Reset() {
+func (c *NamedCollectionNames) Reset() {
 }
diff --git a/internal/collections/single.go b/internal/collections/single.go
new file mode 100644
index 0000000..9b42edb
--- /dev/null
+++ b/internal/collections/single.go
@@ -0,0 +1,53 @@
+// Copyright 2023 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package collections
+
+import (
+	"github.com/corazawaf/coraza/v3/collection"
+	"github.com/corazawaf/coraza/v3/internal/corazarules"
+	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/types/variables"
+)
+
+type Single struct {
+	data     string
+	name     string
+	variable variables.RuleVariable
+}
+
+var _ collection.Single = &Single{}
+
+// NewSingle creates a new Single.
+func NewSingle(variable variables.RuleVariable) *Single {
+	return &Single{
+		variable: variable,
+		name:     variable.Name(),
+	}
+}
+
+func (c *Single) FindAll() []types.MatchData {
+	return []types.MatchData{
+		&corazarules.MatchData{
+			VariableName_: c.name,
+			Variable_:     c.variable,
+			Value_:        c.data,
+		},
+	}
+}
+
+func (c *Single) Get() string {
+	return c.data
+}
+
+func (c *Single) Set(value string) {
+	c.data = value
+}
+
+func (c *Single) Name() string {
+	return c.name
+}
+
+func (c *Single) Reset() {
+	c.data = ""
+}
diff --git a/internal/corazawaf/transaction.go b/internal/corazawaf/transaction.go
index adf76da..bc03f0b 100644
--- a/internal/corazawaf/transaction.go
+++ b/internal/corazawaf/transaction.go
@@ -499,16 +499,16 @@ func (tx *Transaction) MatchRule(r *Rule, mds []types.MatchData) {
 
 	// set highest_severity
 	hs := tx.variables.highestSeverity
-	maxSeverity, _ := types.ParseRuleSeverity(hs.String())
+	maxSeverity, _ := types.ParseRuleSeverity(hs.Get())
 	if r.Severity_ > maxSeverity {
 		hs.Set(strconv.Itoa(r.Severity_.Int()))
 	}
 
 	mr := &corazarules.MatchedRule{
-		URI_:             tx.variables.requestURI.String(),
+		URI_:             tx.variables.requestURI.Get(),
 		TransactionID_:   tx.id,
-		ServerIPAddress_: tx.variables.serverAddr.String(),
-		ClientIPAddress_: tx.variables.remoteAddr.String(),
+		ServerIPAddress_: tx.variables.serverAddr.Get(),
+		ClientIPAddress_: tx.variables.remoteAddr.Get(),
 		Rule_:            &r.RuleMetadata,
 		MatchedDatas_:    mds,
 	}
@@ -546,7 +546,6 @@ func (tx *Transaction) GetStopWatch() string {
 // In future releases we may remove de exceptions slice and
 // make it easier to use
 func (tx *Transaction) GetField(rv ruleVariableParams) []types.MatchData {
-	collection := rv.Variable
 	col := tx.Collection(rv.Variable)
 	if col == nil {
 		return []types.MatchData{}
@@ -554,14 +553,21 @@ func (tx *Transaction) GetField(rv ruleVariableParams) []types.MatchData {
 
 	var matches []types.MatchData
 	// Now that we have access to the collection, we can apply the exceptions
-	if rv.KeyRx == nil {
-		if len(rv.KeyStr) == 0 {
-			matches = col.FindAll()
+	switch {
+	case rv.KeyRx != nil:
+		if m, ok := col.(collection.Keyed); ok {
+			matches = m.FindRegex(rv.KeyRx)
 		} else {
-			matches = col.FindString(rv.KeyStr)
+			panic("attempted to use regex with non-selectable collection: " + rv.Variable.Name())
 		}
-	} else {
-		matches = col.FindRegex(rv.KeyRx)
+	case rv.KeyStr != "":
+		if m, ok := col.(collection.Keyed); ok {
+			matches = m.FindString(rv.KeyStr)
+		} else {
+			panic("attempted to use string with non-selectable collection: " + rv.Variable.Name())
+		}
+	default:
+		matches = col.FindAll()
 	}
 
 	var rmi []int
@@ -591,8 +597,8 @@ func (tx *Transaction) GetField(rv ruleVariableParams) []types.MatchData {
 		count := len(matches)
 		matches = []types.MatchData{
 			&corazarules.MatchData{
-				VariableName_: collection.Name(),
-				Variable_:     collection,
+				VariableName_: rv.Variable.Name(),
+				Variable_:     rv.Variable,
 				Key_:          rv.KeyStr,
 				Value_:        strconv.Itoa(count),
 			},
@@ -952,7 +958,7 @@ func (tx *Transaction) ProcessRequestBody() (*types.Interruption, error) {
 		return nil, err
 	}
 
-	rbp := tx.variables.reqbodyProcessor.String()
+	rbp := tx.variables.reqbodyProcessor.Get()
 
 	// Default variables.ReqbodyProcessor values
 	// XML and JSON must be forced with ctl:requestBodyProcessor=JSON
@@ -1024,7 +1030,7 @@ func (tx *Transaction) ProcessResponseHeaders(code int, proto string) *types.Int
 // directly to the client or write them to Coraza's buffer.
 func (tx *Transaction) IsResponseBodyProcessable() bool {
 	// TODO add more validations
-	ct := tx.variables.responseContentType.String()
+	ct := tx.variables.responseContentType.Get()
 	return stringsutil.InSlice(ct, tx.WAF.ResponseBodyMimeTypes)
 }
 
@@ -1218,7 +1224,7 @@ func (tx *Transaction) ProcessLogging() {
 
 	if tx.AuditEngine == types.AuditEngineRelevantOnly && tx.audit {
 		re := tx.WAF.AuditLogRelevantStatus
-		status := tx.variables.responseStatus.String()
+		status := tx.variables.responseStatus.Get()
 		if re != nil && !re.Match([]byte(status)) {
 			// Not relevant status
 			tx.WAF.Logger.Debug("[%s] Transaction status not marked for audit logging", tx.id)
@@ -1270,35 +1276,38 @@ func (tx *Transaction) AuditLog() *loggers.AuditLog {
 	// YYYY/MM/DD HH:mm:ss
 	ts := time.Unix(0, tx.Timestamp).Format("2006/01/02 15:04:05")
 	al.Parts = tx.AuditLogParts
+	clientPort, _ := strconv.Atoi(tx.variables.remotePort.Get())
+	hostPort, _ := strconv.Atoi(tx.variables.serverPort.Get())
+	status, _ := strconv.Atoi(tx.variables.responseStatus.Get())
 	al.Transaction = loggers.AuditTransaction{
 		Timestamp:     ts,
 		UnixTimestamp: tx.Timestamp,
 		ID:            tx.id,
-		ClientIP:      tx.variables.remoteAddr.String(),
-		ClientPort:    tx.variables.remotePort.Int(),
-		HostIP:        tx.variables.serverAddr.String(),
-		HostPort:      tx.variables.serverPort.Int(),
-		ServerID:      tx.variables.serverName.String(), // TODO check
+		ClientIP:      tx.variables.remoteAddr.Get(),
+		ClientPort:    clientPort,
+		HostIP:        tx.variables.serverAddr.Get(),
+		HostPort:      hostPort,
+		ServerID:      tx.variables.serverName.Get(), // TODO check
 		Request: loggers.AuditTransactionRequest{
-			Method:      tx.variables.requestMethod.String(),
-			Protocol:    tx.variables.requestProtocol.String(),
-			URI:         tx.variables.requestURI.String(),
-			HTTPVersion: tx.variables.requestProtocol.String(),
+			Method:      tx.variables.requestMethod.Get(),
+			Protocol:    tx.variables.requestProtocol.Get(),
+			URI:         tx.variables.requestURI.Get(),
+			HTTPVersion: tx.variables.requestProtocol.Get(),
 			// Body and headers are audit variables.RequestUriRaws
 		},
 		Response: loggers.AuditTransactionResponse{
-			Status: tx.variables.responseStatus.Int(),
+			Status: status,
 			// body and headers are audit parts
 		},
 	}
 	rengine := tx.RuleEngine.String()
 
 	al.Transaction.Request.Headers = tx.variables.requestHeaders.Data()
-	al.Transaction.Request.Body = tx.variables.requestBody.String()
+	al.Transaction.Request.Body = tx.variables.requestBody.Get()
 	// TODO maybe change to:
 	// al.Transaction.Request.Body = tx.RequestBodyBuffer.String()
 	al.Transaction.Response.Headers = tx.variables.responseHeaders.Data()
-	al.Transaction.Response.Body = tx.variables.responseBody.String()
+	al.Transaction.Response.Body = tx.variables.responseBody.Get()
 	al.Transaction.Producer = loggers.AuditTransactionProducer{
 		Connector:  tx.WAF.ProducerConnector,
 		Version:    tx.WAF.ProducerConnectorVersion,
@@ -1395,7 +1404,8 @@ func (tx *Transaction) String() string {
 func (tx *Transaction) Debug() string {
 	res := "\n\n----------------------- ERRORLOG ----------------------\n"
 	for _, mr := range tx.matchedRules {
-		res += mr.ErrorLog(tx.variables.responseStatus.Int())
+		status, _ := strconv.Atoi(tx.variables.responseStatus.Get())
+		res += mr.ErrorLog(status)
 		res += "\n\n----------------------- MATCHDATA ---------------------\n"
 		for _, md := range mr.MatchedDatas() {
 			res += fmt.Sprintf("%+v", md) + "\n"
@@ -1411,9 +1421,9 @@ func (tx *Transaction) Debug() string {
 		}
 		data := map[string][]string{}
 		switch col := tx.Collection(vr).(type) {
-		case *collection.Simple:
+		case *collections.Single:
 			data[""] = []string{
-				col.String(),
+				col.Get(),
 			}
 		case collection.Map:
 			data = col.Data()
@@ -1439,77 +1449,77 @@ func (tx *Transaction) Debug() string {
 // generateReqbodyError generates all the error variables for the request body parser
 func (tx *Transaction) generateReqbodyError(err error) {
 	tx.variables.reqbodyError.Set("1")
-	tx.variables.reqbodyErrorMsg.Set(fmt.Sprintf("%s: %s", tx.variables.reqbodyProcessor.String(), err.Error()))
+	tx.variables.reqbodyErrorMsg.Set(fmt.Sprintf("%s: %s", tx.variables.reqbodyProcessor.Get(), err.Error()))
 	tx.variables.reqbodyProcessorError.Set("1")
 	tx.variables.reqbodyProcessorErrorMsg.Set(string(err.Error()))
 }
 
 // TransactionVariables has pointers to all the variables of the transaction
 type TransactionVariables struct {
-	// Simple Variables
-	userID                        *collection.Simple
-	urlencodedError               *collection.Simple
-	responseContentType           *collection.Simple
-	uniqueID                      *collection.Simple
+	// Single Variables
+	userID                        *collections.Single
+	urlencodedError               *collections.Single
+	responseContentType           *collections.Single
+	uniqueID                      *collections.Single
 	argsCombinedSize              *collections.SizeCollection
-	authType                      *collection.Simple
-	filesCombinedSize             *collection.Simple
-	fullRequest                   *collection.Simple
-	fullRequestLength             *collection.Simple
-	inboundDataError              *collection.Simple
-	matchedVar                    *collection.Simple
-	matchedVarName                *collection.Simple
-	multipartBoundaryQuoted       *collection.Simple
-	multipartBoundaryWhitespace   *collection.Simple
-	multipartCrlfLfLines          *collection.Simple
-	multipartDataAfter            *collection.Simple
-	multipartDataBefore           *collection.Simple
-	multipartFileLimitExceeded    *collection.Simple
-	multipartHeaderFolding        *collection.Simple
-	multipartInvalidHeaderFolding *collection.Simple
-	multipartInvalidPart          *collection.Simple
-	multipartInvalidQuoting       *collection.Simple
-	multipartLfLine               *collection.Simple
-	multipartMissingSemicolon     *collection.Simple
-	multipartStrictError          *collection.Simple
-	multipartUnmatchedBoundary    *collection.Simple
-	outboundDataError             *collection.Simple
-	pathInfo                      *collection.Simple
-	queryString                   *collection.Simple
-	remoteAddr                    *collection.Simple
-	remoteHost                    *collection.Simple
-	remotePort                    *collection.Simple
-	reqbodyError                  *collection.Simple
-	reqbodyErrorMsg               *collection.Simple
-	reqbodyProcessorError         *collection.Simple
-	reqbodyProcessorErrorMsg      *collection.Simple
-	reqbodyProcessor              *collection.Simple
-	requestBasename               *collection.Simple
-	requestBody                   *collection.Simple
-	requestBodyLength             *collection.Simple
-	requestFilename               *collection.Simple
-	requestLine                   *collection.Simple
-	requestMethod                 *collection.Simple
-	requestProtocol               *collection.Simple
-	requestURI                    *collection.Simple
-	requestURIRaw                 *collection.Simple
-	responseBody                  *collection.Simple
-	responseContentLength         *collection.Simple
-	responseProtocol              *collection.Simple
-	responseStatus                *collection.Simple
-	serverAddr                    *collection.Simple
-	serverName                    *collection.Simple
-	serverPort                    *collection.Simple
-	sessionID                     *collection.Simple
-	highestSeverity               *collection.Simple
-	statusLine                    *collection.Simple
-	inboundErrorData              *collection.Simple
+	authType                      *collections.Single
+	filesCombinedSize             *collections.Single
+	fullRequest                   *collections.Single
+	fullRequestLength             *collections.Single
+	inboundDataError              *collections.Single
+	matchedVar                    *collections.Single
+	matchedVarName                *collections.Single
+	multipartBoundaryQuoted       *collections.Single
+	multipartBoundaryWhitespace   *collections.Single
+	multipartCrlfLfLines          *collections.Single
+	multipartDataAfter            *collections.Single
+	multipartDataBefore           *collections.Single
+	multipartFileLimitExceeded    *collections.Single
+	multipartHeaderFolding        *collections.Single
+	multipartInvalidHeaderFolding *collections.Single
+	multipartInvalidPart          *collections.Single
+	multipartInvalidQuoting       *collections.Single
+	multipartLfLine               *collections.Single
+	multipartMissingSemicolon     *collections.Single
+	multipartStrictError          *collections.Single
+	multipartUnmatchedBoundary    *collections.Single
+	outboundDataError             *collections.Single
+	pathInfo                      *collections.Single
+	queryString                   *collections.Single
+	remoteAddr                    *collections.Single
+	remoteHost                    *collections.Single
+	remotePort                    *collections.Single
+	reqbodyError                  *collections.Single
+	reqbodyErrorMsg               *collections.Single
+	reqbodyProcessorError         *collections.Single
+	reqbodyProcessorErrorMsg      *collections.Single
+	reqbodyProcessor              *collections.Single
+	requestBasename               *collections.Single
+	requestBody                   *collections.Single
+	requestBodyLength             *collections.Single
+	requestFilename               *collections.Single
+	requestLine                   *collections.Single
+	requestMethod                 *collections.Single
+	requestProtocol               *collections.Single
+	requestURI                    *collections.Single
+	requestURIRaw                 *collections.Single
+	responseBody                  *collections.Single
+	responseContentLength         *collections.Single
+	responseProtocol              *collections.Single
+	responseStatus                *collections.Single
+	serverAddr                    *collections.Single
+	serverName                    *collections.Single
+	serverPort                    *collections.Single
+	sessionID                     *collections.Single
+	highestSeverity               *collections.Single
+	statusLine                    *collections.Single
+	inboundErrorData              *collections.Single
 	// Custom
 	env                  collection.Map
 	tx                   collection.Map
 	rule                 collection.Map
-	duration             *collection.Simple
-	args                 *collections.ConcatCollection
+	duration             *collections.Single
+	args                 *collections.ConcatKeyed
 	argsGet              *collections.NamedCollection
 	argsGetNames         collection.Collection
 	argsPost             *collections.NamedCollection
@@ -1542,68 +1552,68 @@ type TransactionVariables struct {
 
 func NewTransactionVariables() *TransactionVariables {
 	v := &TransactionVariables{}
-	v.urlencodedError = collection.NewSimple(variables.UrlencodedError)
-	v.responseContentType = collection.NewSimple(variables.ResponseContentType)
-	v.uniqueID = collection.NewSimple(variables.UniqueID)
-	v.authType = collection.NewSimple(variables.AuthType)
-	v.filesCombinedSize = collection.NewSimple(variables.FilesCombinedSize)
-	v.fullRequest = collection.NewSimple(variables.FullRequest)
-	v.fullRequestLength = collection.NewSimple(variables.FullRequestLength)
-	v.inboundDataError = collection.NewSimple(variables.InboundDataError)
-	v.matchedVar = collection.NewSimple(variables.MatchedVar)
-	v.matchedVarName = collection.NewSimple(variables.MatchedVarName)
-	v.multipartBoundaryQuoted = collection.NewSimple(variables.MultipartBoundaryQuoted)
-	v.multipartBoundaryWhitespace = collection.NewSimple(variables.MultipartBoundaryWhitespace)
-	v.multipartCrlfLfLines = collection.NewSimple(variables.MultipartCrlfLfLines)
-	v.multipartDataAfter = collection.NewSimple(variables.MultipartDataAfter)
-	v.multipartDataBefore = collection.NewSimple(variables.MultipartDataBefore)
-	v.multipartFileLimitExceeded = collection.NewSimple(variables.MultipartFileLimitExceeded)
-	v.multipartHeaderFolding = collection.NewSimple(variables.MultipartHeaderFolding)
-	v.multipartInvalidHeaderFolding = collection.NewSimple(variables.MultipartInvalidHeaderFolding)
-	v.multipartInvalidPart = collection.NewSimple(variables.MultipartInvalidPart)
-	v.multipartInvalidQuoting = collection.NewSimple(variables.MultipartInvalidQuoting)
-	v.multipartLfLine = collection.NewSimple(variables.MultipartLfLine)
-	v.multipartMissingSemicolon = collection.NewSimple(variables.MultipartMissingSemicolon)
-	v.multipartStrictError = collection.NewSimple(variables.MultipartStrictError)
-	v.multipartUnmatchedBoundary = collection.NewSimple(variables.MultipartUnmatchedBoundary)
-	v.outboundDataError = collection.NewSimple(variables.OutboundDataError)
-	v.pathInfo = collection.NewSimple(variables.PathInfo)
-	v.queryString = collection.NewSimple(variables.QueryString)
-	v.remoteAddr = collection.NewSimple(variables.RemoteAddr)
-	v.remoteHost = collection.NewSimple(variables.RemoteHost)
-	v.remotePort = collection.NewSimple(variables.RemotePort)
-	v.reqbodyError = collection.NewSimple(variables.ReqbodyError)
-	v.reqbodyErrorMsg = collection.NewSimple(variables.ReqbodyErrorMsg)
-	v.reqbodyProcessorError = collection.NewSimple(variables.ReqbodyProcessorError)
-	v.reqbodyProcessorErrorMsg = collection.NewSimple(variables.ReqbodyProcessorErrorMsg)
-	v.reqbodyProcessor = collection.NewSimple(variables.ReqbodyProcessor)
-	v.requestBasename = collection.NewSimple(variables.RequestBasename)
-	v.requestBody = collection.NewSimple(variables.RequestBody)
-	v.requestBodyLength = collection.NewSimple(variables.RequestBodyLength)
-	v.requestFilename = collection.NewSimple(variables.RequestFilename)
-	v.requestLine = collection.NewSimple(variables.RequestLine)
-	v.requestMethod = collection.NewSimple(variables.RequestMethod)
-	v.requestProtocol = collection.NewSimple(variables.RequestProtocol)
-	v.requestURI = collection.NewSimple(variables.RequestURI)
-	v.requestURIRaw = collection.NewSimple(variables.RequestURIRaw)
-	v.responseBody = collection.NewSimple(variables.ResponseBody)
-	v.responseContentLength = collection.NewSimple(variables.ResponseContentLength)
-	v.responseProtocol = collection.NewSimple(variables.ResponseProtocol)
-	v.responseStatus = collection.NewSimple(variables.ResponseStatus)
-	v.serverAddr = collection.NewSimple(variables.ServerAddr)
-	v.serverName = collection.NewSimple(variables.ServerName)
-	v.serverPort = collection.NewSimple(variables.ServerPort)
-	v.sessionID = collection.NewSimple(variables.Sessionid)
-	v.highestSeverity = collection.NewSimple(variables.HighestSeverity)
-	v.statusLine = collection.NewSimple(variables.StatusLine)
-	v.inboundErrorData = collection.NewSimple(variables.InboundErrorData)
-	v.duration = collection.NewSimple(variables.Duration)
-	v.userID = collection.NewSimple(variables.Userid)
-
-	v.filesSizes = collection.NewMap(variables.FilesSizes)
-	v.filesTmpContent = collection.NewMap(variables.FilesTmpContent)
-	v.multipartFilename = collection.NewMap(variables.MultipartFilename)
-	v.multipartName = collection.NewMap(variables.MultipartName)
+	v.urlencodedError = collections.NewSingle(variables.UrlencodedError)
+	v.responseContentType = collections.NewSingle(variables.ResponseContentType)
+	v.uniqueID = collections.NewSingle(variables.UniqueID)
+	v.authType = collections.NewSingle(variables.AuthType)
+	v.filesCombinedSize = collections.NewSingle(variables.FilesCombinedSize)
+	v.fullRequest = collections.NewSingle(variables.FullRequest)
+	v.fullRequestLength = collections.NewSingle(variables.FullRequestLength)
+	v.inboundDataError = collections.NewSingle(variables.InboundDataError)
+	v.matchedVar = collections.NewSingle(variables.MatchedVar)
+	v.matchedVarName = collections.NewSingle(variables.MatchedVarName)
+	v.multipartBoundaryQuoted = collections.NewSingle(variables.MultipartBoundaryQuoted)
+	v.multipartBoundaryWhitespace = collections.NewSingle(variables.MultipartBoundaryWhitespace)
+	v.multipartCrlfLfLines = collections.NewSingle(variables.MultipartCrlfLfLines)
+	v.multipartDataAfter = collections.NewSingle(variables.MultipartDataAfter)
+	v.multipartDataBefore = collections.NewSingle(variables.MultipartDataBefore)
+	v.multipartFileLimitExceeded = collections.NewSingle(variables.MultipartFileLimitExceeded)
+	v.multipartHeaderFolding = collections.NewSingle(variables.MultipartHeaderFolding)
+	v.multipartInvalidHeaderFolding = collections.NewSingle(variables.MultipartInvalidHeaderFolding)
+	v.multipartInvalidPart = collections.NewSingle(variables.MultipartInvalidPart)
+	v.multipartInvalidQuoting = collections.NewSingle(variables.MultipartInvalidQuoting)
+	v.multipartLfLine = collections.NewSingle(variables.MultipartLfLine)
+	v.multipartMissingSemicolon = collections.NewSingle(variables.MultipartMissingSemicolon)
+	v.multipartStrictError = collections.NewSingle(variables.MultipartStrictError)
+	v.multipartUnmatchedBoundary = collections.NewSingle(variables.MultipartUnmatchedBoundary)
+	v.outboundDataError = collections.NewSingle(variables.OutboundDataError)
+	v.pathInfo = collections.NewSingle(variables.PathInfo)
+	v.queryString = collections.NewSingle(variables.QueryString)
+	v.remoteAddr = collections.NewSingle(variables.RemoteAddr)
+	v.remoteHost = collections.NewSingle(variables.RemoteHost)
+	v.remotePort = collections.NewSingle(variables.RemotePort)
+	v.reqbodyError = collections.NewSingle(variables.ReqbodyError)
+	v.reqbodyErrorMsg = collections.NewSingle(variables.ReqbodyErrorMsg)
+	v.reqbodyProcessorError = collections.NewSingle(variables.ReqbodyProcessorError)
+	v.reqbodyProcessorErrorMsg = collections.NewSingle(variables.ReqbodyProcessorErrorMsg)
+	v.reqbodyProcessor = collections.NewSingle(variables.ReqbodyProcessor)
+	v.requestBasename = collections.NewSingle(variables.RequestBasename)
+	v.requestBody = collections.NewSingle(variables.RequestBody)
+	v.requestBodyLength = collections.NewSingle(variables.RequestBodyLength)
+	v.requestFilename = collections.NewSingle(variables.RequestFilename)
+	v.requestLine = collections.NewSingle(variables.RequestLine)
+	v.requestMethod = collections.NewSingle(variables.RequestMethod)
+	v.requestProtocol = collections.NewSingle(variables.RequestProtocol)
+	v.requestURI = collections.NewSingle(variables.RequestURI)
+	v.requestURIRaw = collections.NewSingle(variables.RequestURIRaw)
+	v.responseBody = collections.NewSingle(variables.ResponseBody)
+	v.responseContentLength = collections.NewSingle(variables.ResponseContentLength)
+	v.responseProtocol = collections.NewSingle(variables.ResponseProtocol)
+	v.responseStatus = collections.NewSingle(variables.ResponseStatus)
+	v.serverAddr = collections.NewSingle(variables.ServerAddr)
+	v.serverName = collections.NewSingle(variables.ServerName)
+	v.serverPort = collections.NewSingle(variables.ServerPort)
+	v.sessionID = collections.NewSingle(variables.Sessionid)
+	v.highestSeverity = collections.NewSingle(variables.HighestSeverity)
+	v.statusLine = collections.NewSingle(variables.StatusLine)
+	v.inboundErrorData = collections.NewSingle(variables.InboundErrorData)
+	v.duration = collections.NewSingle(variables.Duration)
+	v.userID = collections.NewSingle(variables.Userid)
+
+	v.filesSizes = collections.NewMap(variables.FilesSizes)
+	v.filesTmpContent = collections.NewMap(variables.FilesTmpContent)
+	v.multipartFilename = collections.NewMap(variables.MultipartFilename)
+	v.multipartName = collections.NewMap(variables.MultipartName)
 	v.matchedVars = collections.NewNamedCollection(variables.MatchedVars)
 	v.matchedVarsNames = v.matchedVars.Names(variables.MatchedVarsNames)
 	v.requestCookies = collections.NewNamedCollection(variables.RequestCookies)
@@ -1612,17 +1622,17 @@ func NewTransactionVariables() *TransactionVariables {
 	v.requestHeadersNames = v.requestHeaders.Names(variables.RequestHeadersNames)
 	v.responseHeaders = collections.NewNamedCollection(variables.ResponseHeaders)
 	v.responseHeadersNames = v.responseHeaders.Names(variables.ResponseHeadersNames)
-	v.geo = collection.NewMap(variables.Geo)
-	v.tx = collection.NewMap(variables.TX)
-	v.rule = collection.NewMap(variables.Rule)
-	v.env = collection.NewMap(variables.Env)
-	v.ip = collection.NewMap(variables.IP)
-	v.files = collection.NewMap(variables.Files)
-	v.filesNames = collection.NewMap(variables.FilesNames)
-	v.filesTmpNames = collection.NewMap(variables.FilesTmpNames)
-	v.responseXML = collection.NewMap(variables.ResponseXML)
-	v.requestXML = collection.NewMap(variables.RequestXML)
-	v.multipartPartHeaders = collection.NewMap(variables.MultipartPartHeaders)
+	v.geo = collections.NewMap(variables.Geo)
+	v.tx = collections.NewMap(variables.TX)
+	v.rule = collections.NewMap(variables.Rule)
+	v.env = collections.NewMap(variables.Env)
+	v.ip = collections.NewMap(variables.IP)
+	v.files = collections.NewMap(variables.Files)
+	v.filesNames = collections.NewMap(variables.FilesNames)
+	v.filesTmpNames = collections.NewMap(variables.FilesTmpNames)
+	v.responseXML = collections.NewMap(variables.ResponseXML)
+	v.requestXML = collections.NewMap(variables.RequestXML)
+	v.multipartPartHeaders = collections.NewMap(variables.MultipartPartHeaders)
 
 	// XML is a pointer to RequestXML
 	v.xml = v.requestXML
@@ -1633,7 +1643,7 @@ func NewTransactionVariables() *TransactionVariables {
 	v.argsPostNames = v.argsPost.Names(variables.ArgsPostNames)
 	v.argsPath = collections.NewNamedCollection(variables.ArgsPath)
 	v.argsCombinedSize = collections.NewSizeCollection(variables.ArgsCombinedSize, v.argsGet, v.argsPost)
-	v.args = collections.NewConcatCollection(
+	v.args = collections.NewConcatKeyed(
 		variables.Args,
 		v.argsGet,
 		v.argsPost,
@@ -1649,19 +1659,19 @@ func NewTransactionVariables() *TransactionVariables {
 	return v
 }
 
-func (v *TransactionVariables) UserID() *collection.Simple {
+func (v *TransactionVariables) UserID() collection.Single {
 	return v.userID
 }
 
-func (v *TransactionVariables) UrlencodedError() *collection.Simple {
+func (v *TransactionVariables) UrlencodedError() collection.Single {
 	return v.urlencodedError
 }
 
-func (v *TransactionVariables) ResponseContentType() *collection.Simple {
+func (v *TransactionVariables) ResponseContentType() collection.Single {
 	return v.responseContentType
 }
 
-func (v *TransactionVariables) UniqueID() *collection.Simple {
+func (v *TransactionVariables) UniqueID() collection.Single {
 	return v.uniqueID
 }
 
@@ -1669,79 +1679,79 @@ func (v *TransactionVariables) ArgsCombinedSize() collection.Collection {
 	return v.argsCombinedSize
 }
 
-func (v *TransactionVariables) AuthType() *collection.Simple {
+func (v *TransactionVariables) AuthType() collection.Single {
 	return v.authType
 }
 
-func (v *TransactionVariables) FilesCombinedSize() *collection.Simple {
+func (v *TransactionVariables) FilesCombinedSize() collection.Single {
 	return v.filesCombinedSize
 }
 
-func (v *TransactionVariables) FullRequest() *collection.Simple {
+func (v *TransactionVariables) FullRequest() collection.Single {
 	return v.fullRequest
 }
 
-func (v *TransactionVariables) FullRequestLength() *collection.Simple {
+func (v *TransactionVariables) FullRequestLength() collection.Single {
 	return v.fullRequestLength
 }
 
-func (v *TransactionVariables) InboundDataError() *collection.Simple {
+func (v *TransactionVariables) InboundDataError() collection.Single {
 	return v.inboundDataError
 }
 
-func (v *TransactionVariables) MatchedVar() *collection.Simple {
+func (v *TransactionVariables) MatchedVar() collection.Single {
 	return v.matchedVar
 }
 
-func (v *TransactionVariables) MatchedVarName() *collection.Simple {
+func (v *TransactionVariables) MatchedVarName() collection.Single {
 	return v.matchedVarName
 }
 
-func (v *TransactionVariables) MultipartBoundaryQuoted() *collection.Simple {
+func (v *TransactionVariables) MultipartBoundaryQuoted() collection.Single {
 	return v.multipartBoundaryQuoted
 }
 
-func (v *TransactionVariables) MultipartBoundaryWhitespace() *collection.Simple {
+func (v *TransactionVariables) MultipartBoundaryWhitespace() collection.Single {
 	return v.multipartBoundaryWhitespace
 }
 
-func (v *TransactionVariables) MultipartCrlfLfLines() *collection.Simple {
+func (v *TransactionVariables) MultipartCrlfLfLines() collection.Single {
 	return v.multipartCrlfLfLines
 }
 
-func (v *TransactionVariables) MultipartDataAfter() *collection.Simple {
+func (v *TransactionVariables) MultipartDataAfter() collection.Single {
 	return v.multipartDataAfter
 }
 
-func (v *TransactionVariables) MultipartDataBefore() *collection.Simple {
+func (v *TransactionVariables) MultipartDataBefore() collection.Single {
 	return v.multipartDataBefore
 }
 
-func (v *TransactionVariables) MultipartFileLimitExceeded() *collection.Simple {
+func (v *TransactionVariables) MultipartFileLimitExceeded() collection.Single {
 	return v.multipartFileLimitExceeded
 }
 
-func (v *TransactionVariables) MultipartHeaderFolding() *collection.Simple {
+func (v *TransactionVariables) MultipartHeaderFolding() collection.Single {
 	return v.multipartHeaderFolding
 }
 
-func (v *TransactionVariables) MultipartInvalidHeaderFolding() *collection.Simple {
+func (v *TransactionVariables) MultipartInvalidHeaderFolding() collection.Single {
 	return v.multipartInvalidHeaderFolding
 }
 
-func (v *TransactionVariables) MultipartInvalidPart() *collection.Simple {
+func (v *TransactionVariables) MultipartInvalidPart() collection.Single {
 	return v.multipartInvalidPart
 }
 
-func (v *TransactionVariables) MultipartInvalidQuoting() *collection.Simple {
+func (v *TransactionVariables) MultipartInvalidQuoting() collection.Single {
 	return v.multipartInvalidQuoting
 }
 
-func (v *TransactionVariables) MultipartLfLine() *collection.Simple {
+func (v *TransactionVariables) MultipartLfLine() collection.Single {
 	return v.multipartLfLine
 }
 
-func (v *TransactionVariables) MultipartMissingSemicolon() *collection.Simple {
+func (v *TransactionVariables) MultipartMissingSemicolon() collection.Single {
 	return v.multipartMissingSemicolon
 }
 
@@ -1749,135 +1759,135 @@ func (v *TransactionVariables) MultipartPartHeaders() collection.Map {
 	return v.multipartPartHeaders
 }
 
-func (v *TransactionVariables) MultipartStrictError() *collection.Simple {
+func (v *TransactionVariables) MultipartStrictError() collection.Single {
 	return v.multipartStrictError
 }
 
-func (v *TransactionVariables) MultipartUnmatchedBoundary() *collection.Simple {
+func (v *TransactionVariables) MultipartUnmatchedBoundary() collection.Single {
 	return v.multipartUnmatchedBoundary
 }
 
-func (v *TransactionVariables) OutboundDataError() *collection.Simple {
+func (v *TransactionVariables) OutboundDataError() collection.Single {
 	return v.outboundDataError
 }
 
-func (v *TransactionVariables) PathInfo() *collection.Simple {
+func (v *TransactionVariables) PathInfo() collection.Single {
 	return v.pathInfo
 }
 
-func (v *TransactionVariables) QueryString() *collection.Simple {
+func (v *TransactionVariables) QueryString() collection.Single {
 	return v.queryString
 }
 
-func (v *TransactionVariables) RemoteAddr() *collection.Simple {
+func (v *TransactionVariables) RemoteAddr() collection.Single {
 	return v.remoteAddr
 }
 
-func (v *TransactionVariables) RemoteHost() *collection.Simple {
+func (v *TransactionVariables) RemoteHost() collection.Single {
 	return v.remoteHost
 }
 
-func (v *TransactionVariables) RemotePort() *collection.Simple {
+func (v *TransactionVariables) RemotePort() collection.Single {
 	return v.remotePort
 }
 
-func (v *TransactionVariables) RequestBodyError() *collection.Simple {
+func (v *TransactionVariables) RequestBodyError() collection.Single {
 	return v.reqbodyError
 }
 
-func (v *TransactionVariables) RequestBodyErrorMsg() *collection.Simple {
+func (v *TransactionVariables) RequestBodyErrorMsg() collection.Single {
 	return v.reqbodyErrorMsg
 }
 
-func (v *TransactionVariables) RequestBodyProcessorError() *collection.Simple {
+func (v *TransactionVariables) RequestBodyProcessorError() collection.Single {
 	return v.reqbodyProcessorError
 }
 
-func (v *TransactionVariables) RequestBodyProcessorErrorMsg() *collection.Simple {
+func (v *TransactionVariables) RequestBodyProcessorErrorMsg() collection.Single {
 	return v.reqbodyProcessorErrorMsg
 }
 
-func (v *TransactionVariables) RequestBodyProcessor() *collection.Simple {
+func (v *TransactionVariables) RequestBodyProcessor() collection.Single {
 	return v.reqbodyProcessor
 }
 
-func (v *TransactionVariables) RequestBasename() *collection.Simple {
+func (v *TransactionVariables) RequestBasename() collection.Single {
 	return v.requestBasename
 }
 
-func (v *TransactionVariables) RequestBody() *collection.Simple {
+func (v *TransactionVariables) RequestBody() collection.Single {
 	return v.requestBody
 }
 
-func (v *TransactionVariables) RequestBodyLength() *collection.Simple {
+func (v *TransactionVariables) RequestBodyLength() collection.Single {
 	return v.requestBodyLength
 }
 
-func (v *TransactionVariables) RequestFilename() *collection.Simple {
+func (v *TransactionVariables) RequestFilename() collection.Single {
 	return v.requestFilename
 }
 
-func (v *TransactionVariables) RequestLine() *collection.Simple {
+func (v *TransactionVariables) RequestLine() collection.Single {
 	return v.requestLine
 }
 
-func (v *TransactionVariables) RequestMethod() *collection.Simple {
+func (v *TransactionVariables) RequestMethod() collection.Single {
 	return v.requestMethod
 }
 
-func (v *TransactionVariables) RequestProtocol() *collection.Simple {
+func (v *TransactionVariables) RequestProtocol() collection.Single {
 	return v.requestProtocol
 }
 
-func (v *TransactionVariables) RequestURI() *collection.Simple {
+func (v *TransactionVariables) RequestURI() collection.Single {
 	return v.requestURI
 }
 
-func (v *TransactionVariables) RequestURIRaw() *collection.Simple {
+func (v *TransactionVariables) RequestURIRaw() collection.Single {
 	return v.requestURIRaw
 }
 
-func (v *TransactionVariables) ResponseBody() *collection.Simple {
+func (v *TransactionVariables) ResponseBody() collection.Single {
 	return v.responseBody
 }
 
-func (v *TransactionVariables) ResponseContentLength() *collection.Simple {
+func (v *TransactionVariables) ResponseContentLength() collection.Single {
 	return v.responseContentLength
 }
 
-func (v *TransactionVariables) ResponseProtocol() *collection.Simple {
+func (v *TransactionVariables) ResponseProtocol() collection.Single {
 	return v.responseProtocol
 }
 
-func (v *TransactionVariables) ResponseStatus() *collection.Simple {
+func (v *TransactionVariables) ResponseStatus() collection.Single {
 	return v.responseStatus
 }
 
-func (v *TransactionVariables) ServerAddr() *collection.Simple {
+func (v *TransactionVariables) ServerAddr() collection.Single {
 	return v.serverAddr
 }
 
-func (v *TransactionVariables) ServerName() *collection.Simple {
+func (v *TransactionVariables) ServerName() collection.Single {
 	return v.serverName
 }
 
-func (v *TransactionVariables) ServerPort() *collection.Simple {
+func (v *TransactionVariables) ServerPort() collection.Single {
 	return v.serverPort
 }
 
-func (v *TransactionVariables) SessionID() *collection.Simple {
+func (v *TransactionVariables) SessionID() collection.Single {
 	return v.sessionID
 }
 
-func (v *TransactionVariables) HighestSeverity() *collection.Simple {
+func (v *TransactionVariables) HighestSeverity() collection.Single {
 	return v.highestSeverity
 }
 
-func (v *TransactionVariables) StatusLine() *collection.Simple {
+func (v *TransactionVariables) StatusLine() collection.Single {
 	return v.statusLine
 }
 
-func (v *TransactionVariables) InboundErrorData() *collection.Simple {
+func (v *TransactionVariables) InboundErrorData() collection.Single {
 	return v.inboundErrorData
 }
 
@@ -1893,11 +1903,11 @@ func (v *TransactionVariables) Rule() collection.Map {
 	return v.rule
 }
 
-func (v *TransactionVariables) Duration() *collection.Simple {
+func (v *TransactionVariables) Duration() collection.Single {
 	return v.duration
 }
 
-func (v *TransactionVariables) Args() collection.Collection {
+func (v *TransactionVariables) Args() collection.Keyed {
 	return v.args
 }
 
diff --git a/macro/macro.go b/macro/macro.go
index 78b4233..989ee95 100644
--- a/macro/macro.go
+++ b/macro/macro.go
@@ -63,14 +63,14 @@ func expandToken(tx rules.TransactionState, token macroToken) string {
 		return token.text
 	}
 	switch col := tx.Collection(token.variable).(type) {
-	case collection.Map:
+	case collection.Keyed:
 		if c := col.Get(token.key); len(c) > 0 {
 			return c[0]
 		}
-	case *collection.Simple:
-		return col.String()
-	case collection.Collection:
-		if c := col.FindString(token.key); len(c) > 0 {
+	case collection.Single:
+		return col.Get()
+	default:
+		if c := col.FindAll(); len(c) > 0 {
 			return c[0].Value()
 		}
 	}
diff --git a/rules/transaction.go b/rules/transaction.go
index 5a107b1..e6fe0c3 100644
--- a/rules/transaction.go
+++ b/rules/transaction.go
@@ -46,69 +46,69 @@ type TransactionState interface {
 // TransactionVariables has pointers to all the variables of the transaction
 type TransactionVariables interface {
 	// Simple Variables
-	UserID() *collection.Simple
-	UrlencodedError() *collection.Simple
-	ResponseContentType() *collection.Simple
-	UniqueID() *collection.Simple
+	UserID() collection.Single
+	UrlencodedError() collection.Single
+	ResponseContentType() collection.Single
+	UniqueID() collection.Single
 	ArgsCombinedSize() collection.Collection
-	AuthType() *collection.Simple
-	FilesCombinedSize() *collection.Simple
-	FullRequest() *collection.Simple
-	FullRequestLength() *collection.Simple
-	InboundDataError() *collection.Simple
-	MatchedVar() *collection.Simple
-	MatchedVarName() *collection.Simple
-	MultipartBoundaryQuoted() *collection.Simple
-	MultipartBoundaryWhitespace() *collection.Simple
-	MultipartCrlfLfLines() *collection.Simple
-	MultipartDataAfter() *collection.Simple
-	MultipartDataBefore() *collection.Simple
-	MultipartFileLimitExceeded() *collection.Simple
+	AuthType() collection.Single
+	FilesCombinedSize() collection.Single
+	FullRequest() collection.Single
+	FullRequestLength() collection.Single
+	InboundDataError() collection.Single
+	MatchedVar() collection.Single
+	MatchedVarName() collection.Single
+	MultipartBoundaryQuoted() collection.Single
+	MultipartBoundaryWhitespace() collection.Single
+	MultipartCrlfLfLines() collection.Single
+	MultipartDataAfter() collection.Single
+	MultipartDataBefore() collection.Single
+	MultipartFileLimitExceeded() collection.Single
 	MultipartPartHeaders() collection.Map
-	MultipartHeaderFolding() *collection.Simple
-	MultipartInvalidHeaderFolding() *collection.Simple
-	MultipartInvalidPart() *collection.Simple
-	MultipartInvalidQuoting() *collection.Simple
-	MultipartLfLine() *collection.Simple
-	MultipartMissingSemicolon() *collection.Simple
-	MultipartStrictError() *collection.Simple
-	MultipartUnmatchedBoundary() *collection.Simple
-	OutboundDataError() *collection.Simple
-	PathInfo() *collection.Simple
-	QueryString() *collection.Simple
-	RemoteAddr() *collection.Simple
-	RemoteHost() *collection.Simple
-	RemotePort() *collection.Simple
-	RequestBodyError() *collection.Simple
-	RequestBodyErrorMsg() *collection.Simple
-	RequestBodyProcessorError() *collection.Simple
-	RequestBodyProcessorErrorMsg() *collection.Simple
-	RequestBodyProcessor() *collection.Simple
-	RequestBasename() *collection.Simple
-	RequestBody() *collection.Simple
-	RequestBodyLength() *collection.Simple
-	RequestFilename() *collection.Simple
-	RequestLine() *collection.Simple
-	RequestMethod() *collection.Simple
-	RequestProtocol() *collection.Simple
-	RequestURI() *collection.Simple
-	RequestURIRaw() *collection.Simple
-	ResponseBody() *collection.Simple
-	ResponseContentLength() *collection.Simple
-	ResponseProtocol() *collection.Simple
-	ResponseStatus() *collection.Simple
-	ServerAddr() *collection.Simple
-	ServerName() *collection.Simple
-	ServerPort() *collection.Simple
-	SessionID() *collection.Simple
-	HighestSeverity() *collection.Simple
-	StatusLine() *collection.Simple
-	InboundErrorData() *collection.Simple
+	MultipartHeaderFolding() collection.Single
+	MultipartInvalidHeaderFolding() collection.Single
+	MultipartInvalidPart() collection.Single
+	MultipartInvalidQuoting() collection.Single
+	MultipartLfLine() collection.Single
+	MultipartMissingSemicolon() collection.Single
+	MultipartStrictError() collection.Single
+	MultipartUnmatchedBoundary() collection.Single
+	OutboundDataError() collection.Single
+	PathInfo() collection.Single
+	QueryString() collection.Single
+	RemoteAddr() collection.Single
+	RemoteHost() collection.Single
+	RemotePort() collection.Single
+	RequestBodyError() collection.Single
+	RequestBodyErrorMsg() collection.Single
+	RequestBodyProcessorError() collection.Single
+	RequestBodyProcessorErrorMsg() collection.Single
+	RequestBodyProcessor() collection.Single
+	RequestBasename() collection.Single
+	RequestBody() collection.Single
+	RequestBodyLength() collection.Single
+	RequestFilename() collection.Single
+	RequestLine() collection.Single
+	RequestMethod() collection.Single
+	RequestProtocol() collection.Single
+	RequestURI() collection.Single
+	RequestURIRaw() collection.Single
+	ResponseBody() collection.Single
+	ResponseContentLength() collection.Single
+	ResponseProtocol() collection.Single
+	ResponseStatus() collection.Single
+	ServerAddr() collection.Single
+	ServerName() collection.Single
+	ServerPort() collection.Single
+	SessionID() collection.Single
+	HighestSeverity() collection.Single
+	StatusLine() collection.Single
+	InboundErrorData() collection.Single
 	Env() collection.Map
 	TX() collection.Map
 	Rule() collection.Map
-	Duration() *collection.Simple
-	Args() collection.Collection
+	Duration() collection.Single
+	Args() collection.Keyed
 	ArgsGet() collection.Map
 	ArgsPost() collection.Map
 	ArgsPath() collection.Map
diff --git a/types/anchoredvar.go b/types/anchoredvar.go
deleted file mode 100644
index 7f0db39..0000000
--- a/types/anchoredvar.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package types
-
-// AnchoredVar stores the case preserved Original name and value
-// of the variable
-type AnchoredVar struct {
-	// Key sensitive name
-	Name  string
-	Value string
-}


Test patch:
diff --git a/actions/ctl_test.go b/actions/ctl_test.go
index d5bdb1c..1675420 100644
--- a/actions/ctl_test.go
+++ b/actions/ctl_test.go
@@ -24,7 +24,7 @@ func TestCtl(t *testing.T) {
 			t.Errorf("failed to init requestBodyProcessor %s", bp)
 		}
 		ctlf.Evaluate(r, tx)
-		if tx.Variables().RequestBodyProcessor().String() != bp {
+		if tx.Variables().RequestBodyProcessor().Get() != bp {
 			t.Error("failed to set RequestBodyProcessor " + bp)
 		}
 	}
diff --git a/bodyprocessors/urlencoded_test.go b/bodyprocessors/urlencoded_test.go
index 4884447..d480a2b 100644
--- a/bodyprocessors/urlencoded_test.go
+++ b/bodyprocessors/urlencoded_test.go
@@ -4,6 +4,7 @@
 package bodyprocessors_test
 
 import (
+	"strconv"
 	"strings"
 	"testing"
 
@@ -31,11 +32,11 @@ func TestURLEncode(t *testing.T) {
 	if err := bp.ProcessRequest(strings.NewReader(body), v, bodyprocessors.Options{}); err != nil {
 		t.Error(err)
 	}
-	if v.RequestBody().String() != body {
-		t.Errorf("Expected %s, got %s", body, v.RequestBody().String())
+	if v.RequestBody().Get() != body {
+		t.Errorf("Expected %s, got %s", body, v.RequestBody().Get())
 	}
-	if v.RequestBodyLength().Int() != len(body) {
-		t.Errorf("Expected %d, got %s", len(body), v.RequestBodyLength().String())
+	if rbl, _ := strconv.Atoi(v.RequestBodyLength().Get()); rbl != len(body) {
+		t.Errorf("Expected %d, got %s", len(body), v.RequestBodyLength().Get())
 	}
 	for k, val := range m {
 		if v.ArgsPost().Get(k)[0] != val {
diff --git a/http/middleware_test.go b/http/middleware_test.go
index 8187fe4..dd2abcc 100644
--- a/http/middleware_test.go
+++ b/http/middleware_test.go
@@ -35,7 +35,7 @@ func TestProcessRequest(t *testing.T) {
 	if _, err := processRequest(tx, req); err != nil {
 		t.Fatal(err)
 	}
-	if tx.Variables().RequestMethod().String() != "POST" {
+	if tx.Variables().RequestMethod().Get() != "POST" {
 		t.Fatal("failed to set request from request object")
 	}
 	if err := tx.Close(); err != nil {
@@ -51,7 +51,7 @@ func TestProcessRequestEngineOff(t *testing.T) {
 	if _, err := processRequest(tx, req); err != nil {
 		t.Fatal(err)
 	}
-	if tx.Variables().RequestMethod().String() != "POST" {
+	if tx.Variables().RequestMethod().Get() != "POST" {
 		t.Fatal("failed to set request from request object")
 	}
 	if err := tx.Close(); err != nil {
diff --git a/internal/collections/concat_test.go b/internal/collections/concat_test.go
index 0707e6b..b19af0d 100644
--- a/internal/collections/concat_test.go
+++ b/internal/collections/concat_test.go
@@ -8,17 +8,16 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/corazawaf/coraza/v3/collection"
 	"github.com/corazawaf/coraza/v3/types"
 	"github.com/corazawaf/coraza/v3/types/variables"
 )
 
-func TestConcatCollection(t *testing.T) {
-	c1 := collection.NewMap(variables.ArgsGet)
-	c2 := collection.NewMap(variables.ArgsPost)
-	c3 := collection.NewMap(variables.ArgsPath)
+func TestConcatKeyed(t *testing.T) {
+	c1 := NewMap(variables.ArgsGet)
+	c2 := NewMap(variables.ArgsPost)
+	c3 := NewMap(variables.ArgsPath)
 
-	c := NewConcatCollection(variables.Args, c1, c2, c3)
+	c := NewConcatKeyed(variables.Args, c1, c2, c3)
 
 	re1 := regexp.MustCompile("anim")
 	re2 := regexp.MustCompile("pla")
@@ -54,6 +53,32 @@ func TestConcatCollection(t *testing.T) {
 	assertValuesMatch(t, c.FindRegex(re2), "palm")
 }
 
+func TestConcatCollection(t *testing.T) {
+	c1 := NewMap(variables.ArgsGet)
+	c2 := NewMap(variables.ArgsPost)
+	c3 := NewMap(variables.ArgsPath)
+
+	c := NewConcatCollection(variables.Args, c1, c2, c3)
+
+	if want, have := "ARGS", c.Name(); want != have {
+		t.Errorf("want %q, have %q", want, have)
+	}
+
+	assertValuesMatch(t, c.FindAll())
+
+	c1.Add("animal", "cat")
+
+	assertValuesMatch(t, c.FindAll(), "cat")
+
+	c2.Add("animal", "dog")
+
+	assertValuesMatch(t, c.FindAll(), "cat", "dog")
+
+	c3.Add("plant", "palm")
+
+	assertValuesMatch(t, c.FindAll(), "cat", "dog", "palm")
+}
+
 func assertValuesMatch(t *testing.T, matches []types.MatchData, wantValues ...string) {
 	t.Helper()
 
diff --git a/collection/collection_map_test.go b/internal/collections/map_test.go
similarity index 94%
rename from collection/collection_map_test.go
rename to internal/collections/map_test.go
index a3831e8..4f2a8a7 100644
--- a/collection/collection_map_test.go
+++ b/internal/collections/map_test.go
@@ -11,7 +11,7 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-package collection
+package collections
 
 import (
 	"regexp"
@@ -20,7 +20,7 @@ import (
 	"github.com/corazawaf/coraza/v3/types/variables"
 )
 
-func TestCollectionMap(t *testing.T) {
+func TestMap(t *testing.T) {
 	c := NewMap(variables.ArgsPost)
 	c.SetIndex("key", 1, "value")
 	c.Set("key2", []string{"value2"})
diff --git a/internal/collections/single_test.go b/internal/collections/single_test.go
new file mode 100644
index 0000000..ac37420
--- /dev/null
+++ b/internal/collections/single_test.go
@@ -0,0 +1,32 @@
+// Copyright 2023 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package collections
+
+import (
+	"testing"
+
+	"github.com/corazawaf/coraza/v3/types/variables"
+)
+
+func TestSingle(t *testing.T) {
+	c := NewSingle(variables.ArgsPath)
+
+	if want, have := "ARGS_PATH", c.Name(); want != have {
+		t.Errorf("want %s, have %s", want, have)
+	}
+
+	if want, have := "", c.Get(); want != have {
+		t.Errorf("want %s, have %s", want, have)
+	}
+
+	assertValuesMatch(t, c.FindAll())
+
+	c.Set("bear")
+
+	if want, have := "bear", c.Get(); want != have {
+		t.Errorf("want %s, have %s", want, have)
+	}
+
+	assertValuesMatch(t, c.FindAll(), "bear")
+}
diff --git a/internal/corazawaf/transaction_test.go b/internal/corazawaf/transaction_test.go
index 4cf6645..1e1e260 100644
--- a/internal/corazawaf/transaction_test.go
+++ b/internal/corazawaf/transaction_test.go
@@ -13,6 +13,7 @@ import (
 	"testing"
 
 	"github.com/corazawaf/coraza/v3/collection"
+	"github.com/corazawaf/coraza/v3/internal/collections"
 	"github.com/corazawaf/coraza/v3/internal/corazarules"
 	utils "github.com/corazawaf/coraza/v3/internal/strings"
 	"github.com/corazawaf/coraza/v3/loggers"
@@ -352,7 +353,7 @@ func TestWriteRequestBodyIsNopWhenBodyIsNotAccesible(t *testing.T) {
 func TestResponseHeader(t *testing.T) {
 	tx := makeTransaction(t)
 	tx.AddResponseHeader("content-type", "test")
-	if tx.variables.responseContentType.String() != "test" {
+	if tx.variables.responseContentType.Get() != "test" {
 		t.Error("invalid RESPONSE_CONTENT_TYPE after response headers")
 	}
 
@@ -522,7 +523,7 @@ func TestWriteResponseBody(t *testing.T) {
 								}
 								// checking if the body has been populated up to the first POST arg
 								index := strings.Index(urlencodedBody, "&")
-								if tx.variables.responseBody.String()[:index] != urlencodedBody[:index] {
+								if tx.variables.responseBody.Get()[:index] != urlencodedBody[:index] {
 									t.Error("failed to set response body")
 								}
 							}
@@ -766,7 +767,7 @@ func TestRequestBodyProcessingAlgorithm(t *testing.T) {
 	if _, err := tx.ProcessRequestBody(); err != nil {
 		t.Error("failed to process request body")
 	}
-	if tx.variables.requestBody.String() != "test123" {
+	if tx.variables.requestBody.Get() != "test123" {
 		t.Error("failed to set request body")
 	}
 	if err := tx.Close(); err != nil {
@@ -878,10 +879,10 @@ func TestTxPhase4Magic(t *testing.T) {
 	if _, err := tx.ProcessResponseBody(); err != nil {
 		t.Error(err)
 	}
-	if tx.variables.outboundDataError.String() != "1" {
+	if tx.variables.outboundDataError.Get() != "1" {
 		t.Error("failed to set outbound data error")
 	}
-	if tx.variables.responseBody.String() != "mor" {
+	if tx.variables.responseBody.Get() != "mor" {
 		t.Error("failed to set response body")
 	}
 }
@@ -901,7 +902,7 @@ func TestVariablesMatch(t *testing.T) {
 	}
 
 	for k, v := range expect {
-		if m := (tx.Collection(k)).(*collection.Simple).String(); m != v {
+		if m := (tx.Collection(k)).(*collections.Single).Get(); m != v {
 			t.Errorf("failed to match variable %s, Expected: %s, got: %s", k.Name(), v, m)
 		}
 	}
@@ -922,7 +923,7 @@ func TestTxReqBodyForce(t *testing.T) {
 	if _, err := tx.ProcessRequestBody(); err != nil {
 		t.Error(err)
 	}
-	if tx.variables.requestBody.String() != "test" {
+	if tx.variables.requestBody.Get() != "test" {
 		t.Error("failed to set request body")
 	}
 }
@@ -938,7 +939,7 @@ func TestTxReqBodyForceNegative(t *testing.T) {
 	if _, err := tx.ProcessRequestBody(); err != nil {
 		t.Error(err)
 	}
-	if tx.variables.requestBody.String() == "test" {
+	if tx.variables.requestBody.Get() == "test" {
 		t.Error("reqbody should not be there")
 	}
 }
@@ -947,10 +948,10 @@ func TestTxProcessConnection(t *testing.T) {
 	waf := NewWAF()
 	tx := waf.NewTransaction()
 	tx.ProcessConnection("127.0.0.1", 80, "127.0.0.2", 8080)
-	if tx.variables.remoteAddr.String() != "127.0.0.1" {
+	if tx.variables.remoteAddr.Get() != "127.0.0.1" {
 		t.Error("failed to set client ip")
 	}
-	if tx.variables.remotePort.Int() != 80 {
+	if rp, _ := strconv.Atoi(tx.variables.remotePort.Get()); rp != 80 {
 		t.Error("failed to set client port")
 	}
 }
@@ -964,7 +965,7 @@ func TestTxSetServerName(t *testing.T) {
 	tx := waf.NewTransaction()
 	tx.LastPhase = types.PhaseRequestHeaders
 	tx.SetServerName("coraza.io")
-	if tx.variables.serverName.String() != "coraza.io" {
+	if tx.variables.serverName.Get() != "coraza.io" {
 		t.Error("failed to set server name")
 	}
 	if want, have := 1, len(l.entries); want != have {
@@ -1011,13 +1012,13 @@ func TestTxProcessURI(t *testing.T) {
 	tx := waf.NewTransaction()
 	uri := "http://example.com/path/to/file.html?query=string&other=value"
 	tx.ProcessURI(uri, "GET", "HTTP/1.1")
-	if s := tx.variables.requestURI.String(); s != uri {
+	if s := tx.variables.requestURI.Get(); s != uri {
 		t.Errorf("failed to set request uri, got %s", s)
 	}
-	if s := tx.variables.requestBasename.String(); s != "file.html" {
+	if s := tx.variables.requestBasename.Get(); s != "file.html" {
 		t.Errorf("failed to set request path, got %s", s)
 	}
-	if tx.variables.queryString.String() != "query=string&other=value" {
+	if tx.variables.queryString.Get() != "query=string&other=value" {
 		t.Error("failed to set request query")
 	}
 	if v := tx.variables.args.FindAll(); len(v) != 2 {