Got at least one data fetching method working; turns out, we can't use a patched LogicStack to get the data

This commit is contained in:
2026-01-14 22:11:11 +01:00
parent 40a8431464
commit 3f7122d30a
350 changed files with 41444 additions and 119 deletions

View File

@@ -0,0 +1,647 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Swan.Reflection;
namespace Swan.Formatters
{
/// <summary>
/// Represents a reader designed for CSV text.
/// It is capable of deserializing objects from individual lines of CSV text,
/// transforming CSV lines of text into objects,
/// or simply reading the lines of CSV as an array of strings.
/// </summary>
/// <seealso cref="System.IDisposable" />
/// <example>
/// The following example describes how to load a list of objects from a CSV file.
/// <code>
/// using Swan.Formatters;
///
/// class Example
/// {
/// class Person
/// {
/// public string Name { get; set; }
/// public int Age { get; set; }
/// }
///
/// static void Main()
/// {
/// // load records from a CSV file
/// var loadedRecords =
/// CsvReader.LoadRecords&lt;Person&gt;("C:\\Users\\user\\Documents\\file.csv");
///
/// // loadedRecords =
/// // [
/// // { Age = 20, Name = "George" }
/// // { Age = 18, Name = "Juan" }
/// // ]
/// }
/// }
/// </code>
/// The following code explains how to read a CSV formatted string.
/// <code>
/// using Swan.Formatters;
/// using System.Text;
/// using Swan.Formatters;
///
/// class Example
/// {
/// static void Main()
/// {
/// // data to be read
/// var data = @"Company,OpenPositions,MainTechnology,Revenue
/// Co,2,""C#, MySQL, JavaScript, HTML5 and CSS3"",500
/// Ca,2,""C#, MySQL, JavaScript, HTML5 and CSS3"",600";
///
/// using(var stream = new MemoryStream(Encoding.UTF8.GetBytes(data)))
/// {
/// // create a CSV reader
/// var reader = new CsvReader(stream, false, Encoding.UTF8);
/// }
/// }
/// }
/// </code>
/// </example>
public class CsvReader : IDisposable
{
private static readonly PropertyTypeCache TypeCache = new PropertyTypeCache();
private readonly object _syncLock = new object();
private ulong _count;
private char _escapeCharacter = '"';
private char _separatorCharacter = ',';
private bool _hasDisposed; // To detect redundant calls
private string[]? _headings;
private Dictionary<string, string>? _defaultMap;
private StreamReader? _reader;
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="CsvReader" /> class.
/// </summary>
/// <param name="inputStream">The stream.</param>
/// <param name="leaveOpen">if set to <c>true</c> leaves the input stream open.</param>
/// <param name="textEncoding">The text encoding.</param>
public CsvReader(Stream inputStream, bool leaveOpen, Encoding textEncoding)
{
if (inputStream == null)
throw new ArgumentNullException(nameof(inputStream));
if (textEncoding == null)
throw new ArgumentNullException(nameof(textEncoding));
_reader = new StreamReader(inputStream, textEncoding, true, 2048, leaveOpen);
}
/// <summary>
/// Initializes a new instance of the <see cref="CsvReader"/> class.
/// It will automatically close the stream upon disposing.
/// </summary>
/// <param name="stream">The stream.</param>
/// <param name="textEncoding">The text encoding.</param>
public CsvReader(Stream stream, Encoding textEncoding)
: this(stream, false, textEncoding)
{
// placeholder
}
/// <summary>
/// Initializes a new instance of the <see cref="CsvReader"/> class.
/// It automatically closes the stream when disposing this reader
/// and uses the Windows 1253 encoding.
/// </summary>
/// <param name="stream">The stream.</param>
public CsvReader(Stream stream)
: this(stream, false, Definitions.Windows1252Encoding)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CsvReader"/> class.
/// It uses the Windows 1252 Encoding by default and it automatically closes the file
/// when this reader is disposed of.
/// </summary>
/// <param name="filename">The filename.</param>
public CsvReader(string filename)
: this(File.OpenRead(filename), false, Definitions.Windows1252Encoding)
{
// placeholder
}
/// <summary>
/// Initializes a new instance of the <see cref="CsvReader"/> class.
/// It automatically closes the file when disposing this reader.
/// </summary>
/// <param name="filename">The filename.</param>
/// <param name="encoding">The encoding.</param>
public CsvReader(string filename, Encoding encoding)
: this(File.OpenRead(filename), false, encoding)
{
// placeholder
}
#endregion
#region Properties
/// <summary>
/// Gets number of lines that have been read, including the headings.
/// </summary>
/// <value>
/// The count.
/// </value>
public ulong Count
{
get
{
lock (_syncLock)
{
return _count;
}
}
}
/// <summary>
/// Gets or sets the escape character.
/// By default it is the double quote '"'.
/// </summary>
/// <value>
/// The escape character.
/// </value>
public char EscapeCharacter
{
get => _escapeCharacter;
set
{
lock (_syncLock)
{
_escapeCharacter = value;
}
}
}
/// <summary>
/// Gets or sets the separator character.
/// By default it is the comma character ','.
/// </summary>
/// <value>
/// The separator character.
/// </value>
public char SeparatorCharacter
{
get => _separatorCharacter;
set
{
lock (_syncLock)
{
_separatorCharacter = value;
}
}
}
/// <summary>
/// Gets a value indicating whether the stream reader is at the end of the stream
/// In other words, if no more data can be read, this will be set to true.
/// </summary>
/// <value>
/// <c>true</c> if [end of stream]; otherwise, <c>false</c>.
/// </value>
public bool EndOfStream
{
get
{
lock (_syncLock)
{
return _reader?.EndOfStream ?? true;
}
}
}
#endregion
#region Generic, Main ReadLine method
/// <summary>
/// Reads a line of CSV text into an array of strings.
/// </summary>
/// <returns>An array of the specified element type containing copies of the elements of the ArrayList.</returns>
/// <exception cref="System.IO.EndOfStreamException">Cannot read past the end of the stream.</exception>
public string[] ReadLine()
{
if (EndOfStream)
throw new EndOfStreamException("Cannot read past the end of the stream");
lock (_syncLock)
{
var values = ParseRecord(_reader!, _escapeCharacter, _separatorCharacter);
_count++;
return values;
}
}
#endregion
#region Read Methods
/// <summary>
/// Skips a line of CSV text.
/// This operation does not increment the Count property and it is useful when you need to read the headings
/// skipping over a few lines as Reading headings is only supported
/// as the first read operation (i.e. while count is still 0).
/// </summary>
/// <exception cref="System.IO.EndOfStreamException">Cannot read past the end of the stream.</exception>
public void SkipRecord()
{
if (EndOfStream)
throw new EndOfStreamException("Cannot read past the end of the stream");
lock (_syncLock)
{
ParseRecord(_reader!, _escapeCharacter, _separatorCharacter);
}
}
/// <summary>
/// Reads a line of CSV text and stores the values read as a representation of the column names
/// to be used for parsing objects. You have to call this method before calling ReadObject methods.
/// </summary>
/// <returns>An array of the specified element type containing copies of the elements of the ArrayList.</returns>
/// <exception cref="System.InvalidOperationException">
/// Reading headings is only supported as the first read operation.
/// or
/// ReadHeadings.
/// </exception>
/// <exception cref="System.IO.EndOfStreamException">Cannot read past the end of the stream.</exception>
public string[] ReadHeadings()
{
lock (_syncLock)
{
if (_headings != null)
throw new InvalidOperationException($"The {nameof(ReadHeadings)} method had already been called.");
if (_count != 0)
throw new InvalidOperationException("Reading headings is only supported as the first read operation.");
_headings = ReadLine();
_defaultMap = _headings.ToDictionary(x => x, x => x);
return _headings.ToArray();
}
}
/// <summary>
/// Reads a line of CSV text, converting it into a dynamic object in which properties correspond to the names of the headings.
/// </summary>
/// <param name="map">The mappings between CSV headings (keys) and object properties (values).</param>
/// <returns>Object of the type of the elements in the collection of key/value pairs.</returns>
/// <exception cref="System.InvalidOperationException">ReadHeadings.</exception>
/// <exception cref="System.IO.EndOfStreamException">Cannot read past the end of the stream.</exception>
/// <exception cref="System.ArgumentNullException">map.</exception>
public IDictionary<string, object> ReadObject(IDictionary<string, string> map)
{
lock (_syncLock)
{
if (_headings == null)
throw new InvalidOperationException($"Call the {nameof(ReadHeadings)} method before reading as an object.");
if (map == null)
throw new ArgumentNullException(nameof(map));
var result = new Dictionary<string, object>();
var values = ReadLine();
for (var i = 0; i < _headings.Length; i++)
{
if (i > values.Length - 1)
break;
result[_headings[i]] = values[i];
}
return result;
}
}
/// <summary>
/// Reads a line of CSV text, converting it into a dynamic object
/// The property names correspond to the names of the CSV headings.
/// </summary>
/// <returns>Object of the type of the elements in the collection of key/value pairs.</returns>
public IDictionary<string, object> ReadObject() => ReadObject(_defaultMap);
/// <summary>
/// Reads a line of CSV text converting it into an object of the given type, using a map (or Dictionary)
/// where the keys are the names of the headings and the values are the names of the instance properties
/// in the given Type. The result object must be already instantiated.
/// </summary>
/// <typeparam name="T">The type of object to map.</typeparam>
/// <param name="map">The map.</param>
/// <param name="result">The result.</param>
/// <exception cref="System.ArgumentNullException">map
/// or
/// result.</exception>
/// <exception cref="System.InvalidOperationException">ReadHeadings.</exception>
/// <exception cref="System.IO.EndOfStreamException">Cannot read past the end of the stream.</exception>
public void ReadObject<T>(IDictionary<string, string> map, ref T result)
{
lock (_syncLock)
{
// Check arguments
{
if (map == null)
throw new ArgumentNullException(nameof(map));
if (_reader.EndOfStream)
throw new EndOfStreamException("Cannot read past the end of the stream");
if (_headings == null)
throw new InvalidOperationException($"Call the {nameof(ReadHeadings)} method before reading as an object.");
if (Equals(result, default(T)))
throw new ArgumentNullException(nameof(result));
}
// Read line and extract values
var values = ReadLine();
// Extract properties from cache
var properties = TypeCache
.RetrieveFilteredProperties(typeof(T), true, x => x.CanWrite && Definitions.BasicTypesInfo.Value.ContainsKey(x.PropertyType));
// Assign property values for each heading
for (var i = 0; i < _headings.Length; i++)
{
// break if no more headings are matched
if (i > values.Length - 1)
break;
// skip if no heading is available or the heading is empty
if (map.ContainsKey(_headings[i]) == false &&
string.IsNullOrWhiteSpace(map[_headings[i]]) == false)
continue;
// Prepare the target property
var propertyName = map[_headings[i]];
// Parse and assign the basic type value to the property if exists
properties
.FirstOrDefault(p => p.Name == propertyName)?
.TrySetBasicType(values[i], result);
}
}
}
/// <summary>
/// Reads a line of CSV text converting it into an object of the given type, using a map (or Dictionary)
/// where the keys are the names of the headings and the values are the names of the instance properties
/// in the given Type.
/// </summary>
/// <typeparam name="T">The type of object to map.</typeparam>
/// <param name="map">The map of CSV headings (keys) and Type property names (values).</param>
/// <returns>The conversion of specific type of object.</returns>
/// <exception cref="System.ArgumentNullException">map.</exception>
/// <exception cref="System.InvalidOperationException">ReadHeadings.</exception>
/// <exception cref="System.IO.EndOfStreamException">Cannot read past the end of the stream.</exception>
public T ReadObject<T>(IDictionary<string, string> map)
where T : new()
{
if (map == null)
throw new ArgumentNullException(nameof(map));
var result = Activator.CreateInstance<T>();
ReadObject(map, ref result);
return result;
}
/// <summary>
/// Reads a line of CSV text converting it into an object of the given type, and assuming
/// the property names of the target type match the heading names of the file.
/// </summary>
/// <typeparam name="T">The type of object.</typeparam>
/// <returns>The conversion of specific type of object.</returns>
public T ReadObject<T>()
where T : new() =>
ReadObject<T>(_defaultMap);
#endregion
#region Support Methods
/// <summary>
/// Parses a line of standard CSV text into an array of strings.
/// Note that quoted values might have new line sequences in them. Field values will contain such sequences.
/// </summary>
/// <param name="reader">The reader.</param>
/// <param name="escapeCharacter">The escape character.</param>
/// <param name="separatorCharacter">The separator character.</param>
/// <returns>An array of the specified element type containing copies of the elements of the ArrayList.</returns>
private static string[] ParseRecord(StreamReader reader, char escapeCharacter = '"', char separatorCharacter = ',')
{
var values = new List<string>();
var currentValue = new StringBuilder(1024);
var currentState = ReadState.WaitingForNewField;
string line;
while ((line = reader.ReadLine()) != null)
{
for (var charIndex = 0; charIndex < line.Length; charIndex++)
{
// Get the current and next character
var currentChar = line[charIndex];
var nextChar = charIndex < line.Length - 1 ? line[charIndex + 1] : default(char?);
// Perform logic based on state and decide on next state
switch (currentState)
{
case ReadState.WaitingForNewField:
{
currentValue.Clear();
if (currentChar == escapeCharacter)
{
currentState = ReadState.PushingQuoted;
continue;
}
if (currentChar == separatorCharacter)
{
values.Add(currentValue.ToString());
currentState = ReadState.WaitingForNewField;
continue;
}
currentValue.Append(currentChar);
currentState = ReadState.PushingNormal;
continue;
}
case ReadState.PushingNormal:
{
// Handle field content delimiter by comma
if (currentChar == separatorCharacter)
{
currentState = ReadState.WaitingForNewField;
values.Add(currentValue.ToString());
currentValue.Clear();
continue;
}
// Handle double quote escaping
if (currentChar == escapeCharacter && nextChar.HasValue && nextChar == escapeCharacter)
{
// advance 1 character now. The loop will advance one more.
currentValue.Append(currentChar);
charIndex++;
continue;
}
currentValue.Append(currentChar);
break;
}
case ReadState.PushingQuoted:
{
// Handle field content delimiter by ending double quotes
if (currentChar == escapeCharacter && (nextChar.HasValue == false || nextChar != escapeCharacter))
{
currentState = ReadState.PushingNormal;
continue;
}
// Handle double quote escaping
if (currentChar == escapeCharacter && nextChar.HasValue && nextChar == escapeCharacter)
{
// advance 1 character now. The loop will advance one more.
currentValue.Append(currentChar);
charIndex++;
continue;
}
currentValue.Append(currentChar);
break;
}
}
}
// determine if we need to continue reading a new line if it is part of the quoted
// field value
if (currentState == ReadState.PushingQuoted)
{
// we need to add the new line sequence to the output of the field
// because we were pushing a quoted value
currentValue.Append(Environment.NewLine);
}
else
{
// push anything that has not been pushed (flush) into a last value
values.Add(currentValue.ToString());
currentValue.Clear();
// stop reading more lines we have reached the end of the CSV record
break;
}
}
// If we ended up pushing quoted and no closing quotes we might
// have additional text in yt
if (currentValue.Length > 0)
{
values.Add(currentValue.ToString());
}
return values.ToArray();
}
#endregion
#region Helpers
/// <summary>
/// Loads the records from the stream
/// This method uses Windows 1252 encoding.
/// </summary>
/// <typeparam name="T">The type of IList items to load.</typeparam>
/// <param name="stream">The stream.</param>
/// <returns>A generic collection of objects that can be individually accessed by index.</returns>
public static IList<T> LoadRecords<T>(Stream stream)
where T : new()
{
var result = new List<T>();
using (var reader = new CsvReader(stream))
{
reader.ReadHeadings();
while (!reader.EndOfStream)
{
result.Add(reader.ReadObject<T>());
}
}
return result;
}
/// <summary>
/// Loads the records from the give file path.
/// This method uses Windows 1252 encoding.
/// </summary>
/// <typeparam name="T">The type of IList items to load.</typeparam>
/// <param name="filePath">The file path.</param>
/// <returns>A generic collection of objects that can be individually accessed by index.</returns>
public static IList<T> LoadRecords<T>(string filePath)
where T : new() =>
LoadRecords<T>(File.OpenRead(filePath));
#endregion
#region IDisposable Support
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (_hasDisposed) return;
if (disposing)
{
try
{
_reader?.Dispose();
}
finally
{
_reader = null;
}
}
_hasDisposed = true;
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
/// <summary>
/// Defines the 3 different read states
/// for the parsing state machine.
/// </summary>
private enum ReadState
{
WaitingForNewField,
PushingNormal,
PushingQuoted,
}
}
}

