mirror of
https://github.com/mikefarah/yq.git
synced 2024-12-19 20:19:04 +00:00
getting closer
This commit is contained in:
parent
c525fa21c4
commit
ee8cc90307
@ -120,6 +120,9 @@ func (n *CandidateNode) GetNiceTag() string {
|
||||
}
|
||||
|
||||
func (n *CandidateNode) getParsedKey() interface{} {
|
||||
if n.IsMapKey {
|
||||
return n.Value
|
||||
}
|
||||
if n.Key == nil {
|
||||
return nil
|
||||
}
|
||||
@ -172,6 +175,13 @@ func (n *CandidateNode) AsList() *list.List {
|
||||
return elMap
|
||||
}
|
||||
|
||||
func (n *CandidateNode) AddKeyValueChild(rawKey *CandidateNode, rawValue *CandidateNode) {
|
||||
value := rawValue.unwrapDocument()
|
||||
value.Key = rawKey.unwrapDocument()
|
||||
value.Key.IsMapKey = true
|
||||
n.Content = append(n.Content, value.Key, value)
|
||||
}
|
||||
|
||||
func (n *CandidateNode) GetValueRep() (interface{}, error) {
|
||||
log.Debugf("GetValueRep for %v value: %v", n.GetNicePath(), n.Value)
|
||||
realTag := n.guessTagFromCustomType()
|
||||
|
@ -9,3 +9,267 @@ Add behaves differently according to the type of the LHS:
|
||||
Use `+=` as a relative append assign for things like increment. Note that `.a += .x` is equivalent to running `.a = .a + .x`.
|
||||
|
||||
|
||||
## Append to existing array
|
||||
Note that the styling is copied from existing array elements
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: ['dog']
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.a += "cat"' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: ['dog', 'cat']
|
||||
```
|
||||
|
||||
## Prepend to existing array
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a:
|
||||
- dog
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.a = ["cat"] + .a' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a:
|
||||
- cat
|
||||
- dog
|
||||
```
|
||||
|
||||
## Add new object to array
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a:
|
||||
- dog: woof
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.a + {"cat": "meow"}' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
- dog: woof
|
||||
- cat: meow
|
||||
```
|
||||
|
||||
## Relative append
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a:
|
||||
a1:
|
||||
b:
|
||||
- cat
|
||||
a2:
|
||||
b:
|
||||
- dog
|
||||
a3: {}
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.a[].b += ["mouse"]' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a:
|
||||
a1:
|
||||
b:
|
||||
- cat
|
||||
- mouse
|
||||
a2:
|
||||
b:
|
||||
- dog
|
||||
- mouse
|
||||
a3:
|
||||
b:
|
||||
- mouse
|
||||
```
|
||||
|
||||
## String concatenation
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: cat
|
||||
b: meow
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.a += .b' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: catmeow
|
||||
b: meow
|
||||
```
|
||||
|
||||
## Number addition - float
|
||||
If the lhs or rhs are floats then the expression will be calculated with floats.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: 3
|
||||
b: 4.9
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.a = .a + .b' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: 7.9
|
||||
b: 4.9
|
||||
```
|
||||
|
||||
## Number addition - int
|
||||
If both the lhs and rhs are ints then the expression will be calculated with ints.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: 3
|
||||
b: 4
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.a = .a + .b' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: 7
|
||||
b: 4
|
||||
```
|
||||
|
||||
## Increment numbers
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: 3
|
||||
b: 5
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.[] += 1' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: 4
|
||||
b: 6
|
||||
```
|
||||
|
||||
## Date addition
|
||||
You can add durations to dates. Assumes RFC3339 date time format, see [date-time operators](https://mikefarah.gitbook.io/yq/operators/date-time-operators) for more information.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: 2021-01-01T00:00:00Z
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.a += "3h10m"' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: 2021-01-01T03:10:00Z
|
||||
```
|
||||
|
||||
## Date addition - custom format
|
||||
You can add durations to dates. See [date-time operators](https://mikefarah.gitbook.io/yq/operators/date-time-operators) for more information.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: Saturday, 15-Dec-01 at 2:59AM GMT
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq 'with_dtf("Monday, 02-Jan-06 at 3:04PM MST", .a += "3h1m")' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: Saturday, 15-Dec-01 at 6:00AM GMT
|
||||
```
|
||||
|
||||
## Add to null
|
||||
Adding to null simply returns the rhs
|
||||
|
||||
Running
|
||||
```bash
|
||||
yq --null-input 'null + "cat"'
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
cat
|
||||
```
|
||||
|
||||
## Add maps to shallow merge
|
||||
Adding objects together shallow merges them. Use `*` to deeply merge.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a:
|
||||
thing:
|
||||
name: Astuff
|
||||
value: x
|
||||
a1: cool
|
||||
b:
|
||||
thing:
|
||||
name: Bstuff
|
||||
legs: 3
|
||||
b1: neat
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.a += .b' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a:
|
||||
thing:
|
||||
name: Bstuff
|
||||
legs: 3
|
||||
a1: cool
|
||||
b1: neat
|
||||
b:
|
||||
thing:
|
||||
name: Bstuff
|
||||
legs: 3
|
||||
b1: neat
|
||||
```
|
||||
|
||||
## Custom types: that are really strings
|
||||
When custom tags are encountered, yq will try to decode the underlying type.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: !horse cat
|
||||
b: !goat _meow
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.a += .b' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: !horse cat_meow
|
||||
b: !goat _meow
|
||||
```
|
||||
|
||||
## Custom types: that are really numbers
|
||||
When custom tags are encountered, yq will try to decode the underlying type.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: !horse 1.2
|
||||
b: !goat 2.3
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.a += .b' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: !horse 3.5
|
||||
b: !goat 2.3
|
||||
```
|
||||
|
||||
|
@ -2,3 +2,95 @@
|
||||
|
||||
This is used to construct objects (or maps). This can be used against existing yaml, or to create fresh yaml documents.
|
||||
|
||||
## Collect empty object
|
||||
Running
|
||||
```bash
|
||||
yq --null-input '{}'
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
{}
|
||||
```
|
||||
|
||||
## Wrap (prefix) existing object
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
name: Mike
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '{"wrap": .}' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
wrap:
|
||||
name: Mike
|
||||
```
|
||||
|
||||
## Using splat to create multiple objects
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
name: Mike
|
||||
pets:
|
||||
- cat
|
||||
- dog
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '{.name: .pets.[]}' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
Mike: cat
|
||||
Mike: dog
|
||||
```
|
||||
|
||||
## Working with multiple documents
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
name: Mike
|
||||
pets:
|
||||
- cat
|
||||
- dog
|
||||
---
|
||||
name: Rosey
|
||||
pets:
|
||||
- monkey
|
||||
- sheep
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '{.name: .pets.[]}' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
Mike: cat
|
||||
Mike: dog
|
||||
---
|
||||
Rosey: monkey
|
||||
Rosey: sheep
|
||||
```
|
||||
|
||||
## Creating yaml from scratch
|
||||
Running
|
||||
```bash
|
||||
yq --null-input '{"wrap": "frog"}'
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
wrap: frog
|
||||
```
|
||||
|
||||
## Creating yaml from scratch with multiple objects
|
||||
Running
|
||||
```bash
|
||||
yq --null-input '(.a.b = "foo") | (.d.e = "bar")'
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a:
|
||||
b: foo
|
||||
d:
|
||||
e: bar
|
||||
```
|
||||
|
||||
|
@ -159,9 +159,9 @@ func addDateTimes(layout string, target *CandidateNode, lhs *CandidateNode, rhs
|
||||
func addSequences(target *CandidateNode, lhs *CandidateNode, rhs *CandidateNode) error {
|
||||
log.Debugf("adding sequences! target: %v; lhs %v; rhs: %v", NodeToString(target), NodeToString(lhs), NodeToString(rhs))
|
||||
target.Kind = SequenceNode
|
||||
if len(lhs.Content) > 0 {
|
||||
log.Debugf("copy lhs style")
|
||||
target.Style = lhs.Style
|
||||
if len(lhs.Content) == 0 {
|
||||
log.Debugf("dont copy lhs style")
|
||||
target.Style = 0
|
||||
}
|
||||
target.Tag = lhs.Tag
|
||||
|
||||
@ -179,6 +179,11 @@ func addMaps(target *CandidateNode, lhsC *CandidateNode, rhsC *CandidateNode) {
|
||||
lhs := lhsC
|
||||
rhs := rhsC
|
||||
|
||||
if len(lhs.Content) == 0 {
|
||||
log.Debugf("dont copy lhs style")
|
||||
target.Style = 0
|
||||
}
|
||||
|
||||
target.Content = make([]*CandidateNode, len(lhs.Content))
|
||||
copy(target.Content, lhs.Content)
|
||||
|
||||
|
@ -82,272 +82,272 @@ var addOperatorScenarios = []expressionScenario{
|
||||
// "D0, P[a], (!!seq)::[1, 2]\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// skipDoc: true,
|
||||
// description: "Concatenate to empty array",
|
||||
// document: `{a: []}`,
|
||||
// expression: `.a + "cat"`,
|
||||
// expected: []string{
|
||||
// "D0, P[a], (!!seq)::- cat\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// description: "Append to existing array",
|
||||
// subdescription: "Note that the styling is copied from existing array elements",
|
||||
// dontFormatInputForDoc: true,
|
||||
// document: `a: ['dog']`,
|
||||
// expression: `.a += "cat"`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (doc)::a: ['dog', 'cat']\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// description: "Prepend to existing array",
|
||||
// document: `a: [dog]`,
|
||||
// expression: `.a = ["cat"] + .a`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (doc)::a: [cat, dog]\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// skipDoc: true,
|
||||
// description: "Concatenate to existing array",
|
||||
// subdescription: "does not modify original",
|
||||
// document: `{a: ['dog'], b: cat}`,
|
||||
// expression: `.a = .a + .b`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (doc)::{a: ['dog', 'cat'], b: cat}\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// skipDoc: true,
|
||||
// description: "Concatenate to empty array",
|
||||
// document: `a: []`,
|
||||
// expression: `.a += "cat"`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (doc)::a:\n - cat\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// skipDoc: true,
|
||||
// description: "Concatenate to existing array",
|
||||
// document: `a: [dog]`,
|
||||
// expression: `.a += "cat"`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (doc)::a: [dog, cat]\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// skipDoc: true,
|
||||
// description: "Concatenate to empty object",
|
||||
// document: `{a: {}}`,
|
||||
// expression: `.a + {"b": "cat"}`,
|
||||
// expected: []string{
|
||||
// "D0, P[a], (!!map)::b: cat\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// skipDoc: true,
|
||||
// description: "Concatenate to existing object",
|
||||
// document: `{a: {c: dog}}`,
|
||||
// expression: `.a + {"b": "cat"}`,
|
||||
// expected: []string{
|
||||
// "D0, P[a], (!!map)::{c: dog, b: cat}\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// skipDoc: true,
|
||||
// description: "Concatenate to existing object",
|
||||
// subdescription: "matches stylig",
|
||||
// document: "a:\n c: dog",
|
||||
// expression: `.a + {"b": "cat"}`,
|
||||
// expected: []string{
|
||||
// "D0, P[a], (!!map)::c: dog\nb: cat\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// skipDoc: true,
|
||||
// description: "Concatenate to empty object in place",
|
||||
// document: `a: {}`,
|
||||
// expression: `.a += {"b": "cat"}`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (doc)::a:\n b: cat\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// skipDoc: true,
|
||||
// description: "Concatenate to existing object in place",
|
||||
// document: `a: {c: dog}`,
|
||||
// expression: `.a += {"b": "cat"}`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (doc)::a: {c: dog, b: cat}\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// description: "Add new object to array",
|
||||
// document: `a: [{dog: woof}]`,
|
||||
// expression: `.a + {"cat": "meow"}`,
|
||||
// expected: []string{
|
||||
// "D0, P[a], (!!seq)::[{dog: woof}, {cat: meow}]\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// description: "Relative append",
|
||||
// document: `a: { a1: {b: [cat]}, a2: {b: [dog]}, a3: {} }`,
|
||||
// expression: `.a[].b += ["mouse"]`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (doc)::a: {a1: {b: [cat, mouse]}, a2: {b: [dog, mouse]}, a3: {b: [mouse]}}\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// description: "String concatenation",
|
||||
// document: `{a: cat, b: meow}`,
|
||||
// expression: `.a += .b`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (doc)::{a: catmeow, b: meow}\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// description: "String concatenation - str + int",
|
||||
// skipDoc: true,
|
||||
// document: `{a: !cool cat, b: meow}`,
|
||||
// expression: `.a + 3`,
|
||||
// expected: []string{
|
||||
// "D0, P[a], (!cool)::cat3\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// description: "String concatenation - int + str",
|
||||
// skipDoc: true,
|
||||
// document: `{a: !cool cat, b: meow}`,
|
||||
// expression: `3 + .a`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (!cool)::3cat\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// description: "Number addition - float",
|
||||
// subdescription: "If the lhs or rhs are floats then the expression will be calculated with floats.",
|
||||
// document: `{a: 3, b: 4.9}`,
|
||||
// expression: `.a = .a + .b`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (doc)::{a: 7.9, b: 4.9}\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// description: "Number addition - int",
|
||||
// subdescription: "If both the lhs and rhs are ints then the expression will be calculated with ints.",
|
||||
// document: `{a: 3, b: 4}`,
|
||||
// expression: `.a = .a + .b`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (doc)::{a: 7, b: 4}\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// description: "Increment numbers",
|
||||
// document: `{a: 3, b: 5}`,
|
||||
// expression: `.[] += 1`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (doc)::{a: 4, b: 6}\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// description: "Date addition",
|
||||
// subdescription: "You can add durations to dates. Assumes RFC3339 date time format, see [date-time operators](https://mikefarah.gitbook.io/yq/operators/date-time-operators) for more information.",
|
||||
// document: `a: 2021-01-01T00:00:00Z`,
|
||||
// expression: `.a += "3h10m"`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (doc)::a: 2021-01-01T03:10:00Z\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// description: "Date addition -date only",
|
||||
// skipDoc: true,
|
||||
// document: `a: 2021-01-01`,
|
||||
// expression: `.a += "24h"`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (doc)::a: 2021-01-02T00:00:00Z\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// description: "Date addition - custom format",
|
||||
// subdescription: "You can add durations to dates. See [date-time operators](https://mikefarah.gitbook.io/yq/operators/date-time-operators) for more information.",
|
||||
// document: `a: Saturday, 15-Dec-01 at 2:59AM GMT`,
|
||||
// expression: `with_dtf("Monday, 02-Jan-06 at 3:04PM MST", .a += "3h1m")`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (doc)::a: Saturday, 15-Dec-01 at 6:00AM GMT\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// skipDoc: true,
|
||||
// description: "Date addition - custom format",
|
||||
// subdescription: "You can add durations to dates. See [date-time operators](https://mikefarah.gitbook.io/yq/operators/date-time-operators) for more information.",
|
||||
// document: `a: !cat Saturday, 15-Dec-01 at 2:59AM GMT`,
|
||||
// expression: `with_dtf("Monday, 02-Jan-06 at 3:04PM MST", .a += "3h1m")`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (doc)::a: !cat Saturday, 15-Dec-01 at 6:00AM GMT\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// description: "Add to null",
|
||||
// subdescription: "Adding to null simply returns the rhs",
|
||||
// expression: `null + "cat"`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (!!str)::cat\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// description: "Add maps to shallow merge",
|
||||
// subdescription: "Adding objects together shallow merges them. Use `*` to deeply merge.",
|
||||
// document: "a: {thing: {name: Astuff, value: x}, a1: cool}\nb: {thing: {name: Bstuff, legs: 3}, b1: neat}",
|
||||
// expression: `.a += .b`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (doc)::a: {thing: {name: Bstuff, legs: 3}, a1: cool, b1: neat}\nb: {thing: {name: Bstuff, legs: 3}, b1: neat}\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// description: "Custom types: that are really strings",
|
||||
// subdescription: "When custom tags are encountered, yq will try to decode the underlying type.",
|
||||
// document: "a: !horse cat\nb: !goat _meow",
|
||||
// expression: `.a += .b`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (doc)::a: !horse cat_meow\nb: !goat _meow\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// description: "Custom types: that are really numbers",
|
||||
// subdescription: "When custom tags are encountered, yq will try to decode the underlying type.",
|
||||
// document: "a: !horse 1.2\nb: !goat 2.3",
|
||||
// expression: `.a += .b`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (doc)::a: !horse 3.5\nb: !goat 2.3\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// skipDoc: true,
|
||||
// document: "a: !horse 2\nb: !goat 2.3",
|
||||
// expression: `.a += .b`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (doc)::a: !horse 4.3\nb: !goat 2.3\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// skipDoc: true,
|
||||
// document: "a: 2\nb: !goat 2.3",
|
||||
// expression: `.a += .b`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (doc)::a: 4.3\nb: !goat 2.3\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// skipDoc: true,
|
||||
// description: "Custom types: that are really ints",
|
||||
// document: "a: !horse 2\nb: !goat 3",
|
||||
// expression: `.a += .b`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (doc)::a: !horse 5\nb: !goat 3\n",
|
||||
// },
|
||||
// },
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Concatenate to empty array",
|
||||
document: `{a: []}`,
|
||||
expression: `.a + "cat"`,
|
||||
expected: []string{
|
||||
"D0, P[a], (!!seq)::- cat\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Append to existing array",
|
||||
subdescription: "Note that the styling is copied from existing array elements",
|
||||
dontFormatInputForDoc: true,
|
||||
document: `a: ['dog']`,
|
||||
expression: `.a += "cat"`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: ['dog', 'cat']\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Prepend to existing array",
|
||||
document: `a: [dog]`,
|
||||
expression: `.a = ["cat"] + .a`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: [cat, dog]\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Concatenate to existing array",
|
||||
subdescription: "does not modify original",
|
||||
document: `{a: ['dog'], b: cat}`,
|
||||
expression: `.a = .a + .b`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::{a: ['dog', 'cat'], b: cat}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Concatenate to empty array",
|
||||
document: `a: []`,
|
||||
expression: `.a += "cat"`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a:\n - cat\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Concatenate to existing array",
|
||||
document: `a: [dog]`,
|
||||
expression: `.a += "cat"`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: [dog, cat]\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Concatenate to empty object",
|
||||
document: `{a: {}}`,
|
||||
expression: `.a + {"b": "cat"}`,
|
||||
expected: []string{
|
||||
"D0, P[a], (!!map)::b: cat\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Concatenate to existing object",
|
||||
document: `{a: {c: dog}}`,
|
||||
expression: `.a + {"b": "cat"}`,
|
||||
expected: []string{
|
||||
"D0, P[a], (!!map)::{c: dog, b: cat}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Concatenate to existing object",
|
||||
subdescription: "matches stylig",
|
||||
document: "a:\n c: dog",
|
||||
expression: `.a + {"b": "cat"}`,
|
||||
expected: []string{
|
||||
"D0, P[a], (!!map)::c: dog\nb: cat\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Concatenate to empty object in place",
|
||||
document: `a: {}`,
|
||||
expression: `.a += {"b": "cat"}`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a:\n b: cat\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Concatenate to existing object in place",
|
||||
document: `a: {c: dog}`,
|
||||
expression: `.a += {"b": "cat"}`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: {c: dog, b: cat}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Add new object to array",
|
||||
document: `a: [{dog: woof}]`,
|
||||
expression: `.a + {"cat": "meow"}`,
|
||||
expected: []string{
|
||||
"D0, P[a], (!!seq)::[{dog: woof}, {cat: meow}]\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Relative append",
|
||||
document: `a: { a1: {b: [cat]}, a2: {b: [dog]}, a3: {} }`,
|
||||
expression: `.a[].b += ["mouse"]`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: {a1: {b: [cat, mouse]}, a2: {b: [dog, mouse]}, a3: {b: [mouse]}}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "String concatenation",
|
||||
document: `{a: cat, b: meow}`,
|
||||
expression: `.a += .b`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::{a: catmeow, b: meow}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "String concatenation - str + int",
|
||||
skipDoc: true,
|
||||
document: `{a: !cool cat, b: meow}`,
|
||||
expression: `.a + 3`,
|
||||
expected: []string{
|
||||
"D0, P[a], (!cool)::cat3\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "String concatenation - int + str",
|
||||
skipDoc: true,
|
||||
document: `{a: !cool cat, b: meow}`,
|
||||
expression: `3 + .a`,
|
||||
expected: []string{
|
||||
"D0, P[], (!cool)::3cat\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Number addition - float",
|
||||
subdescription: "If the lhs or rhs are floats then the expression will be calculated with floats.",
|
||||
document: `{a: 3, b: 4.9}`,
|
||||
expression: `.a = .a + .b`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::{a: 7.9, b: 4.9}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Number addition - int",
|
||||
subdescription: "If both the lhs and rhs are ints then the expression will be calculated with ints.",
|
||||
document: `{a: 3, b: 4}`,
|
||||
expression: `.a = .a + .b`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::{a: 7, b: 4}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Increment numbers",
|
||||
document: `{a: 3, b: 5}`,
|
||||
expression: `.[] += 1`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::{a: 4, b: 6}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Date addition",
|
||||
subdescription: "You can add durations to dates. Assumes RFC3339 date time format, see [date-time operators](https://mikefarah.gitbook.io/yq/operators/date-time-operators) for more information.",
|
||||
document: `a: 2021-01-01T00:00:00Z`,
|
||||
expression: `.a += "3h10m"`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: 2021-01-01T03:10:00Z\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Date addition -date only",
|
||||
skipDoc: true,
|
||||
document: `a: 2021-01-01`,
|
||||
expression: `.a += "24h"`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: 2021-01-02T00:00:00Z\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Date addition - custom format",
|
||||
subdescription: "You can add durations to dates. See [date-time operators](https://mikefarah.gitbook.io/yq/operators/date-time-operators) for more information.",
|
||||
document: `a: Saturday, 15-Dec-01 at 2:59AM GMT`,
|
||||
expression: `with_dtf("Monday, 02-Jan-06 at 3:04PM MST", .a += "3h1m")`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: Saturday, 15-Dec-01 at 6:00AM GMT\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Date addition - custom format",
|
||||
subdescription: "You can add durations to dates. See [date-time operators](https://mikefarah.gitbook.io/yq/operators/date-time-operators) for more information.",
|
||||
document: `a: !cat Saturday, 15-Dec-01 at 2:59AM GMT`,
|
||||
expression: `with_dtf("Monday, 02-Jan-06 at 3:04PM MST", .a += "3h1m")`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: !cat Saturday, 15-Dec-01 at 6:00AM GMT\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Add to null",
|
||||
subdescription: "Adding to null simply returns the rhs",
|
||||
expression: `null + "cat"`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!str)::cat\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Add maps to shallow merge",
|
||||
subdescription: "Adding objects together shallow merges them. Use `*` to deeply merge.",
|
||||
document: "a: {thing: {name: Astuff, value: x}, a1: cool}\nb: {thing: {name: Bstuff, legs: 3}, b1: neat}",
|
||||
expression: `.a += .b`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: {thing: {name: Bstuff, legs: 3}, a1: cool, b1: neat}\nb: {thing: {name: Bstuff, legs: 3}, b1: neat}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Custom types: that are really strings",
|
||||
subdescription: "When custom tags are encountered, yq will try to decode the underlying type.",
|
||||
document: "a: !horse cat\nb: !goat _meow",
|
||||
expression: `.a += .b`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: !horse cat_meow\nb: !goat _meow\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Custom types: that are really numbers",
|
||||
subdescription: "When custom tags are encountered, yq will try to decode the underlying type.",
|
||||
document: "a: !horse 1.2\nb: !goat 2.3",
|
||||
expression: `.a += .b`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: !horse 3.5\nb: !goat 2.3\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: "a: !horse 2\nb: !goat 2.3",
|
||||
expression: `.a += .b`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: !horse 4.3\nb: !goat 2.3\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: "a: 2\nb: !goat 2.3",
|
||||
expression: `.a += .b`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: 4.3\nb: !goat 2.3\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Custom types: that are really ints",
|
||||
document: "a: !horse 2\nb: !goat 3",
|
||||
expression: `.a += .b`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: !horse 5\nb: !goat 3\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Custom types: that are really arrays",
|
||||
skipDoc: true,
|
||||
@ -358,29 +358,29 @@ var addOperatorScenarios = []expressionScenario{
|
||||
"D0, P[], (doc)::a: !horse [a, b]\nb: !goat [b]\n",
|
||||
},
|
||||
},
|
||||
// {
|
||||
// skipDoc: true,
|
||||
// description: "Keep anchors",
|
||||
// document: "a: &horse [1]",
|
||||
// expression: `.a += 2`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (doc)::a: &horse [1, 2]\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// skipDoc: true,
|
||||
// description: "Add sequence to map",
|
||||
// document: "a: {x: cool}",
|
||||
// expression: `.a += [2]`,
|
||||
// expectedError: "!!seq () cannot be added to a !!map (a)",
|
||||
// },
|
||||
// {
|
||||
// skipDoc: true,
|
||||
// description: "Add sequence to scalar",
|
||||
// document: "a: cool",
|
||||
// expression: `.a += [2]`,
|
||||
// expectedError: "!!seq () cannot be added to a !!str (a)",
|
||||
// },
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Keep anchors",
|
||||
document: "a: &horse [1]",
|
||||
expression: `.a += 2`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: &horse [1, 2]\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Add sequence to map",
|
||||
document: "a: {x: cool}",
|
||||
expression: `.a += [2]`,
|
||||
expectedError: "!!seq () cannot be added to a !!map (a)",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Add sequence to scalar",
|
||||
document: "a: cool",
|
||||
expression: `.a += [2]`,
|
||||
expectedError: "!!seq () cannot be added to a !!str (a)",
|
||||
},
|
||||
}
|
||||
|
||||
func TestAddOperatorScenarios(t *testing.T) {
|
||||
|
@ -22,6 +22,7 @@ func collectObjectOperator(d *dataTreeNavigator, originalContext Context, expres
|
||||
|
||||
if context.MatchingNodes.Len() == 0 {
|
||||
candidate := &CandidateNode{Kind: MappingNode, Tag: "!!map", Value: "{}"}
|
||||
log.Debugf("-- collectObjectOperation - starting with empty map")
|
||||
return context.SingleChildContext(candidate), nil
|
||||
}
|
||||
first := context.MatchingNodes.Front().Value.(*CandidateNode)
|
||||
@ -38,6 +39,7 @@ func collectObjectOperator(d *dataTreeNavigator, originalContext Context, expres
|
||||
rotated[i].PushBack(candidateNode.Content[i])
|
||||
}
|
||||
}
|
||||
log.Debugf("-- collectObjectOperation, lenght of rotated is %v", len(rotated))
|
||||
|
||||
newObject := list.New()
|
||||
for i := 0; i < len(first.Content); i++ {
|
||||
@ -58,6 +60,7 @@ func collect(d *dataTreeNavigator, context Context, remainingMatches *list.List)
|
||||
}
|
||||
|
||||
candidate := remainingMatches.Remove(remainingMatches.Front()).(*CandidateNode)
|
||||
log.Debugf("-- collectObjectOperation - collect %v", NodeToString(candidate))
|
||||
|
||||
splatted, err := splat(context.SingleChildContext(candidate),
|
||||
traversePreferences{DontFollowAlias: true, IncludeMapKeys: false})
|
||||
@ -67,6 +70,7 @@ func collect(d *dataTreeNavigator, context Context, remainingMatches *list.List)
|
||||
}
|
||||
|
||||
if context.MatchingNodes.Len() == 0 {
|
||||
log.Debugf("-- collectObjectOperation - collect context is empty, next")
|
||||
return collect(d, splatted, remainingMatches)
|
||||
}
|
||||
|
||||
@ -79,6 +83,7 @@ func collect(d *dataTreeNavigator, context Context, remainingMatches *list.List)
|
||||
newCandidate := aggCandidate.Copy()
|
||||
|
||||
newCandidate, err = multiply(multiplyPreferences{AppendArrays: false})(d, context, newCandidate, splatCandidate)
|
||||
|
||||
if err != nil {
|
||||
return Context{}, err
|
||||
}
|
||||
|
@ -5,22 +5,22 @@ import (
|
||||
)
|
||||
|
||||
var collectObjectOperatorScenarios = []expressionScenario{
|
||||
// {
|
||||
// skipDoc: true,
|
||||
// document: `[{name: cat}, {name: dog}]`,
|
||||
// expression: `.[] | {.name: "great"}`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (!!map)::cat: great\n",
|
||||
// "D0, P[], (!!map)::dog: great\n",
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// skipDoc: true,
|
||||
// expression: `({} + {}) | (.b = 3)`,
|
||||
// expected: []string{
|
||||
// "D0, P[], (!!map)::b: 3\n",
|
||||
// },
|
||||
// },
|
||||
{
|
||||
skipDoc: true,
|
||||
document: `[{name: cat}, {name: dog}]`,
|
||||
expression: `.[] | {.name: "great"}`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::cat: great\n",
|
||||
"D0, P[], (!!map)::dog: great\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
expression: `({} + {}) | (.b = 3)`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::b: 3\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: "a: []",
|
||||
@ -62,22 +62,24 @@ var collectObjectOperatorScenarios = []expressionScenario{
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: "{name: Mike}\n",
|
||||
document2: "{name: Bob}\n",
|
||||
expression: `{"wrap": .}`,
|
||||
skipDoc: true,
|
||||
description: "Two documents",
|
||||
document: "{name: Mike}\n",
|
||||
document2: "{name: Bob}\n",
|
||||
expression: `{"wrap": .}`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::wrap: {name: Mike}\n",
|
||||
"D0, P[], (!!map)::wrap: {name: Bob}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: "{name: Mike}\n---\n{name: Bob}",
|
||||
expression: `{"wrap": .}`,
|
||||
skipDoc: true,
|
||||
description: "two embedded documents",
|
||||
document: "{name: Mike}\n---\n{name: Bob}",
|
||||
expression: `{"wrap": .}`,
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::wrap: {name: Mike}\n",
|
||||
"D0, P[], (!!map)::wrap: {name: Bob}\n",
|
||||
"D1, P[], (!!map)::wrap: {name: Bob}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -105,8 +107,8 @@ var collectObjectOperatorScenarios = []expressionScenario{
|
||||
expected: []string{
|
||||
"D0, P[], (!!map)::Mike: cat\n",
|
||||
"D0, P[], (!!map)::Mike: dog\n",
|
||||
"D0, P[], (!!map)::Rosey: monkey\n",
|
||||
"D0, P[], (!!map)::Rosey: sheep\n",
|
||||
"D1, P[], (!!map)::Rosey: monkey\n",
|
||||
"D1, P[], (!!map)::Rosey: sheep\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -51,12 +51,9 @@ func sequenceFor(d *dataTreeNavigator, context Context, matchingNode *CandidateN
|
||||
mapPairs, err := crossFunction(d, context.ChildContext(matches), expressionNode,
|
||||
func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
|
||||
node := CandidateNode{Kind: MappingNode, Tag: "!!map"}
|
||||
log.Debugf("LHS:", NodeToString(lhs))
|
||||
log.Debugf("RHS:", NodeToString(rhs))
|
||||
node.Content = []*CandidateNode{
|
||||
lhs.unwrapDocument(),
|
||||
rhs.unwrapDocument(),
|
||||
}
|
||||
|
||||
node.AddKeyValueChild(lhs, rhs)
|
||||
|
||||
node.Document = document
|
||||
|
||||
return &node, nil
|
||||
|
@ -12,6 +12,22 @@ var createMapOperatorScenarios = []expressionScenario{
|
||||
"D0, P[], (!!seq)::- [{frog: jumps}]\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "sets key properly",
|
||||
expression: `("frog": "jumps") | .[0][0] | .frog`,
|
||||
expected: []string{
|
||||
"D0, P[frog], (!!str)::jumps\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "sets key properly on map",
|
||||
expression: `{"frog": "jumps"} | .frog`,
|
||||
expected: []string{
|
||||
"D0, P[frog], (!!str)::jumps\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
document: `{name: Mike, age: 32}`,
|
||||
expression: `.name: .age`,
|
||||
|
@ -54,8 +54,8 @@ func multiply(preferences multiplyPreferences) func(d *dataTreeNavigator, contex
|
||||
leadingContent, headComment, footComment := getComments(lhs, rhs)
|
||||
lhs = lhs.unwrapDocument()
|
||||
rhs = rhs.unwrapDocument()
|
||||
log.Debugf("Multiplying LHS: %v", lhs.Tag)
|
||||
log.Debugf("- RHS: %v", rhs.Tag)
|
||||
log.Debugf("Multiplying LHS: %v", NodeToString(lhs))
|
||||
log.Debugf("- RHS: %v", NodeToString(rhs))
|
||||
|
||||
if rhs.Tag == "!!null" {
|
||||
return lhs.Copy(), nil
|
||||
@ -93,7 +93,7 @@ func multiplyScalars(lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, er
|
||||
} else if (lhsTag == "!!int" || lhsTag == "!!float") && (rhsTag == "!!int" || rhsTag == "!!float") {
|
||||
return multiplyFloats(lhs, rhs, lhsIsCustom)
|
||||
}
|
||||
return nil, fmt.Errorf("Cannot multiply %v with %v", lhs.Tag, rhs.Tag)
|
||||
return nil, fmt.Errorf("cannot multiply %v with %v", lhs.Tag, rhs.Tag)
|
||||
}
|
||||
|
||||
func multiplyFloats(lhs *CandidateNode, rhs *CandidateNode, lhsIsCustom bool) (*CandidateNode, error) {
|
||||
@ -152,10 +152,14 @@ func mergeObjects(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs
|
||||
var pathIndexToStartFrom int
|
||||
if results.Front() != nil {
|
||||
pathIndexToStartFrom = len(results.Front().Value.(*CandidateNode).GetPath())
|
||||
log.Debugf("pathIndexToStartFrom: %v", pathIndexToStartFrom)
|
||||
}
|
||||
|
||||
for el := results.Front(); el != nil; el = el.Next() {
|
||||
candidate := el.Value.(*CandidateNode)
|
||||
|
||||
log.Debugf("*** going to applied assignment to LHS: %v with RHS: %v", NodeToString(lhs), NodeToString(candidate))
|
||||
|
||||
if candidate.Tag == "!!merge" {
|
||||
continue
|
||||
}
|
||||
@ -164,13 +168,14 @@ func mergeObjects(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("*** applied assignment to LHS: %v", NodeToString(lhs))
|
||||
}
|
||||
return lhs, nil
|
||||
}
|
||||
|
||||
func applyAssignment(d *dataTreeNavigator, context Context, pathIndexToStartFrom int, lhs *CandidateNode, rhs *CandidateNode, preferences multiplyPreferences) error {
|
||||
shouldAppendArrays := preferences.AppendArrays
|
||||
log.Debugf("merge - applyAssignment lhs %v, rhs: %v", lhs.GetKey(), rhs.GetKey())
|
||||
|
||||
lhsPath := rhs.GetPath()[pathIndexToStartFrom:]
|
||||
log.Debugf("merge - lhsPath %v", lhsPath)
|
||||
|
Loading…
Reference in New Issue
Block a user