Browse files

Added HttpClient which is a wrapper around HttpWebRequest, to make te…

…sting easier. HttpClient is fully implemented and works. Just create HttpRequests and call Send passing the request. Started implementing the SaveChanges logic after adding a new entity to the session. Also created the initial CouchDocument which uses the new dynamic support in the .net/mono frameworks. All that's left is to finish the end-to-end communication, by creating a couch command that uses the _bulk_docs API call on CouchDB, executing the command with the couch proxy which creates a new REST request using a factory which ultimately gets sent using HttpClient.
  • Loading branch information...
1 parent 2b05b76 commit d6902bf5626d99fe321e0b1dd1e270309ec09d36 @dragan dragan committed Aug 3, 2010
View
72 src/SineSignal.Ottoman.Specs/CouchDocumentSessionSpecs.cs
@@ -1,10 +1,12 @@
using System;
+using System.Collections.Generic;
using System.Reflection;
using NSubstitute;
using NUnit.Framework;
using SineSignal.Ottoman.Specs.Framework;
+using SineSignal.Ottoman.Commands;
using SineSignal.Ottoman.Exceptions;
namespace SineSignal.Ottoman.Specs
@@ -19,6 +21,7 @@ public class When_storing_a_new_entity_into_the_session : ConcernFor<CouchDocume
private Type identityType;
private Guid id;
private IDocumentConvention documentConvention;
+ private ICouchDatabase couchDatabase;
protected override void Given()
{
@@ -27,14 +30,18 @@ protected override void Given()
identityProperty = entity1Type.GetProperty("Id");
identityType = identityProperty.PropertyType;
id = Guid.NewGuid();
+
documentConvention = Fake<IDocumentConvention>();
documentConvention.GetIdentityPropertyFor(entity1Type).Returns(identityProperty);
documentConvention.GenerateIdentityFor(identityType).Returns(id);
+
+ couchDatabase = Fake<ICouchDatabase>();
+ couchDatabase.DocumentConvention.Returns(documentConvention);
}
public override CouchDocumentSession CreateSystemUnderTest()
{
- return new CouchDocumentSession(documentConvention);
+ return new CouchDocumentSession(couchDatabase);
}
protected override void When()
@@ -75,20 +82,25 @@ public class When_storing_an_entity_with_an_id_already_assigned : ConcernFor<Cou
private Type entity1Type;
private PropertyInfo identityProperty;
private IDocumentConvention documentConvention;
+ private ICouchDatabase couchDatabase;
protected override void Given()
{
id = Guid.NewGuid();
entity1 = new Employee { Id = id, Name = "Bob", Login = "boblogin" };
entity1Type = entity1.GetType();
identityProperty = entity1Type.GetProperty("Id");
+
documentConvention = Fake<IDocumentConvention>();
documentConvention.GetIdentityPropertyFor(entity1Type).Returns(identityProperty);
+
+ couchDatabase = Fake<ICouchDatabase>();
+ couchDatabase.DocumentConvention.Returns(documentConvention);
}
public override CouchDocumentSession CreateSystemUnderTest()
{
- return new CouchDocumentSession(documentConvention);
+ return new CouchDocumentSession(couchDatabase);
}
protected override void When()
@@ -129,8 +141,9 @@ public class When_attempting_to_store_a_different_entity_with_the_same_id_as_ano
private Employee entity2;
private Type entity2Type;
private PropertyInfo identityProperty;
- private IDocumentConvention documentConvention;
private NonUniqueEntityException thrownException;
+ private IDocumentConvention documentConvention;
+ private ICouchDatabase couchDatabase;
protected override void Given()
{
@@ -139,13 +152,17 @@ protected override void Given()
entity2 = new Employee { Id = id, Name = "Carl", Login = "carllogin" };
entity2Type = entity2.GetType();
identityProperty = entity2Type.GetProperty("Id");
+
documentConvention = Fake<IDocumentConvention>();
documentConvention.GetIdentityPropertyFor(entity2Type).Returns(identityProperty);
+
+ couchDatabase = Fake<ICouchDatabase>();
+ couchDatabase.DocumentConvention.Returns(documentConvention);
}
public override CouchDocumentSession CreateSystemUnderTest()
{
- var couchDocumentSession = new CouchDocumentSession(documentConvention);
+ var couchDocumentSession = new CouchDocumentSession(couchDatabase);
couchDocumentSession.Store(entity1);
return couchDocumentSession;
}
@@ -169,6 +186,53 @@ public void Should_throw_non_unique_entity_exception()
Assert.That(thrownException.Message, Is.EqualTo("Attempted to associate a different entity with id '" + id + "'."));
}
}
+
+ public class When_saving_changes_after_storing_a_new_entity : ConcernFor<CouchDocumentSession>
+ {
+ private Employee entity1;
+ private Type entity1Type;
+ private PropertyInfo identityProperty;
+ private Type identityType;
+ private Guid id;
+ private IDocumentConvention documentConvention;
+ private dynamic couchDocument;
+ private ICouchDatabase couchDatabase;
+
+ protected override void Given()
+ {
+ entity1 = new Employee { Name = "Bob", Login = "boblogin" };
+ entity1Type = entity1.GetType();
+ identityProperty = entity1Type.GetProperty("Id");
+ identityType = identityProperty.PropertyType;
+ id = Guid.NewGuid();
+
+ documentConvention = Fake<IDocumentConvention>();
+ documentConvention.GetIdentityPropertyFor(entity1Type).Returns(identityProperty);
+ documentConvention.GenerateIdentityFor(identityType).Returns(id);
+
+ couchDocument = new CouchDocument(entity1, identityProperty);
+
+ couchDatabase = Fake<ICouchDatabase>();
+ couchDatabase.DocumentConvention.Returns(documentConvention);
+ }
+
+ public override CouchDocumentSession CreateSystemUnderTest()
+ {
+ return new CouchDocumentSession(couchDatabase);
+ }
+
+ protected override void When()
+ {
+ Sut.Store(entity1);
+ Sut.SaveChanges();
+ }
+
+ [Test]
+ public void Should_call_get_identity_property()
+ {
+ documentConvention.Received().GetIdentityPropertyFor(entity1Type);
+ }
+ }
}
public class Employee
View
42 src/SineSignal.Ottoman.Specs/CouchDocumentSpecs.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Dynamic;
+using System.Reflection;
+
+using NUnit.Framework;
+using SineSignal.Ottoman.Specs.Framework;
+
+namespace SineSignal.Ottoman.Specs
+{
+ public class CouchDocumentSpecs
+ {
+ public class When_instantiating_a_new_couch_document : ConcernFor<CouchDocument>
+ {
+ private Employee entity1;
+ private Type entity1Type;
+ private PropertyInfo identityProperty;
+
+ protected override void Given()
+ {
+ entity1 = new Employee { Name = "Bob", Login = "boblogin" };
+ entity1Type = entity1.GetType();
+ identityProperty = entity1Type.GetProperty("Id");
+ }
+
+ public override CouchDocument CreateSystemUnderTest()
+ {
+ return new CouchDocument(entity1, identityProperty);
+ }
+
+ [Test]
+ public void Should_copy_passed_in_entity()
+ {
+ dynamic sut = (dynamic)Sut;
+
+ Assert.That(sut._id, Is.EqualTo(entity1.Id));
+ Assert.That(sut.Type, Is.EqualTo(entity1Type.Name));
+ Assert.That(sut.Name, Is.EqualTo(entity1.Name));
+ Assert.That(sut.Login, Is.EqualTo(entity1.Login));
+ }
+ }
+ }
+}
View
11 src/SineSignal.Ottoman.Specs/CouchProxySpecs.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace SineSignal.Ottoman.Specs
+{
+ public class CouchProxySpecs
+ {
+ public CouchProxySpecs()
+ {
+ }
+ }
+}
View
3 src/SineSignal.Ottoman.Specs/SineSignal.Ottoman.Specs.csproj
@@ -36,6 +36,8 @@
<Compile Include="Framework\FakeAdaptor.cs" />
<Compile Include="Framework\StaticConcern.cs" />
<Compile Include="CouchDocumentSessionSpecs.cs" />
+ <Compile Include="CouchProxySpecs.cs" />
+ <Compile Include="CouchDocumentSpecs.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SineSignal.Ottoman\SineSignal.Ottoman.csproj">
@@ -54,6 +56,7 @@
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\tools\nsubstitute\NSubstitute.dll</HintPath>
</Reference>
+ <Reference Include="Microsoft.CSharp" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<ItemGroup>
View
8 src/SineSignal.Ottoman/Commands/ICouchCommand.cs
@@ -0,0 +1,8 @@
+using System;
+
+namespace SineSignal.Ottoman.Commands
+{
+ public interface ICouchCommand
+ {
+ }
+}
View
49 src/SineSignal.Ottoman/CouchDocument.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.Dynamic;
+using System.Linq;
+using System.Reflection;
+
+namespace SineSignal.Ottoman
+{
+ public sealed class CouchDocument : DynamicObject
+ {
+ private Dictionary<string, object> Members { get; set; }
+ private PropertyInfo IdentityProperty { get; set; }
+
+ public CouchDocument(object entity, PropertyInfo identityProperty)
+ {
+ Members = new Dictionary<string, object>();
+ IdentityProperty = identityProperty;
+ CopyPropertiesFrom(entity);
+ Members["type"] = entity.GetType().Name;
+ }
+
+ public override bool TryGetMember(GetMemberBinder binder, out object result)
+ {
+ string name = binder.Name.ToLower();
+
+ return Members.TryGetValue(name, out result);
+ }
+
+ public override bool TrySetMember(SetMemberBinder binder, object value)
+ {
+ Members[binder.Name.ToLower()] = value;
+
+ return true;
+ }
+
+ private void CopyPropertiesFrom(object source)
+ {
+ foreach (PropertyInfo property in source.GetType().GetProperties().Where(p => p.CanRead))
+ {
+ var key = property.Name;
+ if (key == IdentityProperty.Name)
+ key = "_id";
+
+ var value = property.GetValue(source, null);
+ Members[key.ToLower()] = value;
+ }
+ }
+ }
+}
View
27 src/SineSignal.Ottoman/CouchDocumentSession.cs
@@ -1,26 +1,30 @@
using System;
using System.Collections.Generic;
+using System.Dynamic;
using System.Reflection;
+using SineSignal.Ottoman.Commands;
using SineSignal.Ottoman.Exceptions;
+using SineSignal.Ottoman.Http;
namespace SineSignal.Ottoman
{
- public class CouchDocumentSession
+ public class CouchDocumentSession : ICouchDocumentSession
{
- public IDocumentConvention DocumentConvention { get; private set; }
+ public ICouchDatabase CouchDatabase { get; private set; }
+
private Dictionary<string, object> IdentityMap { get; set; }
- public CouchDocumentSession(IDocumentConvention documentConvention)
+ public CouchDocumentSession(ICouchDatabase couchDatabase)
{
- DocumentConvention = documentConvention;
+ CouchDatabase = couchDatabase;
IdentityMap = new Dictionary<string, object>();
}
public void Store(object entity)
{
Type entityType = entity.GetType();
- PropertyInfo identityProperty = DocumentConvention.GetIdentityPropertyFor(entityType);
+ PropertyInfo identityProperty = CouchDatabase.DocumentConvention.GetIdentityPropertyFor(entityType);
object id = null;
if (identityProperty != null)
@@ -29,7 +33,7 @@ public void Store(object entity)
if (id == null)
{
- id = DocumentConvention.GenerateIdentityFor(identityProperty.PropertyType);
+ id = CouchDatabase.DocumentConvention.GenerateIdentityFor(identityProperty.PropertyType);
identityProperty.SetValue(entity, id, null);
}
}
@@ -59,6 +63,17 @@ public T Load<T>(string id)
return default(T);
}
+ public void SaveChanges()
+ {
+ var docs = new List<dynamic>();
+ foreach (object entity in IdentityMap.Values)
+ {
+ PropertyInfo identityProperty = CouchDatabase.DocumentConvention.GetIdentityPropertyFor(entity.GetType());
+ dynamic couchDocument = new CouchDocument(entity, identityProperty);
+ docs.Add(couchDocument);
+ }
+ }
+
private static object GetIdentityValueFor(object entity, PropertyInfo identityProperty)
{
object id = identityProperty.GetValue(entity, null);
View
112 src/SineSignal.Ottoman/Http/HttpClient.cs
@@ -0,0 +1,112 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Text;
+
+namespace SineSignal.Ottoman.Http
+{
+ public sealed class HttpClient : IHttpClient
+ {
+ private const string USER_AGENT = "SineSignal Ottoman CouchDB API";
+
+ public HttpResponse Send(HttpRequest httpRequest)
+ {
+ return GenerateHttpResponse(InitializeHttpWebRequest(httpRequest));
+ }
+
+ private static HttpWebRequest InitializeHttpWebRequest(HttpRequest httpRequest)
+ {
+ HttpWebRequest httpWebRequest = WebRequest.Create(httpRequest.Url) as HttpWebRequest;
+ httpWebRequest.UserAgent = USER_AGENT;
+ httpWebRequest.Method = httpRequest.Method;
+ httpWebRequest.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip | DecompressionMethods.None;
+
+ if (httpWebRequest.Method == HttpMethod.Put || httpWebRequest.Method == HttpMethod.Post)
+ {
+ AppendRequestContent(httpRequest, httpWebRequest);
+ }
+
+ return httpWebRequest;
+ }
+
+ private static void AppendRequestContent(HttpRequest httpRequest, HttpWebRequest httpWebRequest)
+ {
+ if (!String.IsNullOrEmpty(httpRequest.ContentType) &&
+ !String.IsNullOrEmpty(httpRequest.Content))
+ {
+ httpWebRequest.ContentType = httpRequest.ContentType;
+ httpWebRequest.ContentLength = httpRequest.ContentLength;
+ using (var httpRequestStream = httpWebRequest.GetRequestStream())
+ {
+ byte[] requestBuffer = Encoding.UTF8.GetBytes(httpRequest.Content);
+ httpRequestStream.Write(requestBuffer, 0, requestBuffer.Length);
+ }
+ }
+ }
+
+ private static HttpResponse GenerateHttpResponse(HttpWebRequest httpWebRequest)
+ {
+ var httpResponse = new HttpResponse();
+
+ try
+ {
+ HttpWebResponse httpWebResponse = GenerateHttpWebResponse(httpWebRequest);
+ MapResponses(httpWebResponse, httpResponse);
+ }
+ catch (Exception exception)
+ {
+ httpResponse.Error = exception;
+ }
+
+ return httpResponse;
+ }
+
+ private static HttpWebResponse GenerateHttpWebResponse(HttpWebRequest httpWebRequest)
+ {
+ HttpWebResponse httpWebResponse = null;
+
+ try
+ {
+ httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse;
+ }
+ catch (WebException webException)
+ {
+ httpWebResponse = webException.Response as HttpWebResponse;
+ }
+
+ return httpWebResponse;
+ }
+
+ private static void MapResponses(HttpWebResponse httpWebResponse, HttpResponse httpResponse)
+ {
+ using (httpWebResponse)
+ {
+ httpResponse.ResponseUri = httpWebResponse.ResponseUri;
+ httpResponse.StatusCode = httpWebResponse.StatusCode;
+ httpResponse.StatusDescription = httpWebResponse.StatusDescription;
+ httpResponse.ContentType = httpWebResponse.ContentType;
+ httpResponse.ContentLength = httpWebResponse.ContentLength;
+ httpResponse.ContentEncoding = httpWebResponse.ContentEncoding;
+ httpResponse.Content = GetContentFrom(httpWebResponse);
+
+ foreach (string headerName in httpWebResponse.Headers.AllKeys)
+ {
+ string headerValue = httpWebResponse.Headers[headerName];
+ httpResponse.Headers.Add(new HttpHeader { Name = headerName, Value = headerValue });
+ }
+ }
+ }
+
+ private static string GetContentFrom(HttpWebResponse httpWebResponse)
+ {
+ string content;
+
+ using (StreamReader streamReader = new StreamReader(httpWebResponse.GetResponseStream()))
+ {
+ content = streamReader.ReadToEnd();
+ }
+
+ return content;
+ }
+ }
+}
View
8 src/SineSignal.Ottoman/Http/HttpHeader.cs
@@ -0,0 +1,8 @@
+namespace SineSignal.Ottoman.Http
+{
+ public sealed class HttpHeader
+ {
+ public string Name { get; set; }
+ public string Value { get; set; }
+ }
+}
View
12 src/SineSignal.Ottoman/Http/HttpMethod.cs
@@ -0,0 +1,12 @@
+namespace SineSignal.Ottoman.Http
+{
+ public static class HttpMethod
+ {
+ public const string Get = "GET";
+ public const string Post = "POST";
+ public const string Put = "PUT";
+ public const string Delete = "DELETE";
+ public const string Head = "HEAD";
+ public const string Options = "OPTIONS";
+ }
+}
View
28 src/SineSignal.Ottoman/Http/HttpRequest.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+
+namespace SineSignal.Ottoman.Http
+{
+ public sealed class HttpRequest
+ {
+ public Uri Url { get; set; }
+ public IList<HttpHeader> Headers { get; private set; }
+ public string Method { get; set; }
+ public string ContentType { get; set; }
+ public string Content { get; set; }
+
+ public long ContentLength
+ {
+ get { return !String.IsNullOrEmpty(Content) ? Content.Length : 0; }
+ }
+
+ public int Timeout { get; set; }
+ public ICredentials Credentials { get; set; }
+
+ public HttpRequest()
+ {
+ Headers = new List<HttpHeader>();
+ }
+ }
+}
View
24 src/SineSignal.Ottoman/Http/HttpResponse.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+
+namespace SineSignal.Ottoman.Http
+{
+ public sealed class HttpResponse
+ {
+ public Uri ResponseUri { get; set; }
+ public IList<HttpHeader> Headers { get; private set; }
+ public HttpStatusCode StatusCode { get; set; }
+ public string StatusDescription { get; set; }
+ public string ContentType { get; set; }
+ public long ContentLength { get; set; }
+ public string ContentEncoding { get; set; }
+ public string Content { get; set; }
+ public Exception Error { get; set; }
+
+ public HttpResponse()
+ {
+ Headers = new List<HttpHeader>();
+ }
+ }
+}
View
7 src/SineSignal.Ottoman/Http/IHttpClient.cs
@@ -0,0 +1,7 @@
+namespace SineSignal.Ottoman.Http
+{
+ public interface IHttpClient
+ {
+ HttpResponse Send(HttpRequest httpRequest);
+ }
+}
View
8 src/SineSignal.Ottoman/ICouchDatabase.cs
@@ -0,0 +1,8 @@
+namespace SineSignal.Ottoman
+{
+ public interface ICouchDatabase
+ {
+ ICouchProxy CouchProxy { get; }
+ IDocumentConvention DocumentConvention { get; }
+ }
+}
View
9 src/SineSignal.Ottoman/ICouchDocumentSession.cs
@@ -0,0 +1,9 @@
+namespace SineSignal.Ottoman
+{
+ public interface ICouchDocumentSession
+ {
+ void Store(object entity);
+ T Load<T>(string id);
+ void SaveChanges();
+ }
+}
View
9 src/SineSignal.Ottoman/ICouchProxy.cs
@@ -0,0 +1,9 @@
+using SineSignal.Ottoman.Commands;
+
+namespace SineSignal.Ottoman
+{
+ public interface ICouchProxy
+ {
+ void Execute(ICouchCommand couchCommand);
+ }
+}
View
14 src/SineSignal.Ottoman/SineSignal.Ottoman.csproj
@@ -34,13 +34,27 @@
<Compile Include="CouchDocumentSession.cs" />
<Compile Include="IDocumentConvention.cs" />
<Compile Include="Exceptions\NonUniqueEntityException.cs" />
+ <Compile Include="Http\HttpClient.cs" />
+ <Compile Include="Http\HttpRequest.cs" />
+ <Compile Include="Http\HttpMethod.cs" />
+ <Compile Include="Http\HttpResponse.cs" />
+ <Compile Include="Http\IHttpClient.cs" />
+ <Compile Include="Http\HttpHeader.cs" />
+ <Compile Include="CouchDocument.cs" />
+ <Compile Include="ICouchProxy.cs" />
+ <Compile Include="Commands\ICouchCommand.cs" />
+ <Compile Include="ICouchDatabase.cs" />
+ <Compile Include="ICouchDocumentSession.cs" />
</ItemGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
+ <Reference Include="Microsoft.CSharp" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<ItemGroup>
<Folder Include="Exceptions\" />
+ <Folder Include="Http\" />
+ <Folder Include="Commands\" />
</ItemGroup>
</Project>

0 comments on commit d6902bf

Please sign in to comment.