Skip to content
Merged
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
275 changes: 93 additions & 182 deletions src/Faker.NET.Files/Csv/CsvFaker.cs
Original file line number Diff line number Diff line change
@@ -1,222 +1,133 @@
using System.Collections.Concurrent;
using System;
using Faker.NET.Attributes;
using Faker.NET.Common.Exceptions;

namespace Faker.NET.Files.Csv
namespace Faker.NET.Files.Csv;

public class CsvFaker : IDisposable
{
public class CsvFaker
public static CsvFaker ToFile(string path)
{
/// <summary>
/// Constructor
/// </summary>
public CsvFaker()
{
BaseStream = new MemoryStream();
}

/// <summary>
/// Constructor
/// </summary>
/// <param name="outputFile">Opens stream to write to</param>
public CsvFaker(string outputFile)
{
BaseStream = File.Open(outputFile, new FileStreamOptions
{
Mode = FileMode.OpenOrCreate,
Access = FileAccess.ReadWrite,
});
}

/// <summary>
/// Stream that can be written to for output
/// </summary>
/// <param name="stream">The stream (must have write permissions)</param>
/// <exception cref="Exception"></exception>
public CsvFaker(Stream stream)
{
if (!stream.CanWrite)
{
throw new Exception("Unable to write to stream");
}

BaseStream = stream;
}

public Stream BaseStream { get; private set; }
ThrowHelper.IfNullOrEmpty(path, $"{nameof(CsvFaker)} file path cannot be null or empty.");

/// <summary>
/// Change the delimiter to whatever best suits your uses
/// </summary>
/// <param name="delimiter"></param>
/// <returns></returns>
public CsvFaker UpdateDelimiter(char delimiter)
return new CsvFaker(System.IO.File.Open(path, new FileStreamOptions
{
_delimiter = delimiter;
return this;
}
Mode = FileMode.OpenOrCreate,
Access = FileAccess.ReadWrite,
}));
}

/// <summary>
/// Builder way to add columns and functions to perform based on headers
/// </summary>
/// <param name="columnName"></param>
/// <param name="func"></param>
/// <returns></returns>
public CsvFaker AddColumn(string columnName, Func<string> func)
{
public static CsvFaker ToStream(Stream stream)
{
return new CsvFaker(stream);
}

_headers.Add(columnName, func);
return this;
}
private CsvFaker(Stream stream)
{
ThrowHelper.IfNotWritable(stream);
_stream = new StreamWriter(stream);
}

/// <summary>
/// Adds a column header and the static value that will be put in the rows
/// </summary>
/// <param name="columnName"></param>
/// <param name="value"></param>
/// <returns></returns>
public CsvFaker AddColumn(string columnName, string value)
{
_headers.Add(columnName, () => value);
return this;
}
/// <summary>
/// Change the delimiter to whatever best suits your uses
/// </summary>
/// <param name="delimiter"></param>
/// <returns></returns>
public CsvFaker WithDelimiter(char delimiter)
{
_delimiter = delimiter;
return this;
}

/// <summary>
/// Amount of rows to be generated when <see cref="Generate"/> or <see cref="Generate{T}"/> are called
/// </summary>
/// <returns></returns>
public uint GetRowCount()
{
return _rowsCountToGenerate;
}
/// <summary>
/// Generate a csv based on a type. Properties to have data generated must use an attribute that extends <see cref="FakerAttribute"/>
/// </summary>
/// <typeparam name="T">Type to generate csv from</typeparam>
/// <returns></returns>
public CsvFaker FromClass<T>()
{
var type = typeof(T);

/// <summary>
/// Generate a csv based on a type. Properties to have data generated must use an attribute that extends <see cref="FakerAttribute"/>
/// </summary>
/// <typeparam name="T">Type to generate csv from</typeparam>
/// <returns></returns>
public IEnumerable<string> Generate<T>()
foreach (var prop in type.GetProperties())
{
var type = typeof(T);
var attrs = prop.GetCustomAttributes(typeof(FakerAttribute), true) as FakerAttribute[] ??
throw new Exception();

foreach (var prop in type.GetProperties())
foreach (var attr in attrs)
{
var attrs = prop.GetCustomAttributes(typeof(FakerAttribute), true) as FakerAttribute[] ??
throw new Exception();

foreach (var attr in attrs)
{
_headers.Add(prop.Name, () => attr.GetPropertyValue().ToString() ?? string.Empty);
}
_columnMetadata.Add(prop.Name, () => attr.GetPropertyValue().ToString() ?? string.Empty);
}

return Generate();
}

/// <summary>
/// Defaults to 10 can update to any uint value
/// </summary>
/// <param name="count"></param>
/// <returns></returns>
public CsvFaker Iterations(uint count)
{
_rowsCountToGenerate = count;
return this;
}
return this;
}

