Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Implemented support of delimiter options #2

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 5 additions & 15 deletions .appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ init:
install:
- ps: |
Execute-Action "installing tools" {
choco install codecov msbuild-sonarqube-runner opencover.portable
choco install codecov opencover.portable -y
dotnet tool install --global dotnet-sonarscanner
}

dotnet_csproj:
Expand All @@ -74,19 +75,10 @@ before_build:
- ps: |
Execute-Action "beginning code analysis" {
if ($env:APPVEYOR_PULL_REQUEST_NUMBER) {
# For security reasons, AppVeyor only exposes secure variables for pull requests from the same repositoy.
# If $env:SONARQUBE_GITHUB_TOKEN is not set, then it is a pull request from another repository and we'll
# skip SonarQube analysis.
if ($env:SONARQUBE_GITHUB_TOKEN) {
MSBuild.SonarQube.Runner.exe begin /o:$env:SONARQUBE_ORGANIZATION /k:$env:APPVEYOR_PROJECT_NAME /v:$env:APPVEYOR_BUILD_VERSION /d:sonar.host.url=https://sonarcloud.io /d:sonar.login=$env:SONARQUBE_TOKEN /d:sonar.cs.opencover.reportsPaths=coverage.xml /d:sonar.coverage.exclusions=**/*Tests.cs,**/*Tests.*.cs,**/*Mock.cs /d:sonar.github.pullRequest=$env:APPVEYOR_PULL_REQUEST_NUMBER /d:sonar.github.repository=$env:APPVEYOR_REPO_NAME /d:sonar.github.oauth=$env:SONARQUBE_GITHUB_TOKEN

$env:SONARQUBE_RUNNING = $true
}
dotnet sonarscanner begin /o:$env:SONARQUBE_ORGANIZATION /k:$env:APPVEYOR_PROJECT_NAME /v:$env:APPVEYOR_BUILD_VERSION /d:sonar.host.url=https://sonarcloud.io /d:sonar.login=$env:SONARQUBE_TOKEN /d:sonar.cs.opencover.reportsPaths=coverage.xml /d:sonar.coverage.exclusions=**/*Tests.cs /d:sonar.github.pullRequest=$env:APPVEYOR_PULL_REQUEST_NUMBER /d:sonar.github.repository=$env:APPVEYOR_REPO_NAME /d:sonar.github.oauth=$env:SONARQUBE_GITHUB_TOKEN
}
else {
MSBuild.SonarQube.Runner.exe begin /o:$env:SONARQUBE_ORGANIZATION /k:$env:APPVEYOR_PROJECT_NAME /v:$env:APPVEYOR_BUILD_VERSION /d:sonar.host.url=https://sonarcloud.io /d:sonar.login=$env:SONARQUBE_TOKEN /d:sonar.cs.opencover.reportsPaths=coverage.xml /d:sonar.coverage.exclusions=**/*Tests.cs,**/*Tests.*.cs,**/*Mock.cs

$env:SONARQUBE_RUNNING = $true
dotnet sonarscanner begin /o:$env:SONARQUBE_ORGANIZATION /k:$env:APPVEYOR_PROJECT_NAME /v:$env:APPVEYOR_BUILD_VERSION /d:sonar.host.url=https://sonarcloud.io /d:sonar.login=$env:SONARQUBE_TOKEN /d:sonar.cs.opencover.reportsPaths=coverage.xml /d:sonar.coverage.exclusions=**/*Tests.cs
}
}

Expand Down Expand Up @@ -134,9 +126,7 @@ after_test:

- ps: |
Execute-Action "ending code analysis" {
if ($env:SONARQUBE_RUNNING -eq $true) {
MSBuild.SonarQube.Runner.exe end /d:sonar.login=$env:SONARQUBE_TOKEN
}
dotnet sonarscanner end /d:sonar.login=$env:SONARQUBE_TOKEN
}

artifacts:
Expand Down
95 changes: 95 additions & 0 deletions src/PartialResponse.Core/DelimiterOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright (c) Arjen Post and contributors. See LICENSE in the project root for license information.

using System;
using System.Collections.Generic;