View File

@@ -0,0 +1,460 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Swan.Reflection;
namespace Swan.Formatters
{
/// <summary>
/// A CSV writer useful for exporting a set of objects.
/// </summary>
/// <example>
/// The following code describes how to save a list of objects into a CSV file.
/// <code>
/// using System.Collections.Generic;
/// using Swan.Formatters;
///
/// class Example
/// {
/// class Person
/// {
/// public string Name { get; set; }
/// public int Age { get; set; }
/// }
///
/// static void Main()
/// {
/// // create a list of people
/// var people = new List&lt;Person&gt;
/// {
/// new Person { Name = "Artyom", Age = 20 },
/// new Person { Name = "Aloy", Age = 18 }
/// }
///
/// // write items inside file.csv
/// CsvWriter.SaveRecords(people, "C:\\Users\\user\\Documents\\file.csv");
///
/// // output
/// // | Name | Age |
/// // | Artyom | 20 |
/// // | Aloy | 18 |
/// }
/// }
/// </code>
/// </example>
public class CsvWriter : IDisposable
{
private static readonly PropertyTypeCache TypeCache = new PropertyTypeCache();
private readonly object _syncLock = new object();
private readonly Stream _outputStream;
private readonly Encoding _encoding;
private readonly bool _leaveStreamOpen;
private bool _isDisposing;
private ulong _mCount;
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="CsvWriter" /> class.
/// </summary>
/// <param name="outputStream">The output stream.</param>
/// <param name="leaveOpen">if set to <c>true</c> [leave open].</param>
/// <param name="encoding">The encoding.</param>
public CsvWriter(Stream outputStream, bool leaveOpen, Encoding encoding)
{
_outputStream = outputStream;
_encoding = encoding;
_leaveStreamOpen = leaveOpen;
}
/// <summary>
/// Initializes a new instance of the <see cref="CsvWriter"/> class.
/// It automatically closes the stream when disposing this writer.
/// </summary>
/// <param name="outputStream">The output stream.</param>
/// <param name="encoding">The encoding.</param>
public CsvWriter(Stream outputStream, Encoding encoding)
: this(outputStream, false, encoding)
{
// placeholder
}
/// <summary>
/// Initializes a new instance of the <see cref="CsvWriter"/> class.
/// It uses the Windows 1252 encoding and automatically closes
/// the stream upon disposing this writer.
/// </summary>
/// <param name="outputStream">The output stream.</param>
public CsvWriter(Stream outputStream)
: this(outputStream, false, Definitions.Windows1252Encoding)
{
// placeholder
}
/// <summary>
/// Initializes a new instance of the <see cref="CsvWriter"/> class.
/// It opens the file given file, automatically closes the stream upon
/// disposing of this writer, and uses the Windows 1252 encoding.
/// </summary>
/// <param name="filename">The filename.</param>
public CsvWriter(string filename)
: this(File.OpenWrite(filename), false, Definitions.Windows1252Encoding)
{
// placeholder
}
/// <summary>
/// Initializes a new instance of the <see cref="CsvWriter"/> class.
/// It opens the file given file, automatically closes the stream upon
/// disposing of this writer, and uses the given text encoding for output.
/// </summary>
/// <param name="filename">The filename.</param>
/// <param name="encoding">The encoding.</param>
public CsvWriter(string filename, Encoding encoding)
: this(File.OpenWrite(filename), false, encoding)
{
// placeholder
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the field separator character.
/// </summary>
/// <value>
/// The separator character.
/// </value>
public char SeparatorCharacter { get; set; } = ',';
/// <summary>
/// Gets or sets the escape character to use to escape field values.
/// </summary>
/// <value>
/// The escape character.
/// </value>
public char EscapeCharacter { get; set; } = '"';
/// <summary>
/// Gets or sets the new line character sequence to use when writing a line.
/// </summary>
/// <value>
/// The new line sequence.
/// </value>
public string NewLineSequence { get; set; } = Environment.NewLine;
/// <summary>
/// Defines a list of properties to ignore when outputting CSV lines.
/// </summary>
/// <value>
/// The ignore property names.
/// </value>
public List<string> IgnorePropertyNames { get; } = new List<string>();
/// <summary>
/// Gets number of lines that have been written, including the headings line.
/// </summary>
/// <value>
/// The count.
/// </value>
public ulong Count
{
get
{
lock (_syncLock)
{
return _mCount;
}
}
}
#endregion
#region Helpers
/// <summary>
/// Saves the items to a stream.
/// It uses the Windows 1252 text encoding for output.
/// </summary>
/// <typeparam name="T">The type of enumeration.</typeparam>
/// <param name="items">The items.</param>
/// <param name="stream">The stream.</param>
/// <param name="truncateData"><c>true</c> if stream is truncated, default <c>false</c>.</param>
/// <returns>Number of item saved.</returns>
public static int SaveRecords<T>(IEnumerable<T> items, Stream stream, bool truncateData = false)
{
// truncate the file if it had data
if (truncateData && stream.Length > 0)
stream.SetLength(0);
using var writer = new CsvWriter(stream);
writer.WriteHeadings<T>();
writer.WriteObjects(items);
return (int)writer.Count;
}
/// <summary>
/// Saves the items to a CSV file.
/// If the file exits, it overwrites it. If it does not, it creates it.
/// It uses the Windows 1252 text encoding for output.
/// </summary>
/// <typeparam name="T">The type of enumeration.</typeparam>
/// <param name="items">The items.</param>
/// <param name="filePath">The file path.</param>
/// <returns>Number of item saved.</returns>
public static int SaveRecords<T>(IEnumerable<T> items, string filePath) => SaveRecords(items, File.OpenWrite(filePath), true);
#endregion
#region Generic, main Write Line Method
/// <summary>
/// Writes a line of CSV text. Items are converted to strings.
/// If items are found to be null, empty strings are written out.
/// If items are not string, the ToStringInvariant() method is called on them.
/// </summary>
/// <param name="items">The items.</param>
public void WriteLine(params object[] items)
=> WriteLine(items.Select(x => x == null ? string.Empty : x.ToStringInvariant()));
/// <summary>
/// Writes a line of CSV text. Items are converted to strings.
/// If items are found to be null, empty strings are written out.
/// If items are not string, the ToStringInvariant() method is called on them.
/// </summary>
/// <param name="items">The items.</param>
public void WriteLine(IEnumerable<object> items)
=> WriteLine(items.Select(x => x == null ? string.Empty : x.ToStringInvariant()));
/// <summary>
/// Writes a line of CSV text.
/// If items are found to be null, empty strings are written out.
/// </summary>
/// <param name="items">The items.</param>
public void WriteLine(params string[] items) => WriteLine((IEnumerable<string>) items);
/// <summary>
/// Writes a line of CSV text.
/// If items are found to be null, empty strings are written out.
/// </summary>
/// <param name="items">The items.</param>
public void WriteLine(IEnumerable<string> items)
{
lock (_syncLock)
{
var length = items.Count();
var separatorBytes = _encoding.GetBytes(new[] { SeparatorCharacter });
var endOfLineBytes = _encoding.GetBytes(NewLineSequence);
// Declare state variables here to avoid recreation, allocation and
// reassignment in every loop
bool needsEnclosing;
string textValue;
byte[] output;
for (var i = 0; i < length; i++)
{
textValue = items.ElementAt(i);
// Determine if we need the string to be enclosed
// (it either contains an escape, new line, or separator char)
needsEnclosing = textValue.IndexOf(SeparatorCharacter) >= 0
|| textValue.IndexOf(EscapeCharacter) >= 0
|| textValue.IndexOf('\r') >= 0
|| textValue.IndexOf('\n') >= 0;
// Escape the escape characters by repeating them twice for every instance
textValue = textValue.Replace($"{EscapeCharacter}",
$"{EscapeCharacter}{EscapeCharacter}");
// Enclose the text value if we need to
if (needsEnclosing)
textValue = string.Format($"{EscapeCharacter}{textValue}{EscapeCharacter}", textValue);
// Get the bytes to write to the stream and write them
output = _encoding.GetBytes(textValue);
_outputStream.Write(output, 0, output.Length);
// only write a separator if we are moving in between values.
// the last value should not be written.
if (i < length - 1)
_outputStream.Write(separatorBytes, 0, separatorBytes.Length);
}
// output the newline sequence
_outputStream.Write(endOfLineBytes, 0, endOfLineBytes.Length);
_mCount += 1;
}
}
#endregion
#region Write Object Method
/// <summary>
/// Writes a row of CSV text. It handles the special cases where the object is
/// a dynamic object or and array. It also handles non-collection objects fine.
/// If you do not like the way the output is handled, you can simply write an extension
/// method of this class and use the WriteLine method instead.
/// </summary>
/// <param name="item">The item.</param>
/// <exception cref="System.ArgumentNullException">item.</exception>
public void WriteObject(object? item)
{
if (item == null)
throw new ArgumentNullException(nameof(item));
lock (_syncLock)
{
switch (item)
{
case IDictionary typedItem:
WriteLine(GetFilteredDictionary(typedItem));
return;
case ICollection typedItem:
WriteLine(typedItem.Cast<object>());
return;
default:
WriteLine(GetFilteredTypeProperties(item.GetType()).Select(x => item.ReadProperty(x.Name)));
break;
}
}
}
/// <summary>
/// Writes a row of CSV text. It handles the special cases where the object is
/// a dynamic object or and array. It also handles non-collection objects fine.
/// If you do not like the way the output is handled, you can simply write an extension
/// method of this class and use the WriteLine method instead.
/// </summary>
/// <typeparam name="T">The type of object to write.</typeparam>
/// <param name="item">The item.</param>
public void WriteObject<T>(T item) => WriteObject(item as object);
/// <summary>
/// Writes a set of items, one per line and atomically by repeatedly calling the
/// WriteObject method. For more info check out the description of the WriteObject
/// method.
/// </summary>
/// <typeparam name="T">The type of object to write.</typeparam>
/// <param name="items">The items.</param>
/// <exception cref="ArgumentNullException">items.</exception>
public void WriteObjects<T>(IEnumerable<T> items)
{
if (items == null)
throw new ArgumentNullException(nameof(items));
lock (_syncLock)
{
foreach (var item in items)
WriteObject(item);
}
}
#endregion
#region Write Headings Methods
/// <summary>
/// Writes the headings.
/// </summary>
/// <param name="type">The type of object to extract headings.</param>
/// <exception cref="System.ArgumentNullException">type.</exception>
public void WriteHeadings(Type type)
{
if (type == null)
throw new ArgumentNullException(nameof(type));
var properties = GetFilteredTypeProperties(type).Select(p => p.Name).Cast<object>();
WriteLine(properties);
}
/// <summary>
/// Writes the headings.
/// </summary>
/// <typeparam name="T">The type of object to extract headings.</typeparam>
public void WriteHeadings<T>() => WriteHeadings(typeof(T));
/// <summary>
/// Writes the headings.
/// </summary>
/// <param name="dictionary">The dictionary to extract headings.</param>
/// <exception cref="System.ArgumentNullException">dictionary.</exception>
public void WriteHeadings(IDictionary dictionary)
{
if (dictionary == null)
throw new ArgumentNullException(nameof(dictionary));
WriteLine(GetFilteredDictionary(dictionary, true));
}
/// <summary>
/// Writes the headings.
/// </summary>
/// <param name="obj">The object to extract headings.</param>
/// <exception cref="ArgumentNullException">obj.</exception>
public void WriteHeadings(object obj)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
WriteHeadings(obj.GetType());
}
#endregion
#region IDisposable Support
/// <inheritdoc />
public void Dispose() => Dispose(true);
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposeAlsoManaged"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposeAlsoManaged)
{
if (_isDisposing) return;
if (disposeAlsoManaged)
{
if (_leaveStreamOpen == false)
{
_outputStream.Dispose();
}
}
_isDisposing = true;
}
#endregion
#region Support Methods
private IEnumerable<string> GetFilteredDictionary(IDictionary dictionary, bool filterKeys = false)
=> dictionary
.Keys
.Cast<object>()
.Select(key => key == null ? string.Empty : key.ToStringInvariant())
.Where(stringKey => !IgnorePropertyNames.Contains(stringKey))
.Select(stringKey =>
filterKeys
? stringKey
: dictionary[stringKey] == null ? string.Empty : dictionary[stringKey].ToStringInvariant());
private IEnumerable<PropertyInfo> GetFilteredTypeProperties(Type type)
=> TypeCache.Retrieve(type, t =>
t.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead))
.Where(p => !IgnorePropertyNames.Contains(p.Name));
#endregion
}
}

