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

[WIP/QRCoder2] Fluent API experimentation PR #558

Open
wants to merge 2 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
7 changes: 7 additions & 0 deletions QRCoder/Builders/Payloads/IConfigurableEccLevel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace QRCoder.Builders.Payloads
{
public interface IConfigurableEccLevel
{
QRCodeGenerator.ECCLevel EccLevel { get; set; }
}
}
7 changes: 7 additions & 0 deletions QRCoder/Builders/Payloads/IConfigurableEciMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace QRCoder.Builders.Payloads
{
public interface IConfigurableEciMode
{
QRCodeGenerator.EciMode EciMode { get; set; }
}
}
7 changes: 7 additions & 0 deletions QRCoder/Builders/Payloads/IConfigurableVersion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace QRCoder.Builders.Payloads
{
public interface IConfigurableVersion
{
int Version { get; set; }
}
}
7 changes: 7 additions & 0 deletions QRCoder/Builders/Payloads/IPayload.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace QRCoder.Builders.Payloads
{
public interface IPayload
{
QRCodeData ToMatrix();
}
}
41 changes: 41 additions & 0 deletions QRCoder/Builders/Payloads/Implementations/EmailPayload.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;

namespace QRCoder.Builders.Payloads.Implementations
{
public class EmailPayload : PayloadBase
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sample where the ctor only has the email address, and the subject/body are provided by extension methods. Realistically, I'm thinking that the required or common parameters always appear in the constructor (or as an overload), but all of the optional/rare options (like MailEncoding) are extension methods.

{
public EmailPayload(string address)
{
_address = address;
}

private string _address { get; set; }
private string _subject { get; set; }
private string _body { get; set; }
private PayloadGenerator.Mail.MailEncoding _encoding { get; set; } = PayloadGenerator.Mail.MailEncoding.MAILTO;

public EmailPayload WithSubject(string subject)
{
_subject = subject;
return this;
}

public EmailPayload WithBody(string body)
{
_body = body;
return this;
}

public EmailPayload WithEncoding(PayloadGenerator.Mail.MailEncoding encoding)
{
if (encoding != PayloadGenerator.Mail.MailEncoding.MAILTO && encoding != PayloadGenerator.Mail.MailEncoding.SMTP && encoding != PayloadGenerator.Mail.MailEncoding.MATMSG)
{
throw new ArgumentOutOfRangeException(nameof(encoding));
}
_encoding = encoding;
return this;
}

protected override string Value => new PayloadGenerator.Mail(_address, _subject, _body, _encoding).ToString();
}
}
14 changes: 14 additions & 0 deletions QRCoder/Builders/Payloads/Implementations/StringPayload.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace QRCoder.Builders.Payloads.Implementations
{
public class StringPayload : PayloadBase
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can use this class for any payload that immediately serializes to a string value. At least if we design it like CreateUrl(...). Of course it would be nearly as easy to have additional classes for each payload type, similar to how it is now, if we want new QRCodeBuilder.Url(...) or whatnot.

{
private string _data;

public StringPayload(string data)
{
_data = data;
}

protected override string Value => _data;
}
}
46 changes: 46 additions & 0 deletions QRCoder/Builders/Payloads/Implementations/WiFiPayload.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
namespace QRCoder.Builders.Payloads.Implementations
{
public class WiFiPayload : PayloadBase
{
private string _ssid { get; set; }
private string _password { get; set; }
private PayloadGenerator.WiFi.Authentication _authentication { get; set; }
private bool _isHiddenSSID { get; set; }
private bool _isHexStrings { get; set; }

public WiFiPayload(string ssid)
{
_ssid = ssid;
_password = "";
_authentication = PayloadGenerator.WiFi.Authentication.nopass;
}

public WiFiPayload(string ssid, string password, PayloadGenerator.WiFi.Authentication authentication)
{
_ssid = ssid;
_password = password;
_authentication = authentication;
}

public WiFiPayload WithHiddenSSID()
{
_isHiddenSSID = true;
return this;
}

public WiFiPayload WithHexStrings()
{
_isHexStrings = true;
return this;
}

protected override string Value
{
get
{
var wifi = new PayloadGenerator.WiFi(_ssid, _password, _authentication, _isHiddenSSID, _isHexStrings);
return wifi.ToString();
}
}
}
}
21 changes: 21 additions & 0 deletions QRCoder/Builders/Payloads/PayloadBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace QRCoder.Builders.Payloads
{
public abstract class PayloadBase : IPayload, IConfigurableEccLevel, IConfigurableEciMode, IConfigurableVersion
Copy link
Contributor Author

@Shane32 Shane32 Jun 1, 2024

Choose a reason for hiding this comment

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

The idea here is that while most all payloads can inherit from PayloadBase and inherit some configurable settings, perhaps some payloads, like the swiss banking payload, does not support configuring the Eci mode or whatever. Or, we can override some of these properties to add validation, for instance ensuring that a certain minimum ECC level is maintained. There's a number of ways to actually implement this, all with similar end results.

{
protected virtual QRCodeGenerator.ECCLevel EccLevel { get; set; } = QRCodeGenerator.ECCLevel.Default;
QRCodeGenerator.ECCLevel IConfigurableEccLevel.EccLevel { get => EccLevel; set => EccLevel = value; }
Copy link
Contributor Author

@Shane32 Shane32 Jun 1, 2024

Choose a reason for hiding this comment

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

These interfaces are always explicitly implemented so that they do not appear on the intellisense list of accessible members for the class. Rather, the extension method will appear, which uses generics to return the same type.

If that's confusing, for example, this code would break the fluent syntax:

public PayloadBase WithEciMode(EciMode value)
{
    _eciMode = value;
}

Because it returns the base class type (PayloadBase), rather than the type of the derived type (e.g. WiFiPayload). Extension methods allow the intended behavior, while eliminating duplicate code across each payload class.


protected virtual QRCodeGenerator.EciMode EciMode { get; set; } = QRCodeGenerator.EciMode.Default;
QRCodeGenerator.EciMode IConfigurableEciMode.EciMode { get => EciMode; set => EciMode = value; }

protected virtual int Version { get; set; } = -1;
int IConfigurableVersion.Version { get => Version; set => Version = value; }

protected abstract string Value { get; }

public virtual QRCodeData ToMatrix()
{
return QRCodeGenerator.GenerateQrCode(Value, EccLevel, false, false, EciMode, Version);
}
}
}
47 changes: 47 additions & 0 deletions QRCoder/Builders/Payloads/PayloadExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using QRCoder.Builders.Payloads;
using QRCoder.Builders.Renderers;
using QRCoder.Builders.Renderers.Implementations;

