From 8f9101b9e75753df9f0c228ce12beea271735c3f Mon Sep 17 00:00:00 2001 From: Nick Lucas Date: Sat, 30 Jul 2016 20:32:06 +0100 Subject: [PATCH] Make Bean check columns in Getters (#25) * Add ValidateGetColumns flag on Bean along with logic to throw if requesting a column not in the Bean * Break Bean creation out into BeanFactory, add BeanConfiguration layer for factory. Interfaces updated all round * Provision BeanFactory in tests for dispensing beans * Totally handle Bean configuration within Factory * Fix documentation and move BeanConfiguration to more appropriate part of Api * Add BeanFactoryTests class to check the effects of optional flags * Minor fix for website text * Rename BeanConfiguration getter to BeanOptions getter * Add BeanOptions to website body * Rename BeanConfiguration and BeanOptions and update all references * Fix broken test for BeanFactory * Slight refactor on Bean * Prevent Bean.Get from validating get columns twice closes #25 --- .gitignore | 3 +- LimeBean.Tests/BeanCrudTests.cs | 28 ++++++--- LimeBean.Tests/BeanFactoryTests.cs | 61 +++++++++++++++++++ LimeBean.Tests/DatabaseBeanFinderTests.cs | 3 +- LimeBean.Tests/IntegrationTests.cs | 3 +- LimeBean.Tests/LimeBean.Tests.csproj | 1 + LimeBean.Website/Body.cs | 19 +++++- LimeBean/Bean.cs | 21 ++++++- LimeBean/BeanApi.cs | 18 +++++- LimeBean/BeanCrud.cs | 12 ++-- LimeBean/BeanFactory.cs | 40 ++++++++++++ LimeBean/BeanOptions.cs | 21 +++++++ .../Exceptions/ColumnNotFoundException.cs | 20 ++++++ LimeBean/Interfaces/IBeanCrud.cs | 5 +- LimeBean/Interfaces/IBeanDispenser.cs | 13 ++++ LimeBean/Interfaces/IBeanFactory.cs | 12 ++++ LimeBean/Interfaces/IBeanOptions.cs | 12 ++++ LimeBean/LimeBean.csproj | 6 ++ 18 files changed, 275 insertions(+), 23 deletions(-) create mode 100644 LimeBean.Tests/BeanFactoryTests.cs create mode 100644 LimeBean/BeanFactory.cs create mode 100644 LimeBean/BeanOptions.cs create mode 100644 LimeBean/Exceptions/ColumnNotFoundException.cs create mode 100644 LimeBean/Interfaces/IBeanDispenser.cs create mode 100644 LimeBean/Interfaces/IBeanFactory.cs create mode 100644 LimeBean/Interfaces/IBeanOptions.cs diff --git a/.gitignore b/.gitignore index 6f0198c..ae74e0b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ packages/ project.lock.json LimeBean.NuGet/*.nupkg LimeBean.NuGet/nuget.exe -index.html \ No newline at end of file +index.html +*.stackdump diff --git a/LimeBean.Tests/BeanCrudTests.cs b/LimeBean.Tests/BeanCrudTests.cs index 424e480..138b441 100644 --- a/LimeBean.Tests/BeanCrudTests.cs +++ b/LimeBean.Tests/BeanCrudTests.cs @@ -4,13 +4,16 @@ using System.Text; using Xunit; +using LimeBean.Interfaces; + namespace LimeBean.Tests { public class BeanCrudTests { [Fact] public void Dispense_Default() { - var crud = new BeanCrud(null, null, null); + IBeanFactory factory = new BeanFactory(); + var crud = new BeanCrud(null, null, null, factory); var bean = crud.Dispense("test"); Assert.Equal("test", bean.GetKind()); Assert.Equal(typeof(Bean), bean.GetType()); @@ -18,7 +21,9 @@ public void Dispense_Default() { [Fact] public void Dispense_Hooks() { - var crud = new BeanCrud(null, null, null); + IBeanFactory factory = new BeanFactory(); + factory.Options.ValidateGetColumns = false; + var crud = new BeanCrud(null, null, null, factory); var observer = new TracingObserver(); crud.AddObserver(observer); @@ -32,7 +37,9 @@ public void Dispense_Hooks() { [Fact] public void Store() { - var crud = new BeanCrud(new InMemoryStorage(), null, new KeyUtil()); + IBeanFactory factory = new BeanFactory(); + factory.Options.ValidateGetColumns = false; + var crud = new BeanCrud(new InMemoryStorage(), null, new KeyUtil(), factory); var observer = new TracingObserver(); crud.AddObserver(observer); @@ -48,7 +55,9 @@ public void Store() { [Fact] public void Load() { - var crud = new BeanCrud(new InMemoryStorage(), null, new KeyUtil()); + IBeanFactory factory = new BeanFactory(); + factory.Options.ValidateGetColumns = false; + var crud = new BeanCrud(new InMemoryStorage(), null, new KeyUtil(), factory); var observer = new TracingObserver(); crud.AddObserver(observer); @@ -72,7 +81,9 @@ public void Load() { [Fact] public void Trash() { - var crud = new BeanCrud(new InMemoryStorage(), null, new KeyUtil()); + IBeanFactory factory = new BeanFactory(); + factory.Options.ValidateGetColumns = false; + var crud = new BeanCrud(new InMemoryStorage(), null, new KeyUtil(), factory); var observer = new TracingObserver(); crud.AddObserver(observer); @@ -96,7 +107,9 @@ public void Trash() { [Fact] public void RowToBean() { - var crud = new BeanCrud(new InMemoryStorage(), null, null); + IBeanFactory factory = new BeanFactory(); + factory.Options.ValidateGetColumns = false; + var crud = new BeanCrud(new InMemoryStorage(), null, null, factory); var observer = new TracingObserver(); crud.AddObserver(observer); @@ -126,7 +139,8 @@ public void RowToBean() { [Fact] public void PreventDirectInstantiation() { - var crud = new BeanCrud(null, null, null); + IBeanFactory factory = new BeanFactory(); + var crud = new BeanCrud(null, null, null, factory); Assert.Throws(delegate() { crud.Store(new Tracer()); diff --git a/LimeBean.Tests/BeanFactoryTests.cs b/LimeBean.Tests/BeanFactoryTests.cs new file mode 100644 index 0000000..101de5e --- /dev/null +++ b/LimeBean.Tests/BeanFactoryTests.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Xunit; + +using LimeBean.Interfaces; +using LimeBean.Exceptions; + +namespace LimeBean.Tests { + + public class BeanFactoryTests { + + [Fact] + public void Dispense_ValidateGetColumns_Test() { + IBeanFactory factory = new BeanFactory(); + object one; + object two; + Bean bean; + + Func make = validateColumns => { + factory.Options.ValidateGetColumns = false; + Bean b = factory.Dispense("test"); + Assert.Equal(typeof(Bean), b.GetType()); + Assert.Equal(false, b.ValidateGetColumns); + b.Put("one", 1); + return b; + }; + + // With ValidateGetColumns switched off + bean = make(false); + one = (int)bean["one"]; + Assert.Equal(1, one); + one = bean.Get("one"); + Assert.Equal(1, one); + two = bean.Get("two"); + Assert.Equal(0, two); + two = bean["two"]; + Assert.Equal(null, two); + + // With ValidateGetColumns switched on + bean = make(true); + one = (int)bean["one"]; + Assert.Equal(1, one); + one = bean.Get("one"); + Assert.Equal(1, one); + try { + two = bean["two"]; + } catch (Exception e) { + Assert.IsType(typeof(ColumnNotFoundException), e); + } + try { + two = bean.Get("two"); + } catch (Exception e) { + Assert.IsType(typeof(ColumnNotFoundException), e); + } + } + + } + +} diff --git a/LimeBean.Tests/DatabaseBeanFinderTests.cs b/LimeBean.Tests/DatabaseBeanFinderTests.cs index 46f15d3..e399c23 100644 --- a/LimeBean.Tests/DatabaseBeanFinderTests.cs +++ b/LimeBean.Tests/DatabaseBeanFinderTests.cs @@ -22,7 +22,8 @@ public DatabaseBeanFinderTests() { IDatabaseAccess db = new DatabaseAccess(_conn, details); IKeyAccess keys = new KeyUtil(); IStorage storage = new DatabaseStorage(details, db, keys); - IBeanCrud crud = new BeanCrud(storage, db, keys); + IBeanFactory factory = new BeanFactory(); + IBeanCrud crud = new BeanCrud(storage, db, keys, factory); IBeanFinder finder = new DatabaseBeanFinder(details, db, crud); db.Exec("create table foo(x)"); diff --git a/LimeBean.Tests/IntegrationTests.cs b/LimeBean.Tests/IntegrationTests.cs index d756b2a..8f57e17 100644 --- a/LimeBean.Tests/IntegrationTests.cs +++ b/LimeBean.Tests/IntegrationTests.cs @@ -19,7 +19,8 @@ public void ImplicitTransactionsOnStoreAndTrash() { IDatabaseAccess db = new DatabaseAccess(conn, details); IKeyAccess keys = new KeyUtil(); DatabaseStorage storage = new DatabaseStorage(details, db, keys); - IBeanCrud crud = new BeanCrud(storage, db, keys); + IBeanFactory factory = new BeanFactory(); + IBeanCrud crud = new BeanCrud(storage, db, keys, factory); storage.EnterFluidMode(); diff --git a/LimeBean.Tests/LimeBean.Tests.csproj b/LimeBean.Tests/LimeBean.Tests.csproj index f971210..1bede51 100644 --- a/LimeBean.Tests/LimeBean.Tests.csproj +++ b/LimeBean.Tests/LimeBean.Tests.csproj @@ -73,6 +73,7 @@ + diff --git a/LimeBean.Website/Body.cs b/LimeBean.Website/Body.cs index 3345ca7..494b99d 100644 --- a/LimeBean.Website/Body.cs +++ b/LimeBean.Website/Body.cs @@ -108,7 +108,7 @@ void CRUD(BeanApi api) { // Store() will Create or Update a record intelligently var id = api.Store(bean); - // Store also returns the Primary Key for the saved Bean, even for [multi-column/compound keys](#primary-keys) + // Store also returns the Primary Key for the saved Bean, even for multi-column/compound keys Console.WriteLine(id); #endif /// **Read** and **Update** @@ -146,6 +146,23 @@ void TypedAccessors(Bean bean) { /// See also: [Custom Bean Classes](#custom-bean-classes) } + void BeanOptions(BeanApi api) { + /// ## Bean Options + /// You can configure the BeanAPI to dispense new Beans with some default options + /// + /// **.ValidateGetColumns** +#if CODE + // Sets whether a Bean throws `ColumnNotFoundException` if + // you request a column which isn't stored in the Bean. True by default + api.BeanOptions.ValidateGetColumns = true; + + Bean bean = api.Dispense("books"); + bean.Put("ColumnOne", 1); // Add a single column + int one = bean.Get("ColumnOne"); // OK + int two = bean.Get("ColumnTwo"); // throws ColumnNotFoundException +#endif + } + void FluidMode(BeanApi api) { /// ## Fluid Mode /// LimeBean mitigates the common inconvenience associated with relational databases, diff --git a/LimeBean/Bean.cs b/LimeBean/Bean.cs index 78d868c..3b930e7 100644 --- a/LimeBean/Bean.cs +++ b/LimeBean/Bean.cs @@ -59,7 +59,11 @@ public override string ToString() { /// /// Name of the Column to Get or Set public object this[string name] { - get { return _props.GetSafe(name); } + get { + if (ValidateGetColumns) + ValidateColumnExists(name); + + return _props.GetSafe(name); } set { SaveDirtyBackup(name, value); _props[name] = value; @@ -94,6 +98,19 @@ public IEnumerable Columns { } + // Bean Options + + internal bool ValidateGetColumns { + get; + set; + } + + private void ValidateColumnExists(string name) { + if (_props.ContainsKey(name) == false) + throw Exceptions.ColumnNotFoundException.New(this, name); + } + + // Import / Export internal IDictionary Export() { @@ -109,7 +126,7 @@ internal void Import(IDictionary data) { // Dirty tracking void SaveDirtyBackup(string name, object newValue) { - var currentValue = this[name]; + var currentValue = _props.GetSafe(name); if(Equals(newValue, currentValue)) return; diff --git a/LimeBean/BeanApi.cs b/LimeBean/BeanApi.cs index 4be7f6c..9d9a5bf 100644 --- a/LimeBean/BeanApi.cs +++ b/LimeBean/BeanApi.cs @@ -16,6 +16,7 @@ public partial class BeanApi : IBeanApi { KeyUtil _keyUtil; DatabaseStorage _storage; IBeanCrud _crud; + IBeanFactory _factory; IBeanFinder _finder; public BeanApi(string connectionString, DbProviderFactory factory) { @@ -74,13 +75,21 @@ DatabaseStorage Storage { IBeanCrud Crud { get { if(_crud == null) { - _crud = new BeanCrud(Storage, Db, KeyUtil); + _crud = new BeanCrud(Storage, Db, KeyUtil, Factory); _crud.AddObserver(new BeanApiLinker(this)); } return _crud; } } + IBeanFactory Factory { + get { + if (_factory == null) + _factory = new BeanFactory(); + return _factory; + } + } + IBeanFinder Finder { get { if(_finder == null) @@ -137,6 +146,13 @@ public void Dispose() { _connectionContainer.Dispose(); } + /// + /// Provides configuration of default Bean + /// options, for Beans created by Limebean + /// + public IBeanOptions BeanOptions { + get { return Factory.Options; } + } // IBeanCrud diff --git a/LimeBean/BeanCrud.cs b/LimeBean/BeanCrud.cs index d1d2f81..1aa347d 100644 --- a/LimeBean/BeanCrud.cs +++ b/LimeBean/BeanCrud.cs @@ -12,12 +12,14 @@ class BeanCrud : IBeanCrud { ITransactionSupport _transactionSupport; IKeyAccess _keyAccess; IList _observers; + IBeanFactory _factory; - public BeanCrud(IStorage storage, ITransactionSupport transactionSupport, IKeyAccess keys) { + public BeanCrud(IStorage storage, ITransactionSupport transactionSupport, IKeyAccess keys, IBeanFactory factory) { _storage = storage; _transactionSupport = transactionSupport; _keyAccess = keys; _observers = new List(); + _factory = factory; DirtyTracking = true; } @@ -32,11 +34,13 @@ public void RemoveObserver(BeanObserver observer) { } public Bean Dispense(string kind) { - return ContinueDispense(new Bean(kind)); + Bean bean = _factory.Dispense(kind); + return ContinueDispense(bean); } public T Dispense() where T : Bean, new() { - return ContinueDispense(new T()); + T bean = _factory.Dispense(); + return ContinueDispense(bean); } public Bean RowToBean(string kind, IDictionary row) { @@ -109,8 +113,6 @@ public void Trash(Bean bean) { } T ContinueDispense(T bean) where T : Bean { - bean.Dispensed = true; - bean.AfterDispense(); foreach(var observer in _observers) observer.AfterDispense(bean); diff --git a/LimeBean/BeanFactory.cs b/LimeBean/BeanFactory.cs new file mode 100644 index 0000000..8ad32c8 --- /dev/null +++ b/LimeBean/BeanFactory.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using LimeBean.Interfaces; + +namespace LimeBean { + + internal class BeanFactory : IBeanFactory { + internal BeanFactory() { } + + IBeanOptions _config; + public IBeanOptions Options { + get { + if (_config == null) + _config = new BeanOptions(); + return _config; + } + } + + public Bean Dispense(string kind) { + Bean bean = new Bean(kind); + return ConfigureBean(bean); + } + + public T Dispense() where T : Bean, new() { + T bean = new T(); + return ConfigureBean(bean); + } + + private T ConfigureBean(T bean) where T : Bean { + bean.Dispensed = true; + bean.ValidateGetColumns = Options.ValidateGetColumns; + return bean; + } + + } + +} diff --git a/LimeBean/BeanOptions.cs b/LimeBean/BeanOptions.cs new file mode 100644 index 0000000..9e40247 --- /dev/null +++ b/LimeBean/BeanOptions.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using LimeBean.Interfaces; + +namespace LimeBean { + class BeanOptions : IBeanOptions { + + /// + /// Specifies whether each Bean[column] or Bean.Get(column) call + /// will throw ColumnNotFoundException if the column does not exist. Default True + /// + public bool ValidateGetColumns { + get { return _ValidateGetColumns; } + set { _ValidateGetColumns = value; } + } + private bool _ValidateGetColumns = true; + } +} diff --git a/LimeBean/Exceptions/ColumnNotFoundException.cs b/LimeBean/Exceptions/ColumnNotFoundException.cs new file mode 100644 index 0000000..56e44a1 --- /dev/null +++ b/LimeBean/Exceptions/ColumnNotFoundException.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace LimeBean.Exceptions { + class ColumnNotFoundException : Exception { + public ColumnNotFoundException() { } + + public ColumnNotFoundException(string message) : base(message) { } + + public static ColumnNotFoundException New(Bean bean, string column) { + string message = String.Format( + @"The requested column '{0}' for Bean '{1}' was not found. + You can assign a value to the column to create it", + column, bean.GetKind()); + return new ColumnNotFoundException(message); + } + } +} diff --git a/LimeBean/Interfaces/IBeanCrud.cs b/LimeBean/Interfaces/IBeanCrud.cs index ed66527..099b0fd 100644 --- a/LimeBean/Interfaces/IBeanCrud.cs +++ b/LimeBean/Interfaces/IBeanCrud.cs @@ -3,15 +3,12 @@ namespace LimeBean.Interfaces { - public interface IBeanCrud { + public interface IBeanCrud : IBeanDispenser { bool DirtyTracking { get; set; } void AddObserver(BeanObserver observer); void RemoveObserver(BeanObserver observer); - Bean Dispense(string kind); - T Dispense() where T : Bean, new(); - Bean RowToBean(string kind, IDictionary row); T RowToBean(IDictionary row) where T : Bean, new(); diff --git a/LimeBean/Interfaces/IBeanDispenser.cs b/LimeBean/Interfaces/IBeanDispenser.cs new file mode 100644 index 0000000..5016229 --- /dev/null +++ b/LimeBean/Interfaces/IBeanDispenser.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace LimeBean.Interfaces { + + public interface IBeanDispenser { + Bean Dispense(string kind); + T Dispense() where T : Bean, new(); + } + +} diff --git a/LimeBean/Interfaces/IBeanFactory.cs b/LimeBean/Interfaces/IBeanFactory.cs new file mode 100644 index 0000000..fa19767 --- /dev/null +++ b/LimeBean/Interfaces/IBeanFactory.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace LimeBean.Interfaces { + + public interface IBeanFactory : IBeanDispenser { + IBeanOptions Options { get; } + } + +} diff --git a/LimeBean/Interfaces/IBeanOptions.cs b/LimeBean/Interfaces/IBeanOptions.cs new file mode 100644 index 0000000..842cc36 --- /dev/null +++ b/LimeBean/Interfaces/IBeanOptions.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace LimeBean.Interfaces { + + public interface IBeanOptions { + bool ValidateGetColumns { get; set; } + } + +} diff --git a/LimeBean/LimeBean.csproj b/LimeBean/LimeBean.csproj index b8e2e07..a54f6cd 100644 --- a/LimeBean/LimeBean.csproj +++ b/LimeBean/LimeBean.csproj @@ -41,6 +41,8 @@ + + @@ -51,8 +53,12 @@ + + + +