namespace PartialResponse.Core
{
/// <summary>
/// Represents delimiter options for parser
/// </summary>
public class DelimiterOptions
{
private static DelimiterOptions defaultOptions;

/// <summary>
/// Initializes a new instance of the <see cref="DelimiterOptions"/> class.
/// </summary>
/// <param name="fieldsDelimiters">Characters representing field delimiters.</param>
/// <param name="nestedFieldDelimiters">Characters representing nested field delimiters.</param>
/// <param name="fieldGroupStartDelimiters">Characters representing field group start delimiters.</param>
/// <param name="fieldGroupEndDelimiters">Characters representing field group end delimiters.</param>
public DelimiterOptions(char[] fieldsDelimiters, char[] nestedFieldDelimiters, char[] fieldGroupStartDelimiters, char[] fieldGroupEndDelimiters)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to use dictionaries and not a single char per type?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It actually makes a lot of sense but then there will be too many changes for parser. I've tried that way and found that it becomes very complicated to make a hand-crafted parser. So probably better go baby steps and implement version allowing one to use single character delimiters (most of the delimiters are indeed are a single character). And then work on enhancing it further.

{
var map = new Dictionary<char, TokenType>();

foreach (var c in fieldsDelimiters ?? throw new ArgumentNullException(nameof(fieldsDelimiters)))
{
map.Add(c, TokenType.FieldsDelimiter);
}

foreach (var c in nestedFieldDelimiters ?? throw new ArgumentNullException(nameof(nestedFieldDelimiters)))
{
map.Add(c, TokenType.NestedFieldDelimiter);
}

foreach (var c in fieldGroupStartDelimiters ?? throw new ArgumentNullException(nameof(fieldGroupStartDelimiters)))
{
map.Add(c, TokenType.FieldGroupStartDelimiter);
}

foreach (var c in fieldGroupEndDelimiters ?? throw new ArgumentNullException(nameof(fieldGroupEndDelimiters)))
{
map.Add(c, TokenType.FieldGroupEndDelimiter);
}

this.DelimiterToTokenTypeMap = map;

this.FieldsDelimiters = fieldsDelimiters;
this.NestedFieldDelimiters = nestedFieldDelimiters;
this.FieldGroupStartDelimiters = fieldGroupStartDelimiters;
this.FieldGroupEndDelimiters = fieldGroupEndDelimiters;
}

/// <summary>
/// Gets default options for parser. Default options are:
/// field delimiters - ','
/// nested field delimiters - '/'
/// field group start delimiters - '('
/// field group end delimiters - ')'
/// </summary>
public static DelimiterOptions DefaultOptions
{
get
{
return defaultOptions
?? (defaultOptions = new DelimiterOptions(new[] { ',' }, new[] { '/' }, new[] { '(' }, new[] { ')' }));
}
}

/// <summary>
/// Gets delimiter to token type map.
/// </summary>
public IReadOnlyDictionary<char, TokenType> DelimiterToTokenTypeMap { get; }

/// <summary>
/// Gets fields delimiters.
/// </summary>
public char[] FieldsDelimiters { get; }

/// <summary>
/// Gets nested field delimiters.
/// </summary>
public char[] NestedFieldDelimiters { get; }

/// <summary>
/// Gets nested field group start delimiters.
/// </summary>
public char[] FieldGroupStartDelimiters { get; }

/// <summary>
/// Gets nested field group end delimiters.
/// </summary>
public char[] FieldGroupEndDelimiters { get; }
}
}
27 changes: 12 additions & 15 deletions src/PartialResponse.Core/Field.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Arjen Post. See LICENSE in the project root for license information.
// Copyright (c) Arjen Post and contributors. See LICENSE in the project root for license information.

using System;
using System.Linq;