View File

@@ -0,0 +1,147 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Swan.Formatters
{
internal class HumanizeJson
{
private readonly StringBuilder _builder = new StringBuilder();
private readonly int _indent;
private readonly string _indentStr;
private readonly object? _obj;
public HumanizeJson(object? obj, int indent)
{
_obj = obj;
_indent = indent;
_indentStr = new string(' ', indent * 4);
ParseObject();
}
public string GetResult() => _obj == null ? string.Empty : _builder.ToString().TrimEnd();
private void ParseObject()
{
if (_obj == null)
{
return;
}
switch (_obj)
{
case Dictionary<string, object> dictionary:
AppendDictionary(dictionary);
break;
case List<object> list:
AppendList(list);
break;
default:
AppendString(_obj.ToString());
break;
}
}
private void AppendDictionary(Dictionary<string, object> objects)
{
foreach (var kvp in objects)
{
if (kvp.Value == null) continue;
var writeOutput = false;
switch (kvp.Value)
{
case Dictionary<string, object> valueDictionary:
if (valueDictionary.Count > 0)
{
writeOutput = true;
_builder
.Append($"{_indentStr}{kvp.Key,-16}: object")
.AppendLine();
}
break;
case List<object> valueList:
if (valueList.Count > 0)
{
writeOutput = true;
_builder
.Append($"{_indentStr}{kvp.Key,-16}: array[{valueList.Count}]")
.AppendLine();
}
break;
default:
writeOutput = true;
_builder.Append($"{_indentStr}{kvp.Key,-16}: ");
break;
}
if (writeOutput)
_builder.AppendLine(new HumanizeJson(kvp.Value, _indent + 1).GetResult());
}
}
private void AppendList(IEnumerable<object> objects)
{
var index = 0;
foreach (var value in objects)
{
var writeOutput = false;
switch (value)
{
case Dictionary<string, object> valueDictionary:
if (valueDictionary.Count > 0)
{
writeOutput = true;
_builder
.Append($"{_indentStr}[{index}]: object")
.AppendLine();
}
break;
case List<object> valueList:
if (valueList.Count > 0)
{
writeOutput = true;
_builder
.Append($"{_indentStr}[{index}]: array[{valueList.Count}]")
.AppendLine();
}
break;
default:
writeOutput = true;
_builder.Append($"{_indentStr}[{index}]: ");
break;
}
index++;
if (writeOutput)
_builder.AppendLine(new HumanizeJson(value, _indent + 1).GetResult());
}
}
private void AppendString(string stringValue)
{
if (stringValue.Length + _indentStr.Length <= 96 && stringValue.IndexOf('\r') < 0 &&
stringValue.IndexOf('\n') < 0)
{
_builder.Append($"{stringValue}");
return;
}
_builder.AppendLine();
var stringLines = stringValue.ToLines().Select(l => l.Trim());
foreach (var line in stringLines)
{
_builder.AppendLine($"{_indentStr}{line}");
}
}
}
}