/// <summary>
/// Generate a single row with no headers
/// </summary>
/// <returns></returns>
public string GenerateRow()
public CsvFaker WithColumns(Dictionary<string, Func<string>> columns)
{
foreach (var key in columns.Keys)
{
var output = new List<string>();

foreach ((var header, var func) in _headers)
{
var val = func();
if (val.Contains(','))
{
output.Add($"\"{val}\"");
}
else
{
output.Add(val);
}
}

return JoinColumns(output);
_columnMetadata[key] = columns[key];
}
return this;
}

/// <summary>
/// Generates the entire csv (headers and rows).
/// </summary>
/// <returns></returns>
public IEnumerable<string> Generate()
{
yield return JoinColumns(_headers.Keys);

var count = 0;
public CsvFaker WithRowCount(uint rowCount)
{
_rowCount = rowCount;
return this;
}

while (count++ < _rowsCountToGenerate)
{
yield return GenerateRow();
}
}
public void Write(bool keepStreamOpen = false)
{
var headers = _columnMetadata.Keys.ToArray();
var headerCount = headers.Length - 1;
var row = new string[headerCount];
WriteRow(headers);

public string[] GenerateParallel(int batchSize = 1000)
for (var i = 0; i < _rowCount; i++)
{
var arr = new string[_rowsCountToGenerate];
arr[0] = JoinColumns(_headers.Keys);

Parallel.ForEach(Partitioner.Create(1, arr.Length, batchSize), range =>
for (var j = 0; j < headerCount; j++)
{
for (int i = range.Item1; i < range.Item2; i++)
{
arr[i] = GenerateRow();
}
});
row[j] = _columnMetadata[headers[j]]();
}

return arr;
WriteRow(row);
}

/// <summary>
/// Outputs the data to the <see cref="BaseStream"/>.
/// Writes to a <see cref="MemoryStream"/> if a file is not given.
/// </summary>
public void WriteRows()
if (keepStreamOpen)
{
using (var streamWriter = new StreamWriter(BaseStream))
{
foreach (var line in Generate())
{
streamWriter.WriteLine(line);
}
}
_stream.Flush();
}

/// <summary>
/// Clears the headers and value creating funcs. Allows to start fresh.
/// </summary>
/// <returns></returns>
public bool Clear()
else
{
_headers = new();

return _headers.Count == 0;
_stream.Close();
}
}

private string JoinColumns(IEnumerable<string> input)
public void Dispose()
{
try
{
return string.Join(_delimiter, input);
_stream.Dispose();
}

~CsvFaker()
finally
{
BaseStream?.Dispose();
GC.SuppressFinalize(this);
}
}

private Dictionary<string, Func<string>> _headers = new();
private uint _rowsCountToGenerate = 10;
private char _delimiter = ',';
~CsvFaker()
{
_stream?.Dispose();
}
}

private void WriteRow(string[] values)
{
_stream.WriteLine(string.Join(_delimiter, values));
}

private char _delimiter = ',';
private uint _rowCount = 500;
private readonly StreamWriter _stream;
private readonly Dictionary<string, Func<string>> _columnMetadata = new Dictionary<string, Func<string>>();
}
6 changes: 6 additions & 0 deletions src/Faker.NET/Attributes/FakerPersonAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ public class FakerPersonBioAttribute : FakerAttribute
public class FakerPersonFirstNameAttribute : FakerAttribute
{
public Sex? Sex { get; set; } = null;

public override object GetPropertyValue() => Faker.Person.FirstName(Sex);
}

public class FakerPersonFullNameAttribute : FakerAttribute
{
public string? FirstName { get; set; } = null;

public string? LastName { get; set; } = null;

public Sex? Sex { get; set; } = null;

public override object GetPropertyValue() => Faker.Person.FullName(FirstName, LastName, Sex);
Expand Down Expand Up @@ -50,18 +53,21 @@ public class FakerPersonJobTypeAttribute : FakerAttribute
public class FakerPersonLastNameAttribute : FakerAttribute
{
public Sex? Sex { get; set; } = null;

public override object GetPropertyValue() => Faker.Person.LastName(Sex);
}

public class FakerPersonMiddleNameAttribute : FakerAttribute
{
public Sex? Sex { get; set; } = null;

public override object GetPropertyValue() => Faker.Person.MiddleName(Sex);
}

public class FakerPersonPrefixAttribute : FakerAttribute
{
public Sex? Sex { get; set; } = null;

public override object GetPropertyValue() => Faker.Person.Prefix(Sex);
}

Expand Down
16 changes: 16 additions & 0 deletions src/Faker.NET/Common/Exceptions/ThrowHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@ namespace Faker.NET.Common.Exceptions;

public static class ThrowHelper
{
public static void IfNullOrEmpty(string value, string message)
{
if (string.IsNullOrEmpty(value))
{
throw new NullReferenceException(message);
}
}

public static void IfNotWritable(Stream stream, string message = "Unable to write to stream")
{
if (!stream.CanWrite)
{
throw new Exception(message);
}
}

public static void NotImplementedException()
{
throw new NotImplementedException();
Expand Down
Loading
Loading