namespace PartialResponse.Core
{
Expand All @@ -16,15 +17,20 @@ public struct Field
/// <summary>
/// Initializes a new instance of the <see cref="Field"/> structure.
/// </summary>
/// <param name="value">The value of the field.</param>
public Field(string value)
/// <param name="parts">The value of the field.</param>
public Field(params string[] parts)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was the constructor changed? As a result Parser new uses a Stack<List> but Field expects an array, so ToArray is called. Less string manipulation, but is this more efficient compared to the string
parameter?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason was to move responsibility of splitting field parts to parser to not to make a dependency on the delimiter options. But looking at it now it made me realize that it will probably have to have it and Wildcard should be also part of the delimiter options.

{
if (value == null)
if (parts == null)
{
throw new ArgumentNullException(nameof(parts));
}

if (parts.Length == 0 || parts.Any(string.IsNullOrEmpty))
{
throw new ArgumentNullException(nameof(value));
throw new ArgumentException("Parts cannot be empty or null", nameof(parts));
}

this.Parts = value.Split('/');
this.Parts = parts;
}

/// <summary>
Expand Down Expand Up @@ -77,14 +83,5 @@ public bool Matches(string[] parts, bool ignoreCase)

return true;
}

/// <summary>
/// Returns a string that represents the current object.
/// </summary>
/// <returns>A string that represents the current object.</returns>
public override string ToString()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the ToString method removed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, unwillingness to make a dependency on delimiter options. But according to comment above I'll change that. This leads to the question, if there are more than one field part delimiter which one should be used, first one? Or better to keep a reference to original field string? What do you think?

{
return string.Join("/", this.Parts);
}
}
}
28 changes: 20 additions & 8 deletions src/PartialResponse.Core/Fields.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Arjen Post. See LICENSE in the project root for license information.
// Copyright (c) Arjen Post and contributors. See LICENSE in the project root for license information.

using System;
using System.Collections.Generic;
Expand All @@ -14,7 +14,7 @@ namespace PartialResponse.Core
/// from your code.</remarks>
public struct Fields
{
private IEnumerable<Field> values;
private readonly IEnumerable<Field> values;

private Fields(IEnumerable<Field> values)
{
Expand All @@ -36,8 +36,10 @@ public IEnumerable<Field> Values
/// <param name="value">The value.</param>
/// <param name="result">When this method returns, contains the <see cref="Fields"/> equivalent of the value,
/// if the conversion succeeded, or null if the conversion failed.</param>
/// <param name="options">Optional options which allow to specify custom delimiters. If no value provided
/// a default options <see cref="DelimiterOptions.DefaultOptions"/> are used.</param>
/// <returns>true if value was converted successfully; otherwise, false.</returns>
public static bool TryParse(string value, out Fields result)
public static bool TryParse(string value, out Fields result, DelimiterOptions options = null)
{
if (value == null)
{
Expand All @@ -46,7 +48,7 @@ public static bool TryParse(string value, out Fields result)

using (var reader = new StringReader(value))
{
var context = new ParserContext(reader);
var context = new ParserContext(reader, options ?? DelimiterOptions.DefaultOptions);
var parser = new Parser(context);

parser.Parse();
Expand All @@ -68,26 +70,36 @@ public static bool TryParse(string value, out Fields result)
/// Indicates whether a field matches the specified value.
/// </summary>
/// <param name="value">The value to match.</param>
/// <param name="delimiterOptions">Delimiter options to use when matching. If no options provided then the
/// default options <see cref="DelimiterOptions.DefaultOptions"/> are used.</param>
/// <returns>true if a field matches the specified value; otherwise, false.</returns>
public bool Matches(string value)
public bool Matches(string value, DelimiterOptions delimiterOptions = null)
{
return this.Matches(value, false);
return this.Matches(value, false, delimiterOptions);
}

/// <summary>
/// Indicates whether a field matches the specified value.
/// </summary>
/// <param name="value">The value to match.</param>
/// <param name="ignoreCase">A value which indicates whether matching should be case-insensitive.</param>
/// <param name="delimiterOptions">Delimiter options to use when matching. If no options provided then the
/// default options <see cref="DelimiterOptions.DefaultOptions"/> are used.</param>
/// <returns>true if a field matches the specified value; otherwise, false.</returns>
public bool Matches(string value, bool ignoreCase)
public bool Matches(string value, bool ignoreCase, DelimiterOptions delimiterOptions = null)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}

var parts = value.Split('/');
if (!this.Values.Any())
{
return false;
}

var separators = (delimiterOptions ?? DelimiterOptions.DefaultOptions).NestedFieldDelimiters;
var parts = value.Split(separators);

return this.Values.Any(field => field.Matches(parts, ignoreCase));
}
Expand Down
Loading