namespace QRCoder
{
public static class PayloadExtensions
{
public static T WithErrorCorrection<T>(this T payload, QRCodeGenerator.ECCLevel eccLevel)
where T : IConfigurableEccLevel
{
payload.EccLevel = eccLevel;
return payload;
}

public static T WithEciMode<T>(this T payload, QRCodeGenerator.EciMode eciMode)
where T : IConfigurableEciMode
{
payload.EciMode = eciMode;
return payload;
}

public static T WithVersion<T>(this T payload, int version)
where T : IConfigurableVersion
{
payload.Version = version;
return payload;
}

public static T RenderWith<T>(this IPayload payload)
where T : IRenderer, new()
{
var renderer = new T();
renderer.Payload = payload;
return renderer;
}
Comment on lines +30 to +36
Copy link
Contributor Author

@Shane32 Shane32 Jun 1, 2024

Choose a reason for hiding this comment

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

This functions, but I think explicit extension methods such as RenderWithAsciiRenderer() or RenderAsPng() would be better than generics here. I specifically don't like that intellisense does not show the compatible renderers for T. Sorta breaks the goal.

image


public static T RenderWith<T>(this IPayload payload, int pixelsPerModule)
where T : IRenderer, IConfigurablePixelsPerModule, new()
{
var renderer = new T();
renderer.Payload = payload;
renderer.PixelsPerModule = pixelsPerModule;
return renderer;
}
}
}
7 changes: 7 additions & 0 deletions QRCoder/Builders/Renderers/IConfigurablePixelsPerModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace QRCoder.Builders.Renderers
{
public interface IConfigurablePixelsPerModule
{
int PixelsPerModule { get; set; }
}
}
7 changes: 7 additions & 0 deletions QRCoder/Builders/Renderers/IConfigurableQuietZones.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace QRCoder.Builders.Renderers
{
public interface IConfigurableQuietZones
{
bool QuietZone { get; set; }
}
}
9 changes: 9 additions & 0 deletions QRCoder/Builders/Renderers/IRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using QRCoder.Builders.Payloads;

namespace QRCoder.Builders.Renderers
{
public interface IRenderer
{
IPayload Payload { set; }
}
}
9 changes: 9 additions & 0 deletions QRCoder/Builders/Renderers/IStreamRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.IO;

namespace QRCoder.Builders.Renderers
{
public interface IStreamRenderer
{
MemoryStream ToStream();
}
Comment on lines +5 to +8
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Concept is that all byte-array-type renderers (or ones that can serialize to a byte array) implement this interface. If they use a MemoryStream under the hood, it's a direct pass-through. If they generate a byte array directly, then wrapping it in a MemoryStream is quite inexpensive, as it does not duplicate the entire byte array.

}
9 changes: 9 additions & 0 deletions QRCoder/Builders/Renderers/ITextRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.IO;