View File

@@ -0,0 +1,353 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Swan.Reflection;
namespace Swan.Formatters
{
/// <summary>
/// A very simple, light-weight JSON library written by Mario
/// to teach Geo how things are done
///
/// This is an useful helper for small tasks but it doesn't represent a full-featured
/// serializer such as the beloved Json.NET.
/// </summary>
public static partial class Json
{
private class Converter
{
private static readonly ConcurrentDictionary<MemberInfo, string> MemberInfoNameCache =
new ConcurrentDictionary<MemberInfo, string>();
private static readonly ConcurrentDictionary<Type, Type?> ListAddMethodCache = new ConcurrentDictionary<Type, Type?>();
private readonly object? _target;
private readonly Type _targetType;
private readonly bool _includeNonPublic;
private readonly JsonSerializerCase _jsonSerializerCase;
private Converter(
object? source,
Type targetType,
ref object? targetInstance,
bool includeNonPublic,
JsonSerializerCase jsonSerializerCase)
{
_targetType = targetInstance != null ? targetInstance.GetType() : targetType;
_includeNonPublic = includeNonPublic;
_jsonSerializerCase = jsonSerializerCase;
if (source == null)
{
return;
}
var sourceType = source.GetType();
if (_targetType == null || _targetType == typeof(object)) _targetType = sourceType;
if (sourceType == _targetType)
{
_target = source;
return;
}
if (!TrySetInstance(targetInstance, source, ref _target))
return;
ResolveObject(source, ref _target);
}
internal static object? FromJsonResult(
object? source,
JsonSerializerCase jsonSerializerCase,
Type? targetType = null,
bool includeNonPublic = false)
{
object? nullRef = null;
return new Converter(source, targetType ?? typeof(object), ref nullRef, includeNonPublic, jsonSerializerCase).GetResult();
}
private static object? FromJsonResult(object source,
Type targetType,
ref object? targetInstance,
bool includeNonPublic)
{
return new Converter(source, targetType, ref targetInstance, includeNonPublic, JsonSerializerCase.None).GetResult();
}
private static Type? GetAddMethodParameterType(Type targetType)
=> ListAddMethodCache.GetOrAdd(targetType,
x => x.GetMethods()
.FirstOrDefault(
m => m.Name == AddMethodName && m.IsPublic && m.GetParameters().Length == 1)?
.GetParameters()[0]
.ParameterType);
private static void GetByteArray(string sourceString, ref object? target)
{
try
{
target = Convert.FromBase64String(sourceString);
} // Try conversion from Base 64
catch (FormatException)
{
target = Encoding.UTF8.GetBytes(sourceString);
} // Get the string bytes in UTF8
}
private object? GetSourcePropertyValue(
IDictionary<string, object> sourceProperties,
MemberInfo targetProperty)
{
var targetPropertyName = MemberInfoNameCache.GetOrAdd(
targetProperty,
x => AttributeCache.DefaultCache.Value.RetrieveOne<JsonPropertyAttribute>(x)?.PropertyName ?? x.Name.GetNameWithCase(_jsonSerializerCase));
return sourceProperties!.GetValueOrDefault(targetPropertyName);
}
private bool TrySetInstance(object? targetInstance, object source, ref object? target)
{
if (targetInstance == null)
{
// Try to create a default instance
try
{
source.CreateTarget(_targetType, _includeNonPublic, ref target);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch
#pragma warning restore CA1031 // Do not catch general exception types
{
return false;
}
}
else
{
target = targetInstance;
}
return true;
}
private object? GetResult() => _target ?? _targetType.GetDefault();
private void ResolveObject(object source, ref object? target)
{
switch (source)
{
// Case 0: Special Cases Handling (Source and Target are of specific convertible types)
// Case 0.1: Source is string, Target is byte[]
case string sourceString when _targetType == typeof(byte[]):
GetByteArray(sourceString, ref target);
break;
// Case 1.1: Source is Dictionary, Target is IDictionary
case Dictionary<string, object> sourceProperties when target is IDictionary targetDictionary:
PopulateDictionary(sourceProperties, targetDictionary);
break;
// Case 1.2: Source is Dictionary, Target is not IDictionary (i.e. it is a complex type)
case Dictionary<string, object> sourceProperties:
PopulateObject(sourceProperties);
break;
// Case 2.1: Source is List, Target is Array
case List<object> sourceList when target is Array targetArray:
PopulateArray(sourceList, targetArray);
break;
// Case 2.2: Source is List, Target is IList
case List<object> sourceList when target is IList targetList:
PopulateIList(sourceList, targetList);
break;
// Case 3: Source is a simple type; Attempt conversion
default:
var sourceStringValue = source.ToStringInvariant();
// Handle basic types or enumerations if not
if (!_targetType.TryParseBasicType(sourceStringValue, out target))
GetEnumValue(sourceStringValue, ref target);
break;
}
}
private void PopulateIList(IEnumerable<object> objects, IList list)
{
var parameterType = GetAddMethodParameterType(_targetType);
if (parameterType == null) return;
foreach (var item in objects)
{
try
{
list.Add(FromJsonResult(
item,
_jsonSerializerCase,
parameterType,
_includeNonPublic));
}
#pragma warning disable CA1031 // Do not catch general exception types
catch
#pragma warning restore CA1031 // Do not catch general exception types
{
// ignored
}
}
}
private void PopulateArray(IList<object> objects, Array array)
{
var elementType = _targetType.GetElementType();
for (var i = 0; i < objects.Count; i++)
{
try
{
var targetItem = FromJsonResult(
objects[i],
_jsonSerializerCase,
elementType,
_includeNonPublic);
array.SetValue(targetItem, i);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch
#pragma warning restore CA1031 // Do not catch general exception types
{
// ignored
}
}
}
private void GetEnumValue(string sourceStringValue, ref object? target)
{
var enumType = Nullable.GetUnderlyingType(_targetType);
if (enumType == null && _targetType.IsEnum) enumType = _targetType;
if (enumType == null) return;
try
{
target = Enum.Parse(enumType, sourceStringValue);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch
#pragma warning restore CA1031 // Do not catch general exception types
{
// ignored
}
}
private void PopulateDictionary(IDictionary<string, object> sourceProperties, IDictionary targetDictionary)
{
// find the add method of the target dictionary
var addMethod = _targetType.GetMethods()
.FirstOrDefault(
m => m.Name == AddMethodName && m.IsPublic && m.GetParameters().Length == 2);
// skip if we don't have a compatible add method
if (addMethod == null) return;
var addMethodParameters = addMethod.GetParameters();
if (addMethodParameters[0].ParameterType != typeof(string)) return;
// Retrieve the target entry type
var targetEntryType = addMethodParameters[1].ParameterType;
// Add the items to the target dictionary
foreach (var sourceProperty in sourceProperties)
{
try
{
var targetEntryValue = FromJsonResult(
sourceProperty.Value,
_jsonSerializerCase,
targetEntryType,
_includeNonPublic);
targetDictionary.Add(sourceProperty.Key, targetEntryValue);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch
#pragma warning restore CA1031 // Do not catch general exception types
{
// ignored
}
}
}
private void PopulateObject(IDictionary<string, object> sourceProperties)
{
if (sourceProperties == null)
return;
if (_targetType.IsValueType)
PopulateFields(sourceProperties);
PopulateProperties(sourceProperties);
}
private void PopulateProperties(IDictionary<string, object> sourceProperties)
{
var properties = PropertyTypeCache.DefaultCache.Value.RetrieveFilteredProperties(_targetType, false, p => p.CanWrite);
foreach (var property in properties)
{
var sourcePropertyValue = GetSourcePropertyValue(sourceProperties, property);
if (sourcePropertyValue == null) continue;
try
{
var currentPropertyValue = !property.PropertyType.IsArray
? _target.ReadProperty(property.Name)
: null;
var targetPropertyValue = FromJsonResult(
sourcePropertyValue,
property.PropertyType,
ref currentPropertyValue,
_includeNonPublic);
_target.WriteProperty(property.Name, targetPropertyValue);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch
#pragma warning restore CA1031 // Do not catch general exception types
{
// ignored
}
}
}
private void PopulateFields(IDictionary<string, object> sourceProperties)
{
foreach (var field in FieldTypeCache.DefaultCache.Value.RetrieveAllFields(_targetType))
{
var sourcePropertyValue = GetSourcePropertyValue(sourceProperties, field);
if (sourcePropertyValue == null) continue;
var targetPropertyValue = FromJsonResult(
sourcePropertyValue,
_jsonSerializerCase,
field.FieldType,
_includeNonPublic);
try
{
field.SetValue(_target, targetPropertyValue);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch
#pragma warning restore CA1031 // Do not catch general exception types
{
// ignored
}
}
}
}
}
}

View File

@@ -0,0 +1,348 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Swan.Formatters
{
/// <summary>
/// A very simple, light-weight JSON library written by Mario
/// to teach Geo how things are done
///
/// This is an useful helper for small tasks but it doesn't represent a full-featured
/// serializer such as the beloved Json.NET.
/// </summary>
public partial class Json
{
/// <summary>
/// A simple JSON Deserializer.
/// </summary>
private class Deserializer
{
#region State Variables
private readonly object? _result;
private readonly string _json;
private Dictionary<string, object?> _resultObject;
private List<object?> _resultArray;
private ReadState _state = ReadState.WaitingForRootOpen;
private string? _currentFieldName;
private int _index;
#endregion
private Deserializer(string json, int startIndex)
{
_json = json;
for (_index = startIndex; _index < _json.Length; _index++)
{
switch (_state)
{
case ReadState.WaitingForRootOpen:
WaitForRootOpen();
continue;
case ReadState.WaitingForField when char.IsWhiteSpace(_json, _index):
continue;
case ReadState.WaitingForField when (_resultObject != null && _json[_index] == CloseObjectChar)
|| (_resultArray != null && _json[_index] == CloseArrayChar):
// Handle empty arrays and empty objects
_result = _resultObject ?? _resultArray as object;
return;
case ReadState.WaitingForField when _json[_index] != StringQuotedChar:
throw CreateParserException($"'{StringQuotedChar}'");
case ReadState.WaitingForField:
{
var charCount = GetFieldNameCount();
_currentFieldName = Unescape(_json.SliceLength(_index + 1, charCount));
_index += charCount + 1;
_state = ReadState.WaitingForColon;
continue;
}
case ReadState.WaitingForColon when char.IsWhiteSpace(_json, _index):
continue;
case ReadState.WaitingForColon when _json[_index] != ValueSeparatorChar:
throw CreateParserException($"'{ValueSeparatorChar}'");
case ReadState.WaitingForColon:
_state = ReadState.WaitingForValue;
continue;
case ReadState.WaitingForValue when char.IsWhiteSpace(_json, _index):
continue;
case ReadState.WaitingForValue when (_resultObject != null && _json[_index] == CloseObjectChar)
|| (_resultArray != null && _json[_index] == CloseArrayChar):
// Handle empty arrays and empty objects
_result = _resultObject ?? _resultArray as object;
return;
case ReadState.WaitingForValue:
ExtractValue();
continue;
}
if (_state != ReadState.WaitingForNextOrRootClose || char.IsWhiteSpace(_json, _index)) continue;
if (_json[_index] == FieldSeparatorChar)
{
if (_resultObject != null)
{
_state = ReadState.WaitingForField;
_currentFieldName = null;
continue;
}
_state = ReadState.WaitingForValue;
continue;
}
if ((_resultObject == null || _json[_index] != CloseObjectChar) &&
(_resultArray == null || _json[_index] != CloseArrayChar))
{
throw CreateParserException($"'{FieldSeparatorChar}' '{CloseObjectChar}' or '{CloseArrayChar}'");
}
_result = _resultObject ?? _resultArray as object;
return;
}
}
internal static object? DeserializeInternal(string json) => new Deserializer(json, 0)._result;
private void WaitForRootOpen()
{
if (char.IsWhiteSpace(_json, _index)) return;
switch (_json[_index])
{
case OpenObjectChar:
_resultObject = new Dictionary<string, object?>();
_state = ReadState.WaitingForField;
return;
case OpenArrayChar:
_resultArray = new List<object?>();
_state = ReadState.WaitingForValue;
return;
default:
throw CreateParserException($"'{OpenObjectChar}' or '{OpenArrayChar}'");
}
}
private void ExtractValue()
{
// determine the value based on what it starts with
switch (_json[_index])
{
case StringQuotedChar: // expect a string
ExtractStringQuoted();
break;
case OpenObjectChar: // expect object
case OpenArrayChar: // expect array
ExtractObject();
break;
case 't': // expect true
ExtractConstant(TrueLiteral, true);
break;
case 'f': // expect false
ExtractConstant(FalseLiteral, false);
break;
case 'n': // expect null
ExtractConstant(NullLiteral);
break;
default: // expect number
ExtractNumber();
break;
}
_currentFieldName = null;
_state = ReadState.WaitingForNextOrRootClose;
}
private static string Unescape(string str)
{
// check if we need to unescape at all
if (str.IndexOf(StringEscapeChar) < 0)
return str;
var builder = new StringBuilder(str.Length);
for (var i = 0; i < str.Length; i++)
{
if (str[i] != StringEscapeChar)
{
builder.Append(str[i]);
continue;
}
if (i + 1 > str.Length - 1)
break;
// escape sequence begins here
switch (str[i + 1])
{
case 'u':
i = ExtractEscapeSequence(str, i, builder);
break;
case 'b':
builder.Append('\b');
i += 1;
break;
case 't':
builder.Append('\t');
i += 1;
break;
case 'n':
builder.Append('\n');
i += 1;
break;
case 'f':
builder.Append('\f');
i += 1;
break;
case 'r':
builder.Append('\r');
i += 1;
break;
default:
builder.Append(str[i + 1]);
i += 1;
break;
}
}
return builder.ToString();
}
private static int ExtractEscapeSequence(string str, int i, StringBuilder builder)
{
var startIndex = i + 2;
var endIndex = i + 5;
if (endIndex > str.Length - 1)
{
builder.Append(str[i + 1]);
i += 1;
return i;
}
var hexCode = str.Slice(startIndex, endIndex).ConvertHexadecimalToBytes();
builder.Append(Encoding.BigEndianUnicode.GetChars(hexCode));
i += 5;
return i;
}
private int GetFieldNameCount()
{
var charCount = 0;
for (var j = _index + 1; j < _json.Length; j++)
{
if (_json[j] == StringQuotedChar && _json[j - 1] != StringEscapeChar)
break;
charCount++;
}
return charCount;
}
private void ExtractObject()
{
// Extract and set the value
var deserializer = new Deserializer(_json, _index);
if (_currentFieldName != null)
_resultObject[_currentFieldName] = deserializer._result;
else
_resultArray.Add(deserializer._result);
_index = deserializer._index;
}
private void ExtractNumber()
{
var charCount = 0;
for (var j = _index; j < _json.Length; j++)
{
if (char.IsWhiteSpace(_json[j]) || _json[j] == FieldSeparatorChar
|| (_resultObject != null && _json[j] == CloseObjectChar)
|| (_resultArray != null && _json[j] == CloseArrayChar))
break;
charCount++;
}
// Extract and set the value
var stringValue = _json.SliceLength(_index, charCount);
if (decimal.TryParse(stringValue, System.Globalization.NumberStyles.Number, System.Globalization.CultureInfo.InvariantCulture, out var value) == false)
throw CreateParserException("[number]");
if (_currentFieldName != null)
_resultObject[_currentFieldName] = value;
else
_resultArray.Add(value);
_index += charCount - 1;
}
private void ExtractConstant(string boolValue, bool? value = null)
{
if (_json.SliceLength(_index, boolValue.Length) != boolValue)
throw CreateParserException($"'{ValueSeparatorChar}'");
// Extract and set the value
if (_currentFieldName != null)
_resultObject[_currentFieldName] = value;
else
_resultArray.Add(value);
_index += boolValue.Length - 1;
}
private void ExtractStringQuoted()
{
var charCount = 0;
var escapeCharFound = false;
for (var j = _index + 1; j < _json.Length; j++)
{
if (_json[j] == StringQuotedChar && !escapeCharFound)
break;
escapeCharFound = _json[j] == StringEscapeChar && !escapeCharFound;
charCount++;
}
// Extract and set the value
var value = Unescape(_json.SliceLength(_index + 1, charCount));
if (_currentFieldName != null)
_resultObject[_currentFieldName] = value;
else
_resultArray.Add(value);
_index += charCount + 1;
}
private FormatException CreateParserException(string expected)
{
var (line, col) = _json.TextPositionAt(_index);
return new FormatException(
$"Parser error (Line {line}, Col {col}, State {_state}): Expected {expected} but got '{_json[_index]}'.");
}
/// <summary>
/// Defines the different JSON read states.
/// </summary>
private enum ReadState
{
WaitingForRootOpen,
WaitingForField,
WaitingForColon,
WaitingForValue,
WaitingForNextOrRootClose,
}
}
}
}

View File

@@ -0,0 +1,370 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using Swan.Reflection;
namespace Swan.Formatters
{
/// <summary>
/// A very simple, light-weight JSON library written by Mario
/// to teach Geo how things are done
///
/// This is an useful helper for small tasks but it doesn't represent a full-featured
/// serializer such as the beloved Json.NET.
/// </summary>
public partial class Json
{
/// <summary>
/// A simple JSON serializer.
/// </summary>
private class Serializer
{
#region Private Declarations
private static readonly Dictionary<int, string> IndentStrings = new Dictionary<int, string>();
private readonly SerializerOptions _options;
private readonly string _result;
private readonly StringBuilder _builder;
private readonly string _lastCommaSearch;
private readonly string[]? _excludedNames = null;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="Serializer" /> class.
/// </summary>
/// <param name="obj">The object.</param>
/// <param name="depth">The depth.</param>
/// <param name="options">The options.</param>
private Serializer(object? obj, int depth, SerializerOptions options, string[]? excludedNames = null)
{
if (depth > 20)
{
throw new InvalidOperationException(
"The max depth (20) has been reached. Serializer can not continue.");
}
// Basic Type Handling (nulls, strings, number, date and bool)
_result = ResolveBasicType(obj);
if (!string.IsNullOrWhiteSpace(_result))
return;
_options = options;
_excludedNames ??= excludedNames;
_options.ExcludeProperties = GetExcludedNames(obj?.GetType(), _excludedNames);
// Handle circular references correctly and avoid them
if (options.IsObjectPresent(obj!))
{
_result = $"{{ \"$circref\": \"{Escape(obj!.GetHashCode().ToStringInvariant(), false)}\" }}";
return;
}
// At this point, we will need to construct the object with a StringBuilder.
_lastCommaSearch = FieldSeparatorChar + (_options.Format ? Environment.NewLine : string.Empty);
_builder = new StringBuilder();
_result = obj switch
{
IDictionary itemsZero when itemsZero.Count == 0 => EmptyObjectLiteral,
IDictionary items => ResolveDictionary(items, depth),
IEnumerable enumerableZero when !enumerableZero.Cast<object>().Any() => EmptyArrayLiteral,
IEnumerable enumerableBytes when enumerableBytes is byte[] bytes => Serialize(bytes.ToBase64(), depth, _options, _excludedNames),
IEnumerable enumerable => ResolveEnumerable(enumerable, depth),
_ => ResolveObject(obj!, depth)
};
}
internal static string Serialize(object? obj, int depth, SerializerOptions options, string[]? excludedNames = null) => new Serializer(obj, depth, options, excludedNames)._result;
#endregion
#region Helper Methods
internal static string[]? GetExcludedNames(Type? type, string[]? excludedNames)
{
if (type == null)
return excludedNames;
var excludedByAttr = IgnoredPropertiesCache.Retrieve(type, t => t.GetProperties()
.Where(x => AttributeCache.DefaultCache.Value.RetrieveOne<JsonPropertyAttribute>(x)?.Ignored == true)
.Select(x => x.Name));
if (excludedByAttr?.Any() != true)
return excludedNames;
return excludedNames?.Any(string.IsNullOrWhiteSpace) == true
? excludedByAttr.Intersect(excludedNames.Where(y => !string.IsNullOrWhiteSpace(y))).ToArray()
: excludedByAttr.ToArray();
}
private static string ResolveBasicType(object? obj)
{
switch (obj)
{
case null:
return NullLiteral;
case string s:
return Escape(s, true);
case bool b:
return b ? TrueLiteral : FalseLiteral;
case Type _:
case Assembly _:
case MethodInfo _:
case PropertyInfo _:
case EventInfo _:
return Escape(obj.ToString(), true);
case DateTime d:
return $"{StringQuotedChar}{d:s}{StringQuotedChar}";
default:
var targetType = obj.GetType();
if (!Definitions.BasicTypesInfo.Value.ContainsKey(targetType))
return string.Empty;
var escapedValue = Escape(Definitions.BasicTypesInfo.Value[targetType].ToStringInvariant(obj), false);
return decimal.TryParse(escapedValue, out _)
? $"{escapedValue}"
: $"{StringQuotedChar}{escapedValue}{StringQuotedChar}";
}
}
private static bool IsNonEmptyJsonArrayOrObject(string serialized)
{
if (serialized == EmptyObjectLiteral || serialized == EmptyArrayLiteral) return false;
// find the first position the character is not a space
return serialized.Where(c => c != ' ').Select(c => c == OpenObjectChar || c == OpenArrayChar).FirstOrDefault();
}
private static string Escape(string str, bool quoted)
{
if (str == null)
return string.Empty;
var builder = new StringBuilder(str.Length * 2);
if (quoted) builder.Append(StringQuotedChar);
Escape(str, builder);
if (quoted) builder.Append(StringQuotedChar);
return builder.ToString();
}
private static void Escape(string str, StringBuilder builder)
{
foreach (var currentChar in str)
{
switch (currentChar)
{
case '\\':
case '"':
case '/':
builder
.Append('\\')
.Append(currentChar);
break;
case '\b':
builder.Append("\\b");
break;
case '\t':
builder.Append("\\t");
break;
case '\n':
builder.Append("\\n");
break;
case '\f':
builder.Append("\\f");
break;
case '\r':
builder.Append("\\r");
break;
default:
if (currentChar < ' ')
{
var escapeBytes = BitConverter.GetBytes((ushort)currentChar);
if (BitConverter.IsLittleEndian == false)
Array.Reverse(escapeBytes);
builder.Append("\\u")
.Append(escapeBytes[1].ToString("X", CultureInfo.InvariantCulture).PadLeft(2, '0'))
.Append(escapeBytes[0].ToString("X", CultureInfo.InvariantCulture).PadLeft(2, '0'));
}
else
{
builder.Append(currentChar);
}
break;
}
}
}
private Dictionary<string, object?> CreateDictionary(
Dictionary<string, MemberInfo> fields,
string targetType,
object target)
{
// Create the dictionary and extract the properties
var objectDictionary = new Dictionary<string, object?>();
if (!string.IsNullOrWhiteSpace(_options.TypeSpecifier))
objectDictionary[_options.TypeSpecifier!] = targetType;
foreach (var field in fields)
{
// Build the dictionary using property names and values
// Note: used to be: property.GetValue(target); but we would be reading private properties
try
{
objectDictionary[field.Key] = field.Value is PropertyInfo property
? target.ReadProperty(property.Name)
: (field.Value as FieldInfo)?.GetValue(target);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch
#pragma warning restore CA1031 // Do not catch general exception types
{
/* ignored */
}
}
return objectDictionary;
}
private string ResolveDictionary(IDictionary items, int depth)
{
Append(OpenObjectChar, depth);
AppendLine();
// Iterate through the elements and output recursively
var writeCount = 0;
foreach (var key in items.Keys)
{
// Serialize and append the key (first char indented)
Append(StringQuotedChar, depth + 1);
Escape(key.ToString(), _builder);
_builder
.Append(StringQuotedChar)
.Append(ValueSeparatorChar)
.Append(" ");
// Serialize and append the value
var serializedValue = Serialize(items[key], depth + 1, _options, _excludedNames);
if (IsNonEmptyJsonArrayOrObject(serializedValue)) AppendLine();
Append(serializedValue, 0);
// Add a comma and start a new line -- We will remove the last one when we are done writing the elements
Append(FieldSeparatorChar, 0);
AppendLine();
writeCount++;
}
// Output the end of the object and set the result
RemoveLastComma();
Append(CloseObjectChar, writeCount > 0 ? depth : 0);
return _builder.ToString();
}
private string ResolveObject(object target, int depth)
{
var targetType = target.GetType();
if (targetType.IsEnum)
return Convert.ToInt64(target, CultureInfo.InvariantCulture).ToStringInvariant();
var fields = _options.GetProperties(targetType);
if (fields.Count == 0 && string.IsNullOrWhiteSpace(_options.TypeSpecifier))
return EmptyObjectLiteral;
// If we arrive here, then we convert the object into a
// dictionary of property names and values and call the serialization
// function again
var objectDictionary = CreateDictionary(fields, targetType.ToString(), target);
return Serialize(objectDictionary, depth, _options, _excludedNames);
}
private string ResolveEnumerable(IEnumerable target, int depth)
{
// Cast the items as a generic object array
var items = target.Cast<object>();
Append(OpenArrayChar, depth);
AppendLine();
// Iterate through the elements and output recursively
var writeCount = 0;
foreach (var entry in items)
{
var serializedValue = Serialize(entry, depth + 1, _options, _excludedNames);
if (IsNonEmptyJsonArrayOrObject(serializedValue))
Append(serializedValue, 0);
else
Append(serializedValue, depth + 1);
Append(FieldSeparatorChar, 0);
AppendLine();
writeCount++;
}
// Output the end of the array and set the result
RemoveLastComma();
Append(CloseArrayChar, writeCount > 0 ? depth : 0);
return _builder.ToString();
}
private void SetIndent(int depth)
{
if (_options.Format == false || depth <= 0) return;
_builder.Append(IndentStrings.GetOrAdd(depth, x => new string(' ', x * 4)));
}
/// <summary>
/// Removes the last comma in the current string builder.
/// </summary>
private void RemoveLastComma()
{
if (_builder.Length < _lastCommaSearch.Length)
return;
if (_lastCommaSearch.Where((t, i) => _builder[_builder.Length - _lastCommaSearch.Length + i] != t).Any())
{
return;
}
// If we got this far, we simply remove the comma character
_builder.Remove(_builder.Length - _lastCommaSearch.Length, 1);
}
private void Append(string text, int depth)
{
SetIndent(depth);
_builder.Append(text);
}
private void Append(char text, int depth)
{
SetIndent(depth);
_builder.Append(text);
}
private void AppendLine()
{
if (_options.Format == false) return;
_builder.Append(Environment.NewLine);
}
#endregion
}
}
}

View File

@@ -0,0 +1,152 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Swan.Reflection;
namespace Swan.Formatters
{
/// <summary>
/// A very simple, light-weight JSON library written by Mario
/// to teach Geo how things are done
///
/// This is an useful helper for small tasks but it doesn't represent a full-featured
/// serializer such as the beloved Json.NET.
/// </summary>
public class SerializerOptions
{
private static readonly ConcurrentDictionary<Type, Dictionary<Tuple<string, string>, MemberInfo>>
TypeCache = new ConcurrentDictionary<Type, Dictionary<Tuple<string, string>, MemberInfo>>();
private readonly string[]? _includeProperties;
private readonly Dictionary<int, List<WeakReference>> _parentReferences = new Dictionary<int, List<WeakReference>>();
/// <summary>
/// Initializes a new instance of the <see cref="SerializerOptions"/> class.
/// </summary>
/// <param name="format">if set to <c>true</c> [format].</param>
/// <param name="typeSpecifier">The type specifier.</param>
/// <param name="includeProperties">The include properties.</param>
/// <param name="excludeProperties">The exclude properties.</param>
/// <param name="includeNonPublic">if set to <c>true</c> [include non public].</param>
/// <param name="parentReferences">The parent references.</param>
/// <param name="jsonSerializerCase">The json serializer case.</param>
public SerializerOptions(
bool format,
string? typeSpecifier,
string[]? includeProperties,
string[]? excludeProperties = null,
bool includeNonPublic = true,
IReadOnlyCollection<WeakReference>? parentReferences = null,
JsonSerializerCase jsonSerializerCase = JsonSerializerCase.None)
{
_includeProperties = includeProperties;
ExcludeProperties = excludeProperties;
IncludeNonPublic = includeNonPublic;
Format = format;
TypeSpecifier = typeSpecifier;
JsonSerializerCase = jsonSerializerCase;
if (parentReferences == null)
return;
foreach (var parentReference in parentReferences.Where(x => x.IsAlive))
{
IsObjectPresent(parentReference.Target);
}
}
/// <summary>
/// Gets a value indicating whether this <see cref="SerializerOptions"/> is format.
/// </summary>
/// <value>
/// <c>true</c> if format; otherwise, <c>false</c>.
/// </value>
public bool Format { get; }
/// <summary>
/// Gets the type specifier.
/// </summary>
/// <value>
/// The type specifier.
/// </value>
public string? TypeSpecifier { get; }
/// <summary>
/// Gets a value indicating whether [include non public].
/// </summary>
/// <value>
/// <c>true</c> if [include non public]; otherwise, <c>false</c>.
/// </value>
public bool IncludeNonPublic { get; }
/// <summary>
/// Gets the json serializer case.
/// </summary>
/// <value>
/// The json serializer case.
/// </value>
public JsonSerializerCase JsonSerializerCase { get; }
/// <summary>
/// Gets or sets the exclude properties.
/// </summary>
/// <value>
/// The exclude properties.
/// </value>
public string[]? ExcludeProperties { get; set; }
internal bool IsObjectPresent(object target)
{
var hashCode = target.GetHashCode();
if (_parentReferences.ContainsKey(hashCode))
{
if (_parentReferences[hashCode].Any(p => ReferenceEquals(p.Target, target)))
return true;
_parentReferences[hashCode].Add(new WeakReference(target));
return false;
}
_parentReferences.Add(hashCode, new List<WeakReference> { new WeakReference(target) });
return false;
}
internal Dictionary<string, MemberInfo> GetProperties(Type targetType)
=> GetPropertiesCache(targetType)
.When(() => _includeProperties?.Length > 0,
query => query.Where(p => _includeProperties.Contains(p.Key.Item1)))
.When(() => ExcludeProperties?.Length > 0,
query => query.Where(p => !ExcludeProperties.Contains(p.Key.Item1)))
.ToDictionary(x => x.Key.Item2, x => x.Value);
private Dictionary<Tuple<string, string>, MemberInfo> GetPropertiesCache(Type targetType)
{
if (TypeCache.TryGetValue(targetType, out var current))
return current;
var fields =
new List<MemberInfo>(PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(targetType).Where(p => p.CanRead));
// If the target is a struct (value type) navigate the fields.
if (targetType.IsValueType)
{
fields.AddRange(FieldTypeCache.DefaultCache.Value.RetrieveAllFields(targetType));
}
var value = fields
.Where(x => x.GetCustomAttribute<JsonPropertyAttribute>()?.Ignored != true)
.ToDictionary(
x => Tuple.Create(x.Name,
x.GetCustomAttribute<JsonPropertyAttribute>()?.PropertyName ?? x.Name.GetNameWithCase(JsonSerializerCase)),
x => x);
TypeCache.TryAdd(targetType, value);
return value;
}
}
}

View File

@@ -0,0 +1,373 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Swan.Collections;
namespace Swan.Formatters
{
/// <summary>
/// A very simple, light-weight JSON library written by Mario
/// to teach Geo how things are done
///
/// This is an useful helper for small tasks but it doesn't represent a full-featured
/// serializer such as the beloved Json.NET.
/// </summary>
public static partial class Json
{
#region Constants
internal const string AddMethodName = "Add";
private const char OpenObjectChar = '{';
private const char CloseObjectChar = '}';
private const char OpenArrayChar = '[';
private const char CloseArrayChar = ']';
private const char FieldSeparatorChar = ',';
private const char ValueSeparatorChar = ':';
private const char StringEscapeChar = '\\';
private const char StringQuotedChar = '"';
private const string EmptyObjectLiteral = "{ }";
private const string EmptyArrayLiteral = "[ ]";
private const string TrueLiteral = "true";
private const string FalseLiteral = "false";
private const string NullLiteral = "null";
#endregion
private static readonly CollectionCacheRepository<string> IgnoredPropertiesCache = new CollectionCacheRepository<string>();
#region Public API
/// <summary>
/// Serializes the specified object into a JSON string.
/// </summary>
/// <param name="obj">The object.</param>
/// <param name="format">if set to <c>true</c> it formats and indents the output.</param>
/// <param name="typeSpecifier">The type specifier. Leave null or empty to avoid setting.</param>
/// <param name="includeNonPublic">if set to <c>true</c> non-public getters will be also read.</param>
/// <param name="includedNames">The included property names.</param>
/// <param name="excludedNames">The excluded property names.</param>
/// <returns>
/// A <see cref="System.String" /> that represents the current object.
/// </returns>
/// <example>
/// The following example describes how to serialize a simple object.
/// <code>
/// using Swan.Formatters;
///
/// class Example
/// {
/// static void Main()
/// {
/// var obj = new { One = "One", Two = "Two" };
///
/// var serial = Json.Serialize(obj); // {"One": "One","Two": "Two"}
/// }
/// }
/// </code>
///
/// The following example details how to serialize an object using the <see cref="JsonPropertyAttribute"/>.
///
/// <code>
/// using Swan.Attributes;
/// using Swan.Formatters;
///
/// class Example
/// {
/// class JsonPropertyExample
/// {
/// [JsonProperty("data")]
/// public string Data { get; set; }
///
/// [JsonProperty("ignoredData", true)]
/// public string IgnoredData { get; set; }
/// }
///
/// static void Main()
/// {
/// var obj = new JsonPropertyExample() { Data = "OK", IgnoredData = "OK" };
///
/// // {"data": "OK"}
/// var serializedObj = Json.Serialize(obj);
/// }
/// }
/// </code>
/// </example>
public static string Serialize(
object? obj,
bool format = false,
string? typeSpecifier = null,
bool includeNonPublic = false,
string[]? includedNames = null,
params string[] excludedNames) =>
Serialize(obj, format, typeSpecifier, includeNonPublic, includedNames, excludedNames, null, JsonSerializerCase.None);
/// <summary>
/// Serializes the specified object into a JSON string.
/// </summary>
/// <param name="obj">The object.</param>
/// <param name="jsonSerializerCase">The json serializer case.</param>
/// <param name="format">if set to <c>true</c> [format].</param>
/// <param name="typeSpecifier">The type specifier.</param>
/// <returns>
/// A <see cref="System.String" /> that represents the current object.
/// </returns>
public static string Serialize(
object? obj,
JsonSerializerCase jsonSerializerCase,
bool format = false,
string? typeSpecifier = null) => Serialize(obj, format, typeSpecifier, false, null, null, null, jsonSerializerCase);
/// <summary>
/// Serializes the specified object into a JSON string.
/// </summary>
/// <param name="obj">The object.</param>
/// <param name="format">if set to <c>true</c> it formats and indents the output.</param>
/// <param name="typeSpecifier">The type specifier. Leave null or empty to avoid setting.</param>
/// <param name="includeNonPublic">if set to <c>true</c> non-public getters will be also read.</param>
/// <param name="includedNames">The included property names.</param>
/// <param name="excludedNames">The excluded property names.</param>
/// <param name="parentReferences">The parent references.</param>
/// <param name="jsonSerializerCase">The json serializer case.</param>
/// <returns>
/// A <see cref="System.String" /> that represents the current object.
/// </returns>
public static string Serialize(
object? obj,
bool format,
string? typeSpecifier,
bool includeNonPublic,
string[]? includedNames,
string[]? excludedNames,
List<WeakReference>? parentReferences,
JsonSerializerCase jsonSerializerCase)
{
if (obj != null && (obj is string || Definitions.AllBasicValueTypes.Contains(obj.GetType())))
{
return SerializePrimitiveValue(obj);
}
var options = new SerializerOptions(
format,
typeSpecifier,
includedNames,
Serializer.GetExcludedNames(obj?.GetType(), excludedNames),
includeNonPublic,
parentReferences,
jsonSerializerCase);
return Serializer.Serialize(obj, 0, options, excludedNames);
}
/// <summary>
/// Serializes the specified object using the SerializerOptions provided.
/// </summary>
/// <param name="obj">The object.</param>
/// <param name="options">The options.</param>
/// <returns>
/// A <see cref="string" /> that represents the current object.
/// </returns>
public static string Serialize(object? obj, SerializerOptions options) => Serializer.Serialize(obj, 0, options);
/// <summary>
/// Serializes the specified object only including the specified property names.
/// </summary>
/// <param name="obj">The object.</param>
/// <param name="format">if set to <c>true</c> it formats and indents the output.</param>
/// <param name="includeNames">The include names.</param>
/// <returns>A <see cref="string" /> that represents the current object.</returns>
/// <example>
/// The following example shows how to serialize a simple object including the specified properties.
/// <code>
/// using Swan.Formatters;
///
/// class Example
/// {
/// static void Main()
/// {
/// // object to serialize
/// var obj = new { One = "One", Two = "Two", Three = "Three" };
///
/// // the included names
/// var includedNames = new[] { "Two", "Three" };
///
/// // serialize only the included names
/// var data = Json.SerializeOnly(basicObject, true, includedNames);
/// // {"Two": "Two","Three": "Three" }
/// }
/// }
/// </code>
/// </example>
public static string SerializeOnly(object? obj, bool format, params string[] includeNames)
=> Serialize(obj, new SerializerOptions(format, null, includeNames));
/// <summary>
/// Serializes the specified object excluding the specified property names.
/// </summary>
/// <param name="obj">The object.</param>
/// <param name="format">if set to <c>true</c> it formats and indents the output.</param>
/// <param name="excludeNames">The exclude names.</param>
/// <returns>A <see cref="string" /> that represents the current object.</returns>
/// <example>
/// The following code shows how to serialize a simple object excluding the specified properties.
/// <code>
/// using Swan.Formatters;
///
/// class Example
/// {
/// static void Main()
/// {
/// // object to serialize
/// var obj = new { One = "One", Two = "Two", Three = "Three" };
///
/// // the excluded names
/// var excludeNames = new[] { "Two", "Three" };
///
/// // serialize excluding
/// var data = Json.SerializeExcluding(basicObject, false, includedNames);
/// // {"One": "One"}
/// }
/// }
/// </code>
/// </example>
public static string SerializeExcluding(object? obj, bool format, params string[] excludeNames)
=> Serializer.Serialize(obj, 0, new SerializerOptions(format, null, null), excludeNames);
/// <summary>
/// Deserializes the specified json string as either a Dictionary[string, object] or as a List[object]
/// depending on the syntax of the JSON string.
/// </summary>
/// <param name="json">The JSON string.</param>
/// <param name="jsonSerializerCase">The json serializer case.</param>
/// <returns>
/// Type of the current deserializes.
/// </returns>
/// <example>
/// The following code shows how to deserialize a JSON string into a Dictionary.
/// <code>
/// using Swan.Formatters;
/// class Example
/// {
/// static void Main()
/// {
/// // json to deserialize
/// var basicJson = "{\"One\":\"One\",\"Two\":\"Two\",\"Three\":\"Three\"}";
/// // deserializes the specified json into a Dictionary&lt;string, object&gt;.
/// var data = Json.Deserialize(basicJson, JsonSerializerCase.None);
/// }
/// }
/// </code></example>
public static object? Deserialize(string json, JsonSerializerCase jsonSerializerCase) =>
json == null
? throw new ArgumentNullException(nameof(json))
: Converter.FromJsonResult(Deserializer.DeserializeInternal(json), jsonSerializerCase);
/// <summary>
/// Deserializes the specified json string as either a Dictionary[string, object] or as a List[object]
/// depending on the syntax of the JSON string.
/// </summary>
/// <param name="json">The JSON string.</param>
/// <returns>
/// Type of the current deserializes.
/// </returns>
/// <example>
/// The following code shows how to deserialize a JSON string into a Dictionary.
/// <code>
/// using Swan.Formatters;
/// class Example
/// {
/// static void Main()
/// {
/// // json to deserialize
/// var basicJson = "{\"One\":\"One\",\"Two\":\"Two\",\"Three\":\"Three\"}";
/// // deserializes the specified json into a Dictionary&lt;string, object&gt;.
/// var data = Json.Deserialize(basicJson);
/// }
/// }
/// </code></example>
public static object? Deserialize(string json) =>
json == null
? throw new ArgumentNullException(nameof(json))
: Deserialize(json, JsonSerializerCase.None);
/// <summary>
/// Deserializes the specified JSON string and converts it to the specified object type.
/// Non-public constructors and property setters are ignored.
/// </summary>
/// <typeparam name="T">The type of object to deserialize.</typeparam>
/// <param name="json">The JSON string.</param>
/// <param name="jsonSerializerCase">The JSON serializer case.</param>
/// <returns>
/// The deserialized specified type object.
/// </returns>
/// <example>
/// The following code describes how to deserialize a JSON string into an object of type T.
/// <code>
/// using Swan.Formatters;
/// class Example
/// {
/// static void Main()
/// {
/// // json type BasicJson to serialize
/// var basicJson = "{\"One\":\"One\",\"Two\":\"Two\",\"Three\":\"Three\"}";
/// // deserializes the specified string in a new instance of the type BasicJson.
/// var data = Json.Deserialize&lt;BasicJson&gt;(basicJson);
/// }
/// }
/// </code></example>
public static T Deserialize<T>(string json, JsonSerializerCase jsonSerializerCase = JsonSerializerCase.None) =>
json == null
? throw new ArgumentNullException(nameof(json))
: (T)Deserialize(json, typeof(T), jsonSerializerCase: jsonSerializerCase);
/// <summary>
/// Deserializes the specified JSON string and converts it to the specified object type.
/// </summary>
/// <typeparam name="T">The type of object to deserialize.</typeparam>
/// <param name="json">The JSON string.</param>
/// <param name="includeNonPublic">if set to true, it also uses the non-public constructors and property setters.</param>
/// <returns>
/// The deserialized specified type object.
/// </returns>
public static T Deserialize<T>(string json, bool includeNonPublic) =>
json == null
? throw new ArgumentNullException(nameof(json))
: (T)Deserialize(json, typeof(T), includeNonPublic);
/// <summary>
/// Deserializes the specified JSON string and converts it to the specified object type.
/// </summary>
/// <param name="json">The JSON string.</param>
/// <param name="resultType">Type of the result.</param>
/// <param name="includeNonPublic">if set to true, it also uses the non-public constructors and property setters.</param>
/// <param name="jsonSerializerCase">The json serializer case.</param>
/// <returns>
/// Type of the current conversion from json result.
/// </returns>
public static object? Deserialize(string json, Type resultType, bool includeNonPublic = false, JsonSerializerCase jsonSerializerCase = JsonSerializerCase.None) =>
json == null
? throw new ArgumentNullException(nameof(json))
: Converter.FromJsonResult(
Deserializer.DeserializeInternal(json),
jsonSerializerCase,
resultType,
includeNonPublic);
#endregion
#region Private API
private static string SerializePrimitiveValue(object obj) =>
obj switch
{
string stringValue => $"\"{stringValue}\"",
bool boolValue => boolValue ? TrueLiteral : FalseLiteral,
_ => obj.ToString()
};
#endregion
}
}

View File

@@ -0,0 +1,39 @@
using System;
namespace Swan.Formatters
{
/// <summary>
/// An attribute used to help setup a property behavior when serialize/deserialize JSON.
/// </summary>
/// <seealso cref="Attribute" />
[AttributeUsage(AttributeTargets.Property)]
public sealed class JsonPropertyAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="JsonPropertyAttribute" /> class.
/// </summary>
/// <param name="propertyName">Name of the property.</param>
/// <param name="ignored">if set to <c>true</c> [ignored].</param>
public JsonPropertyAttribute(string propertyName, bool ignored = false)
{
PropertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName));
Ignored = ignored;
}
/// <summary>
/// Gets or sets the name of the property.
/// </summary>
/// <value>
/// The name of the property.
/// </value>
public string PropertyName { get; }
/// <summary>
/// Gets or sets a value indicating whether this <see cref="JsonPropertyAttribute" /> is ignored.
/// </summary>
/// <value>
/// <c>true</c> if ignored; otherwise, <c>false</c>.
/// </value>
public bool Ignored { get; }
}
}

View File

@@ -0,0 +1,23 @@
namespace Swan.Formatters
{
/// <summary>
/// Enumerates the JSON serializer cases to use: None (keeps the same case), PascalCase, or camelCase.
/// </summary>
public enum JsonSerializerCase
{
/// <summary>
/// The none
/// </summary>
None,
/// <summary>
/// The pascal case (eg. PascalCase)
/// </summary>
PascalCase,
/// <summary>
/// The camel case (eg. camelCase)
/// </summary>
CamelCase,
}
}