namespace QRCoder.Builders.Renderers
{
public interface ITextRenderer
{
string ToString();
}
Comment on lines +5 to +8
Copy link
Contributor Author

@Shane32 Shane32 Jun 1, 2024

Choose a reason for hiding this comment

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

Concept is that all string-based renderers (Ascii, Svg) implement this interface. Another option would be to have it implement ToStringBuilder instead, more similar to IStreamRenderer above. However, we can't create an extension method called ToString because object.ToString() would always take precedence over an extension method.

}
46 changes: 46 additions & 0 deletions QRCoder/Builders/Renderers/Implementations/AsciiRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
namespace QRCoder.Builders.Renderers.Implementations
{
public class AsciiRenderer : RendererBase, ITextRenderer
{
private string _darkString = "██";
private string _lightString = " ";
private int _repeatPerModule = 1;
private string _endOfLine = System.Environment.NewLine;
private bool _inverseDarkLight = false;

public AsciiRenderer WithText(string darkString, string lightString)
{
_darkString = darkString;
_lightString = lightString;
return this;
}

public AsciiRenderer WithRepeatPerModule(int repeatPerModule)
{
_repeatPerModule = repeatPerModule;
return this;
}

public AsciiRenderer WithEndOfLine(string endOfLine)
{
_endOfLine = endOfLine;
return this;
}

public AsciiRenderer WithInverseDarkLight()
{
_inverseDarkLight = true;
return this;
}

public override string ToString()
{
return new AsciiQRCode(QrCodeData).GetGraphic(
_repeatPerModule,
_inverseDarkLight ? _lightString : _darkString,
_inverseDarkLight ? _darkString : _lightString,
QuietZone,
_endOfLine);
}
}
}
43 changes: 43 additions & 0 deletions QRCoder/Builders/Renderers/Implementations/PngRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.IO;
using QRCoder.Builders.Payloads;

namespace QRCoder.Builders.Renderers.Implementations
{
public class PngRenderer : RendererBase, IConfigurablePixelsPerModule, IStreamRenderer
{
private int _pixelsPerModule = 10;
private byte[] _darkColor;
private byte[] _lightColor;

int IConfigurablePixelsPerModule.PixelsPerModule { get => _pixelsPerModule; set => _pixelsPerModule = value; }

#if !NETSTANDARD1_3
public PngRenderer WithColors(System.Drawing.Color darkColor, System.Drawing.Color lightColor)
{
_darkColor = new byte[] { darkColor.R, darkColor.G, darkColor.B, darkColor.A };
_lightColor = new byte[] { lightColor.R, lightColor.G, lightColor.B, lightColor.A };
return this;
}
#endif

public PngRenderer WithColors(byte[] darkColor, byte[] lightColor)
{
_darkColor = darkColor;
_lightColor = lightColor;
return this;
}

public byte[] ToArray()
{
if (_darkColor == null && _lightColor == null)
return new PngByteQRCode(QrCodeData).GetGraphic(_pixelsPerModule, QuietZone);
return new PngByteQRCode(QrCodeData).GetGraphic(_pixelsPerModule, _darkColor, _lightColor, QuietZone);
}

public MemoryStream ToStream()
{
var arr = ToArray();
return new MemoryStream(arr, 0, arr.Length, false, true);
}
}
}
42 changes: 42 additions & 0 deletions QRCoder/Builders/Renderers/Implementations/SvgRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#if !NETSTANDARD1_3
using System.Drawing;

namespace QRCoder.Builders.Renderers.Implementations
{
public class SvgRenderer : RendererBase, IConfigurablePixelsPerModule, ITextRenderer
{
private int _pixelsPerModule = 10;
private Color _darkColor;
private Color _lightColor;
private SvgQRCode.SvgLogo _logo;
private SvgQRCode.SizingMode _sizingMode = SvgQRCode.SizingMode.WidthHeightAttribute;

int IConfigurablePixelsPerModule.PixelsPerModule { get => _pixelsPerModule; set => _pixelsPerModule = value; }

public SvgRenderer WithColors(Color darkColor, Color lightColor)
{
_darkColor = darkColor;
_lightColor = lightColor;
return this;
}

public SvgRenderer WithLogo(SvgQRCode.SvgLogo logo)
{
_logo = logo;
return this;
}

public SvgRenderer WithSizingMode(SvgQRCode.SizingMode sizingMode)
{
_sizingMode = sizingMode;
return this;
}

public override string ToString()
{
return new SvgQRCode(QrCodeData).GetGraphic(
_pixelsPerModule, _darkColor, _lightColor, QuietZone, _sizingMode, _logo);
}
}
}
#endif
Loading
Loading