diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d45997 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +obj +bin +_ReSharper.* +*.csproj.user +*.resharper.user +*.resharper +*.suo +*.cache +*~ +*.swp +.svn \ No newline at end of file diff --git a/Divan.sln b/Divan.sln new file mode 100644 index 0000000..25cde46 --- /dev/null +++ b/Divan.sln @@ -0,0 +1,26 @@ + +Microsoft Visual Studio Solution File, Format Version 10.00 +# Visual Studio 2008 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Divan", "src\Divan.csproj", "{37AC0B66-5340-4B81-BC62-3EE80233A011}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Trivial", "samples\Trivial\Trivial.csproj", "{CDCC7924-F227-46DC-B2E6-2BBE06B84AF2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {37AC0B66-5340-4B81-BC62-3EE80233A011}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37AC0B66-5340-4B81-BC62-3EE80233A011}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37AC0B66-5340-4B81-BC62-3EE80233A011}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37AC0B66-5340-4B81-BC62-3EE80233A011}.Release|Any CPU.Build.0 = Release|Any CPU + {CDCC7924-F227-46DC-B2E6-2BBE06B84AF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CDCC7924-F227-46DC-B2E6-2BBE06B84AF2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CDCC7924-F227-46DC-B2E6-2BBE06B84AF2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CDCC7924-F227-46DC-B2E6-2BBE06B84AF2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/README.rdoc b/README.rdoc new file mode 100644 index 0000000..d6cd275 --- /dev/null +++ b/README.rdoc @@ -0,0 +1,92 @@ += Divan, a C# library for CouchDB + +Divan is a C# library for using CouchDB (http://www.couchdb.org). It should be more or less API complete +including bulk operations, attachments, views and design documents etc. It is quite fast and designed +to be flexible but not bloated. + +Divan has been developed in-house at Foretagsplatsen AB (www.foretagsplatsen.se) and is being used in +the new core system at Foretagsplatsen. It has unit tests (although could benefit from more) and at least +one sample console project included. + +== Does it work under Mono? + +You bet. Foretagsplatsen uses mainly windows (Visual Studio 2008 and .Net 3.5) but Divan is meant to +work fine in Mono too. + +== What about documentation? + +At the moment documentation is... this file! :) But there are unit tests in CouchTest.cs and there +is at least one sample project showing basic usage. One more sample with more advanced usage is coming soon. + +== Dependencies + +The only dependencies and their tested versions are: + +* Newtonsoft.JSON (3.5 Beta 4), MIT-licensed fast library for JSON reading and writing, see: http://json.codeplex.com +* NUnit (2.4.8). Unit testing framework, see: http://www.nunit.org +* CouchDB (0.9.1, 0.9). Running on a server somewhere, see: http://www.couchdb.org + +The two neeed dlls are included in the lib directory. + +== Getting started + +Well... it goes something like this: + +1. First get CouchDB up and running on some box. + +2. Clone Divan and build it. + +3. Run the "Trivial" sample console app by pointing it at a running CouchDB server getting output similar to this: + + C:\Divan\samples\Trivial>bin\Debug\Trivial.exe 192.168.9.205 5984 + Using 192.168.9.205:5984 + Created a CouchServer + Request: http://192.168.9.205:5984/trivial Method: HEAD + Request: http://192.168.9.205:5984/trivial Method: PUT + Created database 'trivial' + Request: http://192.168.9.205:5984/trivial/ Method: POST + Request: http://192.168.9.205:5984/trivial/ Method: POST + Request: http://192.168.9.205:5984/trivial/ Method: POST + Request: http://192.168.9.205:5984/trivial/ Method: POST + Request: http://192.168.9.205:5984/trivial/ Method: POST + Request: http://192.168.9.205:5984/trivial/ Method: POST + Request: http://192.168.9.205:5984/trivial/ Method: POST + Request: http://192.168.9.205:5984/trivial/ Method: POST + Request: http://192.168.9.205:5984/trivial/ Method: POST + Request: http://192.168.9.205:5984/trivial/ Method: POST + Saved 10 Cars with 170 hps each. + Request: http://192.168.9.205:5984/trivial/86a9d1ad306e204a037940c4fb0cbbe7 Method: PUT + Modified last Car with id 86a9d1ad306e204a037940c4fb0cbbe7 + Request: http://192.168.9.205:5984/trivial/86a9d1ad306e204a037940c4fb0cbbe7 Method: GET + Loaded last Car Saab 93 now with 400hps. + Request: http://192.168.9.205:5984/trivial/_all_docs?include_docs=true Method: GET + Loaded all Cars: 10 + Request: http://192.168.9.205:5984/trivial/1f6de464ae8034eb952b93105807f22c?rev1-208231211 Method: DELETE + Deleted car with id 1f6de464ae8034eb952b93105807f22c + Request: http://192.168.9.205:5984/trivial/44f9638877fc09e07bda6504a5bfd40d?rev1-345884075 Method: DELETE + Deleted car with id 44f9638877fc09e07bda6504a5bfd40d + Request: http://192.168.9.205:5984/trivial/591e0ef170154311aafa8a2a5fcbb310?rev1-2891194419 Method: DELETE + Deleted car with id 591e0ef170154311aafa8a2a5fcbb310 + Request: http://192.168.9.205:5984/trivial/7124fada0f90ed93f88168dc6c1c8b4f?rev1-2479256166 Method: DELETE + Deleted car with id 7124fada0f90ed93f88168dc6c1c8b4f + Request: http://192.168.9.205:5984/trivial/73115c4dd3b2bcd512412813dd04b901?rev1-3993011972 Method: DELETE + Deleted car with id 73115c4dd3b2bcd512412813dd04b901 + Request: http://192.168.9.205:5984/trivial/78a305543bff2f474cbcfe2ac667cc6d?rev1-1583012745 Method: DELETE + Deleted car with id 78a305543bff2f474cbcfe2ac667cc6d + Request: http://192.168.9.205:5984/trivial/86a9d1ad306e204a037940c4fb0cbbe7?rev2-1402499148 Method: DELETE + Deleted car with id 86a9d1ad306e204a037940c4fb0cbbe7 + Request: http://192.168.9.205:5984/trivial/9e647b114b9ba1c8a170ed1c1951260c?rev1-30425501 Method: DELETE + Deleted car with id 9e647b114b9ba1c8a170ed1c1951260c + Request: http://192.168.9.205:5984/trivial/bad56664dec8f3d1fd30db70510f7ff2?rev1-1765374009 Method: DELETE + Deleted car with id bad56664dec8f3d1fd30db70510f7ff2 + Request: http://192.168.9.205:5984/trivial/cbf0bb7f8ebc4aad02a2c90228d1076f?rev1-2425501185 Method: DELETE + Deleted car with id cbf0bb7f8ebc4aad02a2c90228d1076f + Request: http://192.168.9.205:5984/trivial Method: HEAD + Request: http://192.168.9.205:5984/trivial Method: DELETE + Deleted database + + +3. Look at couchTest.cs and make sure the tests are green, you may need to edit CochServer.cs with different default server ip. + +4. Have fun! + diff --git a/lib/Newtonsoft.Json.dll b/lib/Newtonsoft.Json.dll new file mode 100644 index 0000000..782e9c0 Binary files /dev/null and b/lib/Newtonsoft.Json.dll differ diff --git a/lib/nunit.framework.dll b/lib/nunit.framework.dll new file mode 100644 index 0000000..2a0a0aa Binary files /dev/null and b/lib/nunit.framework.dll differ diff --git a/samples/Trivial/Program.cs b/samples/Trivial/Program.cs new file mode 100644 index 0000000..7f175f0 --- /dev/null +++ b/samples/Trivial/Program.cs @@ -0,0 +1,136 @@ +using System; +using System.Diagnostics; +using Divan; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Trivial +{ + /// + /// A trivial example of using Divan. Requires a running CouchDB on localhost! + /// + /// Run using: + /// + /// Trivial.exe + /// + /// + class Program + { + static void Main(string[] args) { + string host = "localhost"; + int port = 5984; + + // Lets you see all HTTP requests made by Divan + Trace.Listeners.Add(new ConsoleTraceListener()); + + // Trivial parse of args to get host and port + switch (args.Length) { + case 0: + Console.WriteLine("Using localhost:5984"); + break; + case 1: + Console.WriteLine("Using " + args[0] + ":5984"); + host = args[0]; + break; + case 2: + Console.WriteLine("Using " + args[0] + ":" + args[1]); + host = args[0]; + port = int.Parse(args[1]); + break; + } + + // Get a server for default couch port 5984 on localhost + var server = new CouchServer(host, port); + Console.WriteLine("Created a CouchServer"); + + // Get (creates it if needed) a CouchDB database. + var db = server.GetDatabase("trivial"); + Console.WriteLine("Created database 'trivial'"); + + // Create and save 10 Cars with automatically allocated Ids by Couch + Car car = null; + for (int i = 0; i < 10; i++) + { + car = new Car("Saab", "93", 170); + db.SaveDocument(car); + } + Console.WriteLine("Saved 10 Cars with 170 hps each."); + + // Modify the last Car we saved... + car.HorsePowers = 400; + + // ...and save the change. + // We could also have used WriteDocument if we knew it was an existing doc + db.SaveDocument(car); + Console.WriteLine("Modified last Car with id " + car.Id); + + // Load a Car by known id, class to instantiate using generics + var sameCar = db.GetDocument(car.Id); + Console.WriteLine("Loaded last Car " + sameCar.Make + " " + sameCar.Model + " now with " + sameCar.HorsePowers + "hps."); + + // Load all Cars, class to instantiate using generics + var cars = db.QueryAllDocuments().IncludeDocuments().GetResult().Documents(); + Console.WriteLine("Loaded all Cars: " + cars.Count); + + // Delete all Cars one by one + foreach (var eachCar in cars) + { + db.DeleteDocument(eachCar); + Console.WriteLine("Deleted car with id " + eachCar.Id); + } + + // Delete the db itself + db.Delete(); + Console.WriteLine("Deleted database"); + } + + /// + /// The simplest way to deal with domain objects is to subclass CouchDocument + /// and inherit members Id and Rev. You will need to implement WriteJson/ReadJson. + /// + private class Car : CouchDocument + { + public string Make; + public string Model; + public int HorsePowers; + + public Car() + { + // This constructor is needed by Divan + } + + public Car(string make, string model, int hps) + { + Make = make; + Model = model; + HorsePowers = hps; + } + #region CouchDocument Members + + public override void WriteJson(JsonWriter writer) + { + // This will write id and rev + base.WriteJson(writer); + + writer.WritePropertyName("Make"); + writer.WriteValue(Make); + writer.WritePropertyName("Model"); + writer.WriteValue(Model); + writer.WritePropertyName("Hps"); + writer.WriteValue(HorsePowers); + } + + public override void ReadJson(JObject obj) + { + // This will read id and rev + base.ReadJson(obj); + + Make = obj["Make"].Value(); + Model = obj["Model"].Value(); + HorsePowers = obj["Hps"].Value(); + } + + #endregion + } + } +} diff --git a/samples/Trivial/Properties/AssemblyInfo.cs b/samples/Trivial/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..66856b0 --- /dev/null +++ b/samples/Trivial/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Trivial")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("MSC Konsult AB")] +[assembly: AssemblyProduct("Trivial")] +[assembly: AssemblyCopyright("Copyright © MSC Konsult AB 2009")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("05c213a1-cf72-45ce-8607-7827b32a1699")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/samples/Trivial/Trivial.csproj b/samples/Trivial/Trivial.csproj new file mode 100644 index 0000000..54b823d --- /dev/null +++ b/samples/Trivial/Trivial.csproj @@ -0,0 +1,69 @@ + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {CDCC7924-F227-46DC-B2E6-2BBE06B84AF2} + Exe + Properties + Trivial + Trivial + v3.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\..\lib\Newtonsoft.Json.dll + + + + 3.5 + + + 3.5 + + + 3.5 + + + + + + + + + + + {37AC0B66-5340-4B81-BC62-3EE80233A011} + Divan + + + + + \ No newline at end of file diff --git a/src/CouchBulkDeleteDocuments.cs b/src/CouchBulkDeleteDocuments.cs new file mode 100644 index 0000000..a6d6ae3 --- /dev/null +++ b/src/CouchBulkDeleteDocuments.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Divan +{ + /// + /// Only used as pseudo doc when doing bulk updates/inserts. + /// + public class CouchBulkDeleteDocuments : CouchBulkDocuments + { + public CouchBulkDeleteDocuments(IList docs) : base(docs) + { + } + + public override void WriteJson(JsonWriter writer) + { + writer.WritePropertyName("docs"); + writer.WriteStartArray(); + foreach (ICouchDocument doc in Docs) + { + writer.WriteStartObject(); + CouchDocument.WriteIdAndRev(doc, writer); + writer.WritePropertyName("_deleted"); + writer.WriteValue(true); + writer.WriteEndObject(); + } + writer.WriteEndArray(); + } + + public override void ReadJson(JObject obj) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/CouchBulkDocuments.cs b/src/CouchBulkDocuments.cs new file mode 100644 index 0000000..dc95009 --- /dev/null +++ b/src/CouchBulkDocuments.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Divan +{ + /// + /// Only used as psuedo doc when doing bulk updates/inserts. + /// + public class CouchBulkDocuments : ICanJson + { + public CouchBulkDocuments(IList docs) + { + Docs = docs; + } + + public IList Docs { get; private set; } + + #region ICouchBulk Members + + public int Count() + { + return Docs.Count; + } + + public virtual void WriteJson(JsonWriter writer) + { + writer.WritePropertyName("docs"); + writer.WriteStartArray(); + foreach (ICouchDocument doc in Docs) + { + writer.WriteStartObject(); + doc.WriteJson(writer); + writer.WriteEndObject(); + } + writer.WriteEndArray(); + } + + public virtual void ReadJson(JObject obj) + { + throw new NotImplementedException(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/CouchBulkKeys.cs b/src/CouchBulkKeys.cs new file mode 100644 index 0000000..2ac3787 --- /dev/null +++ b/src/CouchBulkKeys.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Divan +{ + /// + /// Only used as psuedo doc when doing bulk reads. + /// + public class CouchBulkKeys : ICanJson + { + public CouchBulkKeys(IEnumerable ids) + { + Ids = ids.ToArray(); + } + + public CouchBulkKeys() + { + } + + public CouchBulkKeys(string[] ids) + { + Ids = ids; + } + + public string[] Ids { get; set; } + + #region ICouchBulk Members + + public virtual void WriteJson(JsonWriter writer) + { + writer.WritePropertyName("keys"); + writer.WriteStartArray(); + foreach (string id in Ids) + { + writer.WriteValue(id); + } + writer.WriteEndArray(); + } + + public virtual void ReadJson(JObject obj) + { + throw new NotImplementedException(); + } + + public int Count() + { + return Ids.Count(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/CouchConflictException.cs b/src/CouchConflictException.cs new file mode 100644 index 0000000..0573066 --- /dev/null +++ b/src/CouchConflictException.cs @@ -0,0 +1,14 @@ +using System; + +namespace Divan +{ + /// + /// Represents a CouchDB HTTP 409 conflict. + /// + public class CouchConflictException : Exception + { + public CouchConflictException(string msg, Exception e) : base(msg, e) + { + } + } +} \ No newline at end of file diff --git a/src/CouchDatabase.cs b/src/CouchDatabase.cs new file mode 100644 index 0000000..672a67b --- /dev/null +++ b/src/CouchDatabase.cs @@ -0,0 +1,615 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Net; +using Newtonsoft.Json.Linq; + +namespace Divan +{ + /// + /// A CouchDatabase corresponds to a named CouchDB database in a specific CouchServer. + /// This is the main API to work with CouchDB. One useful approach is to create your own subclasses + /// for your different databases. + /// + public class CouchDatabase + { + public CouchDatabase() : this("default", new CouchServer()) + { + } + + public CouchDatabase(string name) : this(name, new CouchServer()) + { + } + + public CouchDatabase(CouchServer server) : this("default", server) + { + } + + public CouchDatabase(string name, CouchServer server) + { + Name = server.DatabasePrefix + name; + Server = server; + } + + public CouchServer Server { get; set; } + public string Name { get; set; } + + public CouchRequest Request() + { + return new CouchRequest(this); + } + + public CouchRequest Request(string path) + { + return (new CouchRequest(this)).Path(path); + } + + public int CountDocuments() + { + return (Request().Parse())["doc_count"].Value(); + } + + public CouchRequest RequestAllDocuments() + { + return Request("_all_docs"); + } + + /// + /// Read all documents in database. + /// + /// List of documents in database. + public IList GetAllDocuments() + { + var list = new List(); + JObject json = Request("_all_docs").Parse(); + foreach (JObject row in json["rows"]) + { + list.Add(new CouchDocument(row["id"].ToString(), (row["value"])["rev"].ToString())); + } + return list; + } + + /// + /// Initialize CouchDB database by loading design documents into it. + /// Override in subclasses. + /// + public virtual void Initialize() + { + // Nothing by default + } + + public bool Exists() + { + return Server.HasDatabase(Name); + } + + /// + /// Check first if database exists, and if it does not - create it and initialize it. + /// + public void Create() + { + if (!Exists()) + { + Server.CreateDatabase(Name); + Initialize(); + } + } + + public void Delete() + { + if (Exists()) + { + Server.DeleteDatabase(Name); + } + } + + /// + /// Write a CouchDocument, it may already exist in db and will then be overwritten. + /// + /// Document as Json + /// Document identifier + /// A new CouchDocument + public ICouchDocument WriteDocument(string json, string documentId) + { + return WriteDocument(new CouchDocument(json, documentId)); + } + + /// + /// Write a CouchDocument or ICouchDocument, it may already exist in db and will then be overwritten. + /// + /// Couch document + /// Couch Document with new Rev set. + /// This relies on the document to already have an id. + public ICouchDocument + WriteDocument(ICouchDocument document) + { + return WriteDocument(document, false); + } + + /// + /// This is a convenience method that creates or writes a ICouchDocument depending on if + /// it has an id or not. If it does not have an id we create the document and let CouchDB allocate + /// an id. If it has an id we use WriteDocument which will overwrite the document in CouchDB. + /// + /// Couch document + /// Couch Document with new Rev set and possibly an Id set. + public ICouchDocument SaveDocument(ICouchDocument document) + { + if (document.Id == null) + { + return CreateDocument(document); + } + return WriteDocument(document); + } + + /// + /// Write a CouchDocument or ICouchDocument, it may already exist in db and will then be overwritten. + /// + /// Couch document + /// True if we don't want to wait for flush (commit). + /// Couch Document with new Rev set. + /// This relies on the document to already have an id. + public ICouchDocument WriteDocument(ICouchDocument document, bool batch) + { + if (document.Id == null) + { + throw CouchException.Create( + "Failed to write document using PUT because it lacks an id, use CreateDocument instead to let CouchDB generate an id"); + } + JObject result = + Request(document.Id).Query(batch ? "?batch=ok" : null).Data(CouchDocument.WriteJson(document)).Put().Check("Failed to write document").Result(); + document.Id = result["id"].Value(); // Not really neeed + document.Rev = result["rev"].Value(); + + return document; + } + + /// + /// Add an attachment to an existing ICouchDocument, it may already exist in db and will then be overwritten. + /// + /// Couch document + /// Binary data as string + /// The MIME type for the attachment. + /// The document. + /// This relies on the document to already have an id. + public ICouchDocument WriteAttachment(ICouchDocument document, string attachment, string mimeType) + { + if (document.Id == null) + { + throw CouchException.Create( + "Failed to add attachment to document using PUT because it lacks an id"); + } + + JObject result = + Request(document.Id + "/attachment").Query("?rev=" + document.Rev).Data(attachment).MimeType(mimeType).Put().Check("Failed to write attachment") + .Result(); + document.Id = result["id"].Value(); // Not really neeed + document.Rev = result["rev"].Value(); + + return document; + } + + /// + /// Read a ICouchDocument with an id even if it has not changed revision. + /// + /// Document to fill. + public void ReadDocument(ICouchDocument document) + { + document.ReadJson(ReadDocument(document.Id)); + } + + /// + /// Read the attachment for an ICouchDocument. + /// + /// Document to read. + public string ReadAttachment(ICouchDocument document) + { + return ReadAttachment(document.Id); + } + + /// + /// First use HEAD to see if it has indeed changed. + /// + /// Document to fill. + public void FetchDocumentIfChanged(ICouchDocument document) + { + if (HasDocumentChanged(document)) + { + ReadDocument(document); + } + } + + /// + /// Read a CouchDocument or ICouchDocument, this relies on the document to obviously have an id. + /// We also check the revision so that we can avoid parsing JSON if the document is unchanged. + /// + /// Document to fill. + public void ReadDocumentIfChanged(ICouchDocument document) + { + JObject result = Request(document.Id).Etag(document.Rev).Parse(); + if (result == null) + { + return; + } + document.ReadJson(result); + } + + /// + /// Read a couch document given an id, this method does not have enough information to do caching. + /// + /// Document identifier + /// Document Json as JObject + public JObject ReadDocument(string documentId) + { + try + { + return Request(documentId).Parse(); + } + catch (WebException e) + { + throw CouchException.Create("Failed to read document", e); + } + } + + /// + /// Read a couch document given an id, this method does not have enough information to do caching. + /// + /// Document identifier + /// Document Json as string + public string ReadDocumentString(string documentId) + { + try + { + return Request(documentId).String(); + } + catch (WebException e) + { + throw CouchException.Create("Failed to read document: " + e.Message, e); + } + } + + /// + /// Read a couch attachment given a document id, this method does not have enough information to do caching. + /// + /// Document identifier + /// Document attachment + public string ReadAttachment(string documentId) + { + try + { + return Request(documentId + "/attachment").String(); + } + catch (WebException e) + { + throw CouchException.Create("Failed to read document: " + e.Message, e); + } + } + + /// + /// Create a CouchDocument given JSON as a string. Uses POST and CouchDB will allocate a new id. + /// + /// Json data to store. + /// Couch document with data, id and rev set. + /// POST which may be problematic in some environments. + public CouchJsonDocument CreateDocument(string json) + { + return (CouchJsonDocument) CreateDocument(new CouchJsonDocument(json)); + } + + /// + /// Create a given ICouchDocument in CouchDB. Uses POST and CouchDB will allocate a new id and overwrite any existing id. + /// + /// Document to store. + /// Document with Id and Rev set. + /// POST which may be problematic in some environments. + public ICouchDocument CreateDocument(ICouchDocument document) + { + try + { + JObject result = Request().Data(CouchDocument.WriteJson(document)).Post().Check("Failed to create document").Result(); + document.Id = result["id"].Value(); + document.Rev = result["rev"].Value(); + return document; + } + catch (WebException e) + { + throw CouchException.Create("Failed to create document", e); + } + } + + /// + /// Create or update a list of ICouchDocuments in CouchDB. Uses POST and CouchDB will + /// allocate new ids if the documents lack them. + /// + /// List of documents to store. + /// POST may be problematic in some environments. + public void SaveDocuments(IList documents, bool allOrNothing) + { + var bulk = new CouchBulkDocuments(documents); + try + { + var result = + Request("_bulk_docs").Data(CouchDocument.WriteJson(bulk)).Query("?all_or_nothing=" + allOrNothing.ToString().ToLower()).PostJson().Parse + (); + for (int i = 0; i < documents.Count; i++) + { + documents[i].Id = (result[i])["id"].Value(); + documents[i].Rev = (result[i])["rev"].Value(); + } + } + catch (WebException e) + { + throw CouchException.Create("Failed to create bulk documents", e); + } + } + + /// + /// Create or updates documents in bulk fashion, chunk wise. Optionally access given view + /// after each chunk to trigger reindexing. + /// + /// List of documents to store. + /// Number of documents to store per "POST" + /// List of views to touch per chunk. + public void SaveDocuments(IList documents, int chunkCount, List views, bool allOrNothing) + { + var chunk = new List(chunkCount); + int counter = 0; + + foreach (ICouchDocument doc in documents) + { + // Do we have a chunk ready to create? + if (counter == chunkCount) + { + counter = 0; + SaveDocuments(chunk, allOrNothing); + TouchViews(views); + /* Skipping separate thread for now, ASP.Net goes bonkers... + (new Thread( + () => GetView(designDocumentName, viewName, "")) + { + Name = "View access in background", Priority = ThreadPriority.BelowNormal + }).Start(); */ + + chunk = new List(chunkCount); + } + counter++; + chunk.Add(doc); + } + + SaveDocuments(chunk, allOrNothing); + TouchViews(views); + } + + public void TouchViews(List views) + { + var timer = new Stopwatch(); + if (views != null) + { + foreach (CouchViewDefinition view in views) + { + if (view != null) + { + timer.Reset(); + timer.Start(); + view.Touch(); + timer.Stop(); + Trace.WriteLine("Update view " + view.Path() + ":" + timer.ElapsedMilliseconds + " ms"); + } + } + } + } + + /// + /// Create documents in bulk fashion, chunk wise. + /// + /// List of documents to store. + /// Number of documents to store per "POST" + public void SaveDocuments(IList documents, int chunkCount, bool allOrNothing) + { + SaveDocuments(documents, chunkCount, null, allOrNothing); + } + + /// + /// Get multiple documents. + /// + /// List of documents to get. + public IList GetDocuments(IList documentIds) where T : ICouchDocument, new() + { + return GetDocuments(documentIds.ToArray()); + } + + public IList GetDocuments(IList documentIds) + { + return GetDocuments(documentIds); + } + + public IList GetDocuments(string[] documentIds) + { + return GetDocuments(documentIds); + } + + public IList GetDocuments(string[] documentIds) where T : ICouchDocument, new() + { + var bulk = new CouchBulkKeys(documentIds); + return QueryAllDocuments().Data(CouchDocument.WriteJson(bulk)).IncludeDocuments().GetResult().Documents(); + } + + public T GetDocument(string documentId) where T : ICouchDocument, new() + { + var doc = new T {Id = documentId}; + try + { + ReadDocument(doc); + } + catch (CouchNotFoundException) + { + return default(T); + } + return doc; + } + + public CouchJsonDocument GetDocument(string documentId) + { + try + { + try + { + return new CouchJsonDocument(Request(documentId).Parse()); + } + catch (WebException e) + { + throw CouchException.Create("Failed to get document", e); + } + } + catch (CouchNotFoundException) + { + return null; + } + } + + public CouchQuery Query(string designName, string viewName) + { + return Query(new CouchViewDefinition(viewName, new DesignCouchDocument(designName, this))); + } + + public CouchQuery Query(CouchViewDefinition view) + { + return new CouchQuery(view); + } + + public CouchQuery QueryAllDocuments() + { + return Query(null, "_all_docs"); + } + + public void TouchView(string designDocumentId, string viewName) + { + Query(designDocumentId, viewName).Limit(0).GetResult(); + } + + public void DeleteDocument(ICouchDocument document) + { + DeleteDocument(document.Id, document.Rev); + } + + public ICouchDocument DeleteAttachment(ICouchDocument document) + { + JObject result = Request(document.Id + "/attachment").Query("?rev=" + document.Rev).Delete().Check("Failed to delete attachment").Result(); + document.Id = result["id"].Value(); // Not really neeed + document.Rev = result["rev"].Value(); + return document; + } + + public void DeleteAttachment(string id, string rev) + { + Request(id + "/attachment").Query("?rev=" + rev).Delete().Check("Failed to delete attachment"); + } + + public void DeleteDocument(string id, string rev) + { + Request(id).Query("?rev=" + rev).Delete().Check("Failed to delete document"); + } + + /// + /// Delete documents in bulk fashion. + /// + /// List of documents to delete. + public void DeleteDocuments(IList documents) + { + DeleteDocuments(documents.ToArray()); + } + + /// + /// Delete documents in key range. This method needs to retrieve + /// revisions and then use them to post a bulk delete. Couch can not + /// delete documents without being told about their revisions. + /// + public void DeleteDocuments(string startKey, string endKey) + { + IList docs = QueryAllDocuments().StartKey(startKey).EndKey(endKey).GetResult().RowDocuments(); + DeleteDocuments(docs.ToArray()); + } + + /// + /// Delete documents in bulk fashion. + /// + /// Array of documents to delete. + public void DeleteDocuments(ICouchDocument[] documents) + { + DeleteDocuments(new CouchBulkDeleteDocuments(documents)); + } + + /// + /// Delete documents in bulk fashion. + /// + public void DeleteDocuments(ICanJson bulk) + { + try + { + var result = Request("_bulk_docs").Data(CouchDocument.WriteJson(bulk)).PostJson().Parse(); + for (int i = 0; i < result.Count(); i++) + { + //documents[i].id = (result[i])["id"].Value(); + //documents[i].rev = (result[i])["rev"].Value(); + if ((result[i])["error"] != null) + { + throw CouchException.Create(string.Format(CultureInfo.InvariantCulture, + "Document with id {0} was not deleted: {1}: {2}", + (result[i])["id"].Value(), (result[i])["error"], (result[i])["reason"])); + } + } + } + catch (WebException e) + { + throw CouchException.Create("Failed to bulk delete documents", e); + } + } + + public bool HasDocument(ICouchDocument document) + { + return HasDocument(document.Id); + } + + public bool HasAttachment(ICouchDocument document) + { + return HasAttachment(document.Id); + } + + public bool HasDocumentChanged(ICouchDocument document) + { + return HasDocumentChanged(document.Id, document.Rev); + } + + public bool HasDocumentChanged(string documentId, string rev) + { + return Request(documentId).Head().Send().Etag() != rev; + } + + public bool HasDocument(string documentId) + { + try + { + Request(documentId).Head().Send(); + return true; + } + catch (WebException) + { + return false; + } + } + + public bool HasAttachment(string documentId) + { + try + { + Request(documentId + "/attachment").Head().Send(); + return true; + } + catch (WebException) + { + return false; + } + } + } +} \ No newline at end of file diff --git a/src/CouchDocument.cs b/src/CouchDocument.cs new file mode 100644 index 0000000..279607d --- /dev/null +++ b/src/CouchDocument.cs @@ -0,0 +1,114 @@ +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Divan +{ + /// + /// This is a base class that domain objects can inherit in order to get + /// Id and Rev instance variables. You can also implement ICouchDocument yourself if + /// you are not free to pick this class as your base. Some static methods to read and write + /// CouchDB documents are also kept here. + /// + /// See sample subclasses to understand how to use this class + /// + public class CouchDocument : ICouchDocument + { + public CouchDocument(string id, string rev) + { + Id = id; + Rev = rev; + } + + public CouchDocument(string id) + { + Id = id; + } + + public CouchDocument() + { + } + + public CouchDocument(IDictionary doc) + : this(doc["_id"].Value(), doc["_rev"].Value()) + { + } + + #region ICouchDocument Members + + public string Id { get; set; } + public string Rev { get; set; } + + public virtual void WriteJson(JsonWriter writer) + { + WriteIdAndRev(this, writer); + } + + public virtual void ReadJson(JObject obj) + { + ReadIdAndRev(this, obj); + } + + #endregion + + public void WriteJsonObject(JsonWriter writer) + { + writer.WriteStartObject(); + WriteJson(writer); + writer.WriteEndObject(); + } + + public static string WriteJson(ICanJson doc) + { + var sb = new StringBuilder(); + using (JsonWriter jsonWriter = new JsonTextWriter(new StringWriter(sb, CultureInfo.InvariantCulture))) + { + //jsonWriter.Formatting = Formatting.Indented; + jsonWriter.WriteStartObject(); + doc.WriteJson(jsonWriter); + jsonWriter.WriteEndObject(); + string result = sb.ToString(); + return result; + } + } + + public static void WriteIdAndRev(ICouchDocument doc, JsonWriter writer) + { + if (doc.Id != null) + { + writer.WritePropertyName("_id"); + writer.WriteValue(doc.Id); + } + if (doc.Rev != null) + { + writer.WritePropertyName("_rev"); + writer.WriteValue(doc.Rev); + } + } + + public static void ReadIdAndRev(ICouchDocument doc, JObject obj) + { + doc.Id = obj["_id"].Value(); + doc.Rev = obj["_rev"].Value(); + } + + public static void ReadIdAndRev(ICouchDocument doc, JsonReader reader) + { + reader.Read(); + if (reader.TokenType == JsonToken.PropertyName && (reader.Value as string == "_id")) + { + reader.Read(); + doc.Id = reader.Value as string; + } + reader.Read(); + if (reader.TokenType == JsonToken.PropertyName && (reader.Value as string == "_rev")) + { + reader.Read(); + doc.Rev = reader.Value as string; + } + } + } +} \ No newline at end of file diff --git a/src/CouchException.cs b/src/CouchException.cs new file mode 100644 index 0000000..55c057c --- /dev/null +++ b/src/CouchException.cs @@ -0,0 +1,62 @@ +using System; +using System.Globalization; +using System.Net; +using System.Runtime.Serialization; + +namespace Divan +{ + /// + /// All Exceptions thrown inside Divan uses this class, MOST of these wrap a WebException + /// and we extract the HttpStatusCode to make it easily accessible. + /// + [Serializable] + public class CouchException : Exception + { + public HttpStatusCode StatusCode; + + public CouchException() + { + } + + public CouchException(string message) + : base(message) + { + } + + public CouchException(string message, Exception innerException) : base(message, innerException) + { + } + + protected CouchException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + public static Exception Create(string message) + { + return new CouchException(message); + } + + public static Exception Create(string message, WebException e) + { + string msg = string.Format(CultureInfo.InvariantCulture, message + ": {0}", e.Message); + if (e.Response != null) + { + // Pick out status code + HttpStatusCode code = ((HttpWebResponse) e.Response).StatusCode; + + // Create any specific exceptions we care to use + if (code == HttpStatusCode.Conflict) + { + return new CouchConflictException(msg, e); + } + if (code == HttpStatusCode.NotFound) + { + return new CouchNotFoundException(msg, e); + } + } + + // Fall back on generic CouchException + return new CouchException(msg, e); + } + } +} \ No newline at end of file diff --git a/src/CouchGenericViewResult.cs b/src/CouchGenericViewResult.cs new file mode 100644 index 0000000..8af8c18 --- /dev/null +++ b/src/CouchGenericViewResult.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +namespace Divan +{ + /// + /// This is a view result from a CouchQuery that can return CouchDocuments for + /// resulting documents (include_docs) and/or ICanJson documents for the + /// result values. A value returned from a CouchDB view does not need to be + /// a CouchDocument. + /// + public class CouchGenericViewResult : CouchViewResult + { + /// + /// Return all found values as documents of given type + /// + /// Type of value. + /// All found values. + public IList ValueDocuments() where T : ICanJson, new() + { + return RetrieveDocuments("value"); + } + + /// + /// Return first value found as document of given type. + /// + /// Type of value + /// First value found or null if not found. + public T ValueDocument() where T : ICanJson, new() + { + return RetrieveDocument("value"); + } + + /// + /// Return all found docs as documents of given type + /// + /// Type of documents. + /// List of documents found. + public IList Documents() where T : ICouchDocument, new() + { + return RetrieveDocuments("doc"); + } + + /// + /// Return first document found as document of given type + /// + /// Type of document + /// First document found or null if not found. + public T Document() where T : ICouchDocument, new() + { + return RetrieveDocument("doc"); + } + + protected virtual IList RetrieveDocuments(string docOrValue) where T : ICanJson, new() + { + var list = new List(); + foreach (JToken row in Rows()) + { + var doc = new T(); + doc.ReadJson(row[docOrValue].Value()); + list.Add(doc); + } + return list; + } + + protected virtual T RetrieveDocument(string docOrValue) where T : ICanJson, new() + { + foreach (JToken row in Rows()) + { + var doc = new T(); + doc.ReadJson(row[docOrValue].Value()); + return doc; + } + return default(T); + } + + public IList RowDocuments() + { + return RowDocuments(); + } + + public IList RowDocuments() where T : ICanJson, new() + { + var list = new List(); + foreach (JObject row in Rows()) + { + var doc = new T(); + doc.ReadJson(row); + list.Add(doc); + } + return list; + } + } +} \ No newline at end of file diff --git a/src/CouchJSONDocument.cs b/src/CouchJSONDocument.cs new file mode 100644 index 0000000..dae5662 --- /dev/null +++ b/src/CouchJSONDocument.cs @@ -0,0 +1,86 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Divan +{ + /// + /// A CouchDocument that holds its contents as a parsed JObject DOM which can be used + /// as a "light weight" base document instead of CouchDocument. + /// The _id and _rev are held inside the JObject. + /// + public class CouchJsonDocument : ICouchDocument + { + public CouchJsonDocument(string json, string id, string rev) + { + Obj = JObject.Parse(json); + Id = id; + Rev = rev; + } + + public CouchJsonDocument(string json, string id) + { + Obj = JObject.Parse(json); + Id = id; + } + + public CouchJsonDocument(string json) + { + Obj = JObject.Parse(json); + } + + public CouchJsonDocument(JObject doc) + { + Obj = doc; + } + + public CouchJsonDocument() + { + Obj = new JObject(); + } + + public JObject Obj { get; set; } + + #region ICouchDocument Members + + public void WriteJson(JsonWriter writer) + { + foreach (JToken token in Obj.Children()) + { + token.WriteTo(writer); + } + } + + // Presume that Obj has _id and _rev + public void ReadJson(JObject obj) + { + Obj = obj; + } + + public string Rev + { + get + { + if (Obj["_rev"] == null) + { + return null; + } + return Obj["_rev"].Value(); + } + set { Obj["_rev"] = JToken.FromObject(value); } + } + public string Id + { + get + { + if (Obj["_id"] == null) + { + return null; + } + return Obj["_id"].Value(); + } + set { Obj["_id"] = JToken.FromObject(value); } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/CouchNotFoundException.cs b/src/CouchNotFoundException.cs new file mode 100644 index 0000000..63c9903 --- /dev/null +++ b/src/CouchNotFoundException.cs @@ -0,0 +1,14 @@ +using System; + +namespace Divan +{ + /// + /// Represents a HttpStatusCode of 404, document not found. + /// + public class CouchNotFoundException : Exception + { + public CouchNotFoundException(string msg, Exception e) : base(msg, e) + { + } + } +} \ No newline at end of file diff --git a/src/CouchPermanentViewResult.cs b/src/CouchPermanentViewResult.cs new file mode 100644 index 0000000..66731b7 --- /dev/null +++ b/src/CouchPermanentViewResult.cs @@ -0,0 +1,9 @@ +namespace Divan +{ + /// + /// This is a view result from a CouchQuery on a permanent CouchDB view. + /// + public class CouchPermanentViewResult : CouchViewResult + { + } +} \ No newline at end of file diff --git a/src/CouchQuery.cs b/src/CouchQuery.cs new file mode 100644 index 0000000..d7f9600 --- /dev/null +++ b/src/CouchQuery.cs @@ -0,0 +1,212 @@ +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +namespace Divan +{ + /// + /// A view query with all its options. A CouchQuery is constructed to hold all query options that + /// CouchDB views support and to support ETag caching. + /// A CouchQuery object can be executed multiple times, holds the last result, the ETag for it, + /// and a reference to the CouchDatabase object used to perform the query. + /// + public class CouchQuery + { + public readonly CouchViewDefinition View; + + // Special options + public bool checkETagUsingHead; + public Dictionary Options = new Dictionary(); + public string postData; + public CouchViewResult Result; + + public CouchQuery(CouchViewDefinition view) + { + View = view; + } + + public void ClearOptions() + { + Options = new Dictionary(); + } + + public CouchQuery Data(string data) + { + postData = data; + return this; + } + + public CouchQuery Key(string value) + { + Options["key"] = "\"" + value + "\""; + return this; + } + + public CouchQuery Key(params object[] value) + { + Options["key"] = JArray.FromObject(value).ToString(); + return this; + } + + public CouchQuery StartKey(string value) + { + Options["startkey"] = "\"" + value + "\""; + return this; + } + + public CouchQuery StartKey(params object[] value) + { + Options["startkey"] = JArray.FromObject(value).ToString(); + return this; + } + + public CouchQuery StartKeyDocumentId(string value) + { + Options["startkey_docid"] = value; + return this; + } + + public CouchQuery EndKey(string value) + { + Options["endkey"] = "\"" + value + "\""; + return this; + } + + public CouchQuery EndKey(params object[] value) + { + Options["endkey"] = JArray.FromObject(value).ToString(); + return this; + } + + public CouchQuery EndKeyDocumentId(string value) + { + Options["endkey_docid"] = value; + return this; + } + + public CouchQuery Limit(int value) + { + Options["limit"] = value.ToString(); + return this; + } + + public CouchQuery Stale() + { + Options["stale"] = "ok"; + return this; + } + + public CouchQuery Descending() + { + Options["descending"] = "true"; + return this; + } + + public CouchQuery Skip(int value) + { + Options["skip"] = value.ToString(); + return this; + } + + public CouchQuery Group() + { + Options["group"] = "true"; + return this; + } + + public CouchQuery GroupLevel(int value) + { + Options["group_level"] = value.ToString(); + return this; + } + + public CouchQuery Reduce() + { + Options["reduce"] = "true"; + return this; + } + + public CouchQuery IncludeDocuments() + { + Options["include_docs"] = "true"; + return this; + } + + /// + /// Tell this query to do a HEAD request first to see + /// if ETag has changed and only then do the full request. + /// This is only interesting if you are reusing this query object. + /// + public CouchQuery CheckETagUsingHead() + { + checkETagUsingHead = true; + return this; + } + + public CouchGenericViewResult GetResult() + { + return GetResult(); + } + + public bool IsCachedAndValid() + { + // If we do not have a result it is not cached + if (Result == null) + { + return false; + } + CouchRequest req = View.Request().QueryOptions(Options); + req.Etag(Result.etag); + return req.Head().Send().IsETagValid(); + } + + public string String() + { + CouchRequest req = View.Request().QueryOptions(Options); + + if (postData != null) + { + req.Data(postData).Post(); + } + + return req.String(); + } + + public T GetResult() where T : CouchViewResult, new() + { + CouchRequest req = View.Request().QueryOptions(Options); + + if (postData != null) + { + req.Data(postData).Post(); + } + + if (Result == null) + { + Result = new T(); + } + else + { + // Tell the request what we already have + req.Etag(Result.etag); + if (checkETagUsingHead) + { + // Make a HEAD request to avoid transfer of data + if (req.Head().Send().IsETagValid()) + { + return (T) Result; + } + // Set back to GET before proceeding below + req.Get(); + } + } + + JObject json = req.Parse(); + if (json != null) // ETag did not match, view has changed + { + Result.Result(json); + Result.etag = req.Etag(); + } + return (T) Result; + } + } +} \ No newline at end of file diff --git a/src/CouchQueryDocument.cs b/src/CouchQueryDocument.cs new file mode 100644 index 0000000..e14ffef --- /dev/null +++ b/src/CouchQueryDocument.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json.Linq; + +namespace Divan +{ + /// + /// This is used to hold only metadata about a document retrieved from view queries. + /// + public class CouchQueryDocument : CouchDocument + { + public string Key { get; set; } + + public override void ReadJson(JObject obj) + { + Id = obj["id"].Value(); + Key = obj["key"].Value(); + Rev = (obj["value"].Value())["rev"].Value(); + } + } +} \ No newline at end of file diff --git a/src/CouchRequest.cs b/src/CouchRequest.cs new file mode 100644 index 0000000..8aab17f --- /dev/null +++ b/src/CouchRequest.cs @@ -0,0 +1,291 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Net; +using System.Text; +using System.Web; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Divan +{ + /// + /// A CouchDB HTTP request with all its options. This is where we do the actual HTTP requests to CouchDB. + /// + public class CouchRequest + { + private readonly CouchDatabase db; + private readonly CouchServer server; + private string etag, etagToCheck; + public Dictionary headers = new Dictionary(); + + // Query options + public string method = "GET"; // PUT, DELETE, POST, HEAD + public string mimeType; + public string path; + public string postData; + public string query; + + public JToken result; + + public CouchRequest(CouchServer server) + { + this.server = server; + } + + public CouchRequest(CouchDatabase db) + { + server = db.Server; + this.db = db; + } + + public CouchRequest Etag(string value) + { + etagToCheck = value; + headers["If-Modified"] = value; + return this; + } + + public CouchRequest Path(string name) + { + path = name; + return this; + } + + public CouchRequest Query(string name) + { + query = name; + return this; + } + + public CouchRequest QueryOptions(ICollection> options) + { + if (options == null || options.Count == 0) + { + return this; + } + + var sb = new StringBuilder(); + sb.Append("?"); + foreach (var q in options) + { + if (sb.Length > 1) + { + sb.Append("&"); + } + sb.Append(HttpUtility.UrlEncode(q.Key)); + sb.Append("="); + sb.Append(HttpUtility.UrlEncode(q.Value)); + } + + return Query(sb.ToString()); + } + + public CouchRequest Head() + { + method = "HEAD"; + return this; + } + + public CouchRequest PostJson() + { + MimeTypeJson(); + return Post(); + } + + public CouchRequest Post() + { + method = "POST"; + return this; + } + + public CouchRequest Get() + { + method = "GET"; + return this; + } + + public CouchRequest Put() + { + method = "PUT"; + return this; + } + + public CouchRequest Delete() + { + method = "DELETE"; + return this; + } + + public CouchRequest Data(string data) + { + postData = data; + return this; + } + + public CouchRequest MimeType(string type) + { + mimeType = type; + return this; + } + + public CouchRequest MimeTypeJson() + { + MimeType("application/json"); + return this; + } + + public JObject Result() + { + return (JObject) result; + } + + public T Result() where T : JToken + { + return (T) result; + } + + public string Etag() + { + return etag; + } + + public CouchRequest Check(string message) + { + try + { + if (result == null) + { + Parse(); + } + if (!result["ok"].Value()) + { + throw CouchException.Create(string.Format(CultureInfo.InvariantCulture, message + ": {0}", result)); + } + return this; + } + catch (WebException e) + { + throw CouchException.Create(message, e); + } + } + + private HttpWebRequest GetRequest() + { + Uri requestUri = new UriBuilder("http", server.Host, server.Port, ((db != null) ? db.Name + "/" : "") + path, query).Uri; + var request = WebRequest.Create(requestUri) as HttpWebRequest; + if (request == null) + { + throw CouchException.Create("Failed to create request"); + } + request.Timeout = 3600000; // 1 hour. May use System.Threading.Timeout.Infinite; + request.Method = method; + + if (mimeType != null) + { + request.ContentType = mimeType; + } + + if (postData != null) + { + byte[] bytes = Encoding.UTF8.GetBytes(postData); + request.ContentLength = bytes.Length; + using (Stream ps = request.GetRequestStream()) + { + ps.Write(bytes, 0, bytes.Length); + ps.Close(); + } + } + + Trace.WriteLine(string.Format(CultureInfo.InvariantCulture, "Request: {0} Method: {1}", requestUri, method)); + return request; + } + + public JObject Parse() + { + return Parse(); + } + + public T Parse() where T : JToken + { + //var timer = new Stopwatch(); + //timer.Start(); + using (WebResponse response = GetResponse()) + { + using (Stream stream = response.GetResponseStream()) + { + using (var reader = new StreamReader(stream)) + { + using (var textReader = new JsonTextReader(reader)) + { + PickETag(response); + if (etagToCheck != null) + { + if (IsETagValid()) + { + return null; + } + } + result = JToken.ReadFrom(textReader); // We know it is a top level JSON JObject. + } + } + } + } + //timer.Stop(); + //Trace.WriteLine("Time for Couch HTTP & JSON PARSE: " + timer.ElapsedMilliseconds); + return (T) result; + } + + private void PickETag(WebResponse response) + { + etag = response.Headers["ETag"]; + if (etag != null) + { + etag = etag.EndsWith("\"") ? etag.Substring(1, etag.Length - 2) : etag; + } + } + + /// + /// Return the request as a plain string instead of trying to parse it. + /// + public string String() + { + using (WebResponse response = GetResponse()) + { + using (var reader = new StreamReader(response.GetResponseStream())) + { + PickETag(response); + if (etagToCheck != null) + { + if (IsETagValid()) + { + return null; + } + } + return reader.ReadToEnd(); + } + } + } + + private WebResponse GetResponse() + { + return GetRequest().GetResponse(); + } + + public CouchRequest Send() + { + using (WebResponse response = GetResponse()) + { + PickETag(response); + return this; + } + } + + public bool IsETagValid() + { + return etagToCheck == etag; + } + } +} \ No newline at end of file diff --git a/src/CouchServer.cs b/src/CouchServer.cs new file mode 100644 index 0000000..561c6ed --- /dev/null +++ b/src/CouchServer.cs @@ -0,0 +1,180 @@ +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Net; +using System.Text.RegularExpressions; +using Newtonsoft.Json; + +namespace Divan +{ + /// + /// A CouchServer is simply a communication end point holding a hostname and a port number to talk to. + /// It has an API to list, lookup, create or delete CouchDB "databases" in the CouchDB server. + /// One nice approach is to create a specific subclass that knows about its databases. + /// DatabasePrefix can be used to separate all databases created from other CouchDB databases. + /// + public class CouchServer + { + private const string DefaultHost = "192.168.9.205"; + private const int DefaultPort = 5984; + private readonly JsonSerializer serializer = new JsonSerializer(); + + public readonly string Host; + public readonly int Port; + + public string DatabasePrefix = ""; // Used by databases to prefix their names + + public CouchServer(string host, int port) + { + Host = host; + Port = port; + } + + public CouchServer(string host) + : this(host, DefaultPort) + { + } + + public CouchServer() + : this(DefaultHost, DefaultPort) + { + } + + public string ServerName + { + get { return Host + ":" + Port; } + } + + public CouchRequest Request() + { + return new CouchRequest(this); + } + + public bool HasDatabase(string name) + { + //return GetDatabaseNames().Contains(name); // This is too slow when we have thousands of dbs!!! + try + { + Request().Path(name).Head().Send(); + return true; + } + catch (WebException) + { + return false; + } + } + + /// + /// Get a CouchDatabase with given name. We create + /// the database if needed. + /// + public CouchDatabase GetDatabase(string name) + { + var db = new CouchDatabase(name, this); + db.Create(); + return db; + } + + /// + /// Get specialized subclass of CouchDatabase with given name. + /// We check if the database exists and delete it if it does, + /// then we recreate it. + /// + public CouchDatabase GetNewDatabase(string name) + { + var db = new CouchDatabase(name, this); + if (db.Exists()) + { + db.Delete(); + } + db.Create(); + return db; + } + + /// + /// Get specialized subclass of CouchDatabase. That class should + /// define its own database name. We presume it is already created. + /// + public T GetExistingDatabase() where T : CouchDatabase, new() + { + return new T {Server = this}; + } + + /// + /// Get specialized subclass of CouchDatabase with given name. + /// We presume it is already created. + /// + public T GetExistingDatabase(string name) where T : CouchDatabase, new() + { + return new T {Name = name, Server = this}; + } + + /// + /// Get specialized subclass of CouchDatabase. That class should + /// define its own database name. We ensure that it is created. + /// + public T GetDatabase() where T : CouchDatabase, new() + { + var db = GetExistingDatabase(); + db.Create(); + return db; + } + + /// + /// Get specialized subclass of CouchDatabase with given name. + /// We create the database if needed. + /// + public T GetDatabase(string name) where T : CouchDatabase, new() + { + var db = GetExistingDatabase(name); + db.Create(); + return db; + } + + public void CreateDatabase(string name) + { + try + { + Request().Path(name).Put().Check("Failed to create database"); + } + catch (WebException e) + { + throw CouchException.Create("Failed to create database", e); + } + } + + public void DeleteAllDatabases() + { + DeleteDatabases(".*"); + } + + public void DeleteDatabases(string regExp) + { + var reg = new Regex(regExp); + foreach (string name in GetDatabaseNames()) + { + if (reg.IsMatch(name)) + { + DeleteDatabase(name); + } + } + } + + public void DeleteDatabase(string name) + { + try + { + Request().Path(name).Delete().Check("Failed to delete database"); + } + catch (WebException e) + { + throw new CouchException("Failed to delete database", e); + } + } + + public IList GetDatabaseNames() + { + return (List) serializer.Deserialize(new JsonTextReader(new StringReader(Request().Path("_all_dbs").String())), typeof (List)); + } + } +} \ No newline at end of file diff --git a/src/CouchTest.cs b/src/CouchTest.cs new file mode 100644 index 0000000..3a4bbc3 --- /dev/null +++ b/src/CouchTest.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using NUnit.Framework.SyntaxHelpers; + +namespace Divan +{ + /// + /// Unit tests for Divan. Operates in a separate CouchDB database called divan_unit_tests. + /// + [TestFixture] + public class CouchTest + { + #region Setup/Teardown + + [SetUp] + public void SetUp() + { + server = new CouchServer(); + db = server.GetNewDatabase(DbName); + } + + [TearDown] + public void TearDown() + { + db.Delete(); + } + + #endregion + + private CouchServer server; + private CouchDatabase db; + private const string DbName = "divan_unit_tests"; + + [Test] + public void ShouldCheckChangedDocument() + { + CouchJsonDocument doc = db.CreateDocument("{\"CPU\": \"Intel\"}"); + CouchJsonDocument doc2 = db.GetDocument(doc.Id); + Assert.That(db.HasDocumentChanged(doc), Is.False); + doc2.Obj["CPU"] = JToken.FromObject("AMD"); + db.WriteDocument(doc2); + Assert.That(db.HasDocumentChanged(doc), Is.True); + } + + [Test] + public void ShouldCountDocuments() + { + Assert.That(db.CountDocuments(), Is.EqualTo(0)); + db.CreateDocument("{\"CPU\": \"Intel\"}"); + Assert.That(db.CountDocuments(), Is.EqualTo(1)); + } + + [Test] + public void ShouldCreateDocument() + { + var doc = new CouchJsonDocument("{\"CPU\": \"Intel\"}"); + ICouchDocument cd = db.CreateDocument(doc); + Assert.That(db.CountDocuments(), Is.EqualTo(1)); + Assert.That(cd.Id, Is.Not.Null); + Assert.That(cd.Rev, Is.Not.Null); + } + + [Test] + public void ShouldCreateDocuments() + { + const string doc = "{\"CPU\": \"Intel\"}"; + var doc1 = new CouchJsonDocument(doc); + var doc2 = new CouchJsonDocument(doc); + IList list = new List {doc1, doc2}; + db.SaveDocuments(list, true); + Assert.That(db.CountDocuments(), Is.EqualTo(2)); + Assert.That(doc1.Id, Is.Not.Null); + Assert.That(doc1.Rev, Is.Not.Null); + Assert.That(doc2.Id, Is.Not.Null); + Assert.That(doc2.Rev, Is.Not.Null); + Assert.That(doc1.Id, Is.Not.EqualTo(doc2.Id)); + } + + [Test, ExpectedException(typeof (CouchNotFoundException))] + public void ShouldDeleteDatabase() + { + db.Delete(); + Assert.That(server.HasDatabase(db.Name), Is.EqualTo(false)); + server.DeleteDatabase(db.Name); // one more time should fail + } + + [Test] + public void ShouldDeleteDocuments() + { + const string doc = "{\"CPU\": \"Intel\"}"; + CouchJsonDocument doc1 = db.CreateDocument(doc); + CouchJsonDocument doc2 = db.CreateDocument(doc); + if (String.Compare(doc1.Id, doc2.Id) < 0) + { + db.DeleteDocuments(doc1.Id, doc2.Id); + } + else + { + db.DeleteDocuments(doc2.Id, doc1.Id); + } + Assert.That(db.HasDocument(doc1.Id), Is.False); + Assert.That(db.HasDocument(doc2.Id), Is.False); + } + + [Test, ExpectedException(typeof (CouchException))] + public void ShouldFailCreateDatabase() + { + server.CreateDatabase(db.Name); // one more time should fail + } + + [Test] + public void ShouldGetDatabaseNames() + { + bool result = server.GetDatabaseNames().Contains(db.Name); + Assert.That(result, Is.EqualTo(true)); + } + + [Test] + public void ShouldGetDocument() + { + const string doc = "{\"CPU\": \"Intel\"}"; + CouchJsonDocument oldDoc = db.CreateDocument(doc); + CouchJsonDocument newDoc = db.GetDocument(oldDoc.Id); + Assert.That(oldDoc.Id, Is.EqualTo(newDoc.Id)); + Assert.That(oldDoc.Rev, Is.EqualTo(newDoc.Rev)); + } + + [Test] + public void ShouldGetDocuments() + { + const string doc = "{\"CPU\": \"Intel\"}"; + CouchJsonDocument doc1 = db.CreateDocument(doc); + CouchJsonDocument doc2 = db.CreateDocument(doc); + var ids = new List {doc1.Id, doc2.Id}; + IList docs = db.GetDocuments(ids); + Assert.That(doc1.Id, Is.EqualTo(docs.First().Id)); + Assert.That(doc2.Id, Is.EqualTo(docs.Last().Id)); + } + + [Test] + public void ShouldReturnNullWhenNotFound() + { + var doc = db.GetDocument("jadda"); + Assert.That(doc, Is.Null); + CouchJsonDocument doc2 = db.GetDocument("jadda"); + Assert.That(doc2, Is.Null); + } + + [Test] + public void ShouldSaveDocumentWithId() + { + var doc = new CouchJsonDocument("{\"_id\":\"123\", \"CPU\": \"Intel\"}"); + ICouchDocument cd = db.SaveDocument(doc); + Assert.That(db.CountDocuments(), Is.EqualTo(1)); + Assert.That(cd.Id, Is.Not.Null); + Assert.That(cd.Rev, Is.Not.Null); + } + + [Test] + public void ShouldSaveDocumentWithoutId() + { + var doc = new CouchJsonDocument("{\"CPU\": \"Intel\"}"); + ICouchDocument cd = db.SaveDocument(doc); + Assert.That(db.CountDocuments(), Is.EqualTo(1)); + Assert.That(cd.Id, Is.Not.Null); + Assert.That(cd.Rev, Is.Not.Null); + } + + [Test] + public void ShouldStoreGetAndDeleteAttachment() + { + var doc = new CouchJsonDocument("{\"CPU\": \"Intel\"}"); + ICouchDocument cd = db.CreateDocument(doc); + Assert.That(db.HasAttachment(cd), Is.False); + db.WriteAttachment(cd, "jabbadabba", "text/plain"); + Assert.That(db.HasAttachment(cd), Is.True); + Assert.That(db.ReadAttachment(cd), Is.EqualTo("jabbadabba")); + db.WriteAttachment(cd, "jabbadabba-doo", "text/plain"); + Assert.That(db.HasAttachment(cd), Is.True); + Assert.That(db.ReadAttachment(cd), Is.EqualTo("jabbadabba-doo")); + db.DeleteAttachment(cd); + Assert.That(db.HasAttachment(cd), Is.False); + } + + [Test, ExpectedException(typeof (CouchConflictException))] + public void ShouldThrowConflictExceptionOnAlreadyExists() + { + const string doc = "{\"CPU\": \"Intel\"}"; + CouchJsonDocument doc1 = db.CreateDocument(doc); + var doc2 = new CouchJsonDocument(doc) {Id = doc1.Id}; + db.WriteDocument(doc2); + } + + [Test, ExpectedException(typeof (CouchConflictException))] + public void ShouldThrowConflictExceptionOnStaleWrite() + { + const string doc = "{\"CPU\": \"Intel\"}"; + CouchJsonDocument doc1 = db.CreateDocument(doc); + CouchJsonDocument doc2 = db.GetDocument(doc1.Id); + doc1.Obj["CPU"] = JToken.FromObject("AMD"); + db.SaveDocument(doc1); + doc2.Obj["CPU"] = JToken.FromObject("Via"); + db.SaveDocument(doc2); + } + + [Test] + public void ShouldUseETagForView() + { + var design = new DesignCouchDocument("computers", db); + design.AddView("by_cpumake", + @"function(doc) { + emit(doc.CPU, doc); + }"); + db.WriteDocument(design); + + CouchJsonDocument doc1 = db.CreateDocument("{\"CPU\": \"Intel\"}"); + db.CreateDocument("{\"CPU\": \"AMD\"}"); + db.CreateDocument("{\"CPU\": \"Via\"}"); + db.CreateDocument("{\"CPU\": \"Sparq\"}"); + + CouchQuery query = db.Query("computers", "by_cpumake").StartKey("Intel").EndKey("Via").CheckETagUsingHead(); + // Query has no result yet so should not be cached + Assert.That(query.IsCachedAndValid(), Is.False); + query.GetResult(); + // Now it is cached and should be valid + Assert.That(query.IsCachedAndValid(), Is.True); + // Make a change invalidating the view + db.SaveDocument(doc1); + // It should now be false + Assert.That(query.IsCachedAndValid(), Is.False); + query.GetResult(); + // And now it should be cached again + Assert.That(query.IsCachedAndValid(), Is.True); + query.GetResult(); + // Still cached of course + Assert.That(query.IsCachedAndValid(), Is.True); + } + + [Test] + public void ShouldWriteDocument() + { + var doc = new CouchJsonDocument("{\"_id\":\"123\", \"CPU\": \"Intel\"}"); + ICouchDocument cd = db.WriteDocument(doc); + Assert.That(db.CountDocuments(), Is.EqualTo(1)); + Assert.That(cd.Id, Is.Not.Null); + Assert.That(cd.Rev, Is.Not.Null); + } + } +} \ No newline at end of file diff --git a/src/CouchViewDefinition.cs b/src/CouchViewDefinition.cs new file mode 100644 index 0000000..769674f --- /dev/null +++ b/src/CouchViewDefinition.cs @@ -0,0 +1,85 @@ +using Newtonsoft.Json; + +namespace Divan +{ + /// + /// A definition of a CouchDB view with a name, a map and a reduce function and a reference to the + /// owning DesignCouchDocument. + /// + public class CouchViewDefinition + { + /// + /// Constructor used to create "on the fly" definitions, like for example for "_all_docs". + /// + /// View name used in URI. + /// A design doc, can also be created on the fly. + public CouchViewDefinition(string name, DesignCouchDocument doc) + { + Doc = doc; + Name = name; + } + + /// + /// Constructor used for permanent views, see CouchDesignDocument. + /// + /// View name. + /// Map function. + /// Optional reduce function. + /// Parent document. + public CouchViewDefinition(string name, string map, string reduce, DesignCouchDocument doc) + { + Doc = doc; + Name = name; + Map = map; + Reduce = reduce; + } + + public DesignCouchDocument Doc { get; set; } + public string Name { get; set; } + public string Map { get; set; } + public string Reduce { get; set; } + + public CouchRequest Request() + { + return Doc.Owner.Request(Path()); + } + + public CouchDatabase Db() + { + return Doc.Owner; + } + + public void WriteJson(JsonWriter writer) + { + writer.WritePropertyName(Name); + writer.WriteStartObject(); + writer.WritePropertyName("map"); + writer.WriteValue(Map); + if (Reduce != null) + { + writer.WritePropertyName("reduce"); + writer.WriteValue(Reduce); + } + writer.WriteEndObject(); + } + + public CouchQuery Query() + { + return Doc.Owner.Query(this); + } + + public void Touch() + { + Query().Limit(0).GetResult(); + } + + public string Path() + { + if (Doc.Id == "_design/") + { + return Name; + } + return Doc.Id + "/_view/" + Name; + } + } +} \ No newline at end of file diff --git a/src/CouchViewResult.cs b/src/CouchViewResult.cs new file mode 100644 index 0000000..b31ec5e --- /dev/null +++ b/src/CouchViewResult.cs @@ -0,0 +1,36 @@ +using Newtonsoft.Json.Linq; + +namespace Divan +{ + /// + /// This is a view result from a CouchQuery. The result is returned as JSON + /// from CouchDB and parsed into a JObject by Newtonsoft.Json. A view result + /// also includes some meta information and this class has methods to access these. + /// Typically you use a subclass. + /// + public class CouchViewResult + { + public string etag; + public JObject result; + + public void Result(JObject obj) + { + result = obj; + } + + public int Count() + { + return result["total_rows"].Value(); + } + + public int Offset() + { + return result["offset"].Value(); + } + + public JEnumerable Rows() + { + return result["rows"].Children(); + } + } +} \ No newline at end of file diff --git a/src/CustomDictionary.xml b/src/CustomDictionary.xml new file mode 100644 index 0000000..9b13063 --- /dev/null +++ b/src/CustomDictionary.xml @@ -0,0 +1,468 @@ + + + + + + cb + ch + csc + elem + gt + idx + img + lg + multi + num + ps + pw + scp + si + sig + tk + tw + val + + + + json + accessor + accessors + acos + aes + aptca + arg + args + asin + asm + aspx + async + atan + baml + bcl + bindable + bitrate + blittable + blog + bool + bootstrapper + bootstrappers + browsable + cacheability + callee + callees + canonicalize + cdecl + cdo + chtml + cim + cloneable + clr + clr's + cls + clsid + clsids + cmd + cmdlet + cmdlets + comparand + concat + config + contravariant + cookieless + cos + crm + css + cyclomatic + debuggable + decommission + deformatter + delegator + dequeue + dereferenced + des + deserialization + deserialize + deserialized + deserializing + dhcp + discardable + dll + dns + documentable + dsig + dtd + em + email + emails + emf + encodable + endian + enqueue + enum + enums + expando + finalizer + finalizers + fixup + fixups + formattable + func + guid + guids + hashtable + hashtables + hashtable's + hdc + hijri + href + iis + il + ime + initializer + initializers + int + interop + intrinsics + ipv + iterator + iterators + jit + ldap + linq + localhost + loopback + loopbacks + mapper + mappers + marshaler + marshalers + mdi + mergable + misc + miscased + monitorable + oks + mscoree + mscorlib + msh + multiline + multipanel + multipanels + multiview + multiviews + mutator + mutators + mutex + mutexes + ndpsec + nls + nop + ntfs + ntlm + nullable + obj + odbc + overridable + pageable + parameterless + pdb + persistable + playlist + pragma + prepend + prog + ptr + queryable + ras + rect + rects + recurse + refactor + reg + regex + remoted + remoting + representable + res + resolver + resolvers + rethrow + rethrows + rijndael + rpc + rtc + rva + sdl + searchspace + searchspaces + seekable + seq + serializable + serializer + serializers + smtp + specifier + specifiers + spline + sql + ssl + sta + stickies + struct + structs + subaddress + subaddresses + subclass + subclasses + subdirectories + subdirectory + subexpression + subexpressions + subitem + subitems + subkey + subkeys + submenu + submenus + subpath + subpaths + subsegment + subsegments + subtree + subtrees + tcp + templated + thunk + thunks + tlb + tuple + tuples + udp + udt + unboxing + uncategorize + unindent + uninitialize + uninitialized + uninstantiated + unmaintainable + unmarshal + unregister + unregistering + unregisters + unregistration + unrepresentable + unterminated + untrusted + uri + uris + url + urls + utc + utf + validator + vsa + weblog + wiki + wcf + wmf + wmi + wml + wpf + wql + wsdl + xaml + xhtml + xmlns + xor + xrml + xsd + xsi + xsl + xslt + + + + complus + cancelled + indices + login + logout + signon + signoff + writeable + cant + arent + dont + doesnt + didnt + couldnt + wouldnt + shouldnt + wont + havent + hasnt + hadnt + isnt + wasnt + werent + flag + flags + + + + datastore + datastores + dataset + datasets + textbox + textboxes + codepage + codepages + checkbox + checkboxes + pushbutton + pushbuttons + dropdown + dropdowns + toolbar + toolbars + scrollbar + scrollbars + bitflag + bitflags + filename + filenames + fileserver + fileservers + username + usernames + hostname + hostnames + fieldname + fieldnames + pathname + pathnames + whitespace + whitespaces + logon + logons + logoff + logoffs + signin + signins + signout + signouts + frontend + frontends + backend + backends + sitemap + sitemaps + datatype + datatypes + designtime + designtimes + readonly + truetype + netbios + autodetect + autodetects + autoscroll + autoscrolls + autocomplete + autocompletes + autosave + autosaves + javascript + jscript + voiceview + appletalk + mapinfo + newline + newlines + qword + qwords + keyset + keysets + + + + onset + inset + byname + setout + countertype + editor + longtime + drawstring + hookup + cleanup + breakout + setline + maybe + nods + classis + gettable + inform + beset + settable + standalone + threadlike + infield + infields + meantime + mackey + jscript + ipv + tooltip + tooltips + indispose + + + + + Pi + Na + NESW + NWSE + Json + + + diff --git a/src/DesignCouchDocument.cs b/src/DesignCouchDocument.cs new file mode 100644 index 0000000..6722026 --- /dev/null +++ b/src/DesignCouchDocument.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Divan +{ + /// + /// A named design document in CouchDB. Holds CouchViewDefinitions. + /// + public class DesignCouchDocument : CouchDocument + { + public IList Definitions = new List(); + public string Language = "javascript"; + public CouchDatabase Owner; + + public DesignCouchDocument(string documentId, CouchDatabase owner) + : base("_design/" + documentId) + { + Owner = owner; + } + + /// + /// Add view without a reduce function. + /// + /// Name of view + /// Map function + /// + public CouchViewDefinition AddView(string name, string map) + { + return AddView(name, map, null); + } + + /// + /// Add view with a reduce function. + /// + /// Name of view + /// Map function + /// Reduce function + /// + public CouchViewDefinition AddView(string name, string map, string reduce) + { + var def = new CouchViewDefinition(name, map, reduce, this); + Definitions.Add(def); + return def; + } + + public override void WriteJson(JsonWriter writer) + { + WriteIdAndRev(this, writer); + writer.WritePropertyName("language"); + writer.WriteValue(Language); + writer.WritePropertyName("views"); + writer.WriteStartObject(); + foreach (CouchViewDefinition definition in Definitions) + { + definition.WriteJson(writer); + } + writer.WriteEndObject(); + } + + public override void ReadJson(JObject obj) + { + ReadIdAndRev(this, obj); + } + } +} \ No newline at end of file diff --git a/src/Divan.FxCop b/src/Divan.FxCop new file mode 100644 index 0000000..a3875f0 --- /dev/null +++ b/src/Divan.FxCop @@ -0,0 +1,307 @@ + + + + True + c:\sandbox\monitor2\thirdparty\tools\fxcop\Xml\FxCopReport.xsl + + + + + + True + True + True + 10 + 1 + + False + + False + 120 + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + 'Divan.dll' + + + + + + + + + + + + + 'CouchDatabase.GetAllDocuments()' + + + + + + + + + 'CouchDatabase.GetDocument<T>(string)' + 'T' + + + + + + + + + 'CouchDatabase.GetResultWithOptions<T>(string, string, Dictionary<string, string>)' + 'T' + + + + + + + + + 'CouchDatabase.GetView<T>(string, string)' + 'T' + + + + + + + + + 'CouchDatabase.GetView<T>(string, string, string)' + 'T' + + + + + + + + + 'CouchDatabase.GetView<T>(string, string, string, string)' + 'T' + + + + + + + + + + + + + 'CouchGenericViewResult.Document<T>()' + 'T' + + + + + + + + + 'CouchGenericViewResult.Documents<T>()' + 'T' + + + + + + + + + 'CouchGenericViewResult.RetrieveDocument<T>(string)' + 'T' + + + + + + + + + 'CouchGenericViewResult.RetrieveDocuments<T>(string)' + 'T' + + + + + + + + + 'CouchGenericViewResult.ValueDocument<T>()' + 'T' + + + + + + + + + 'CouchGenericViewResult.ValueDocuments<T>()' + 'T' + + + + + + + + + + + + + 'CouchQuery.GetResult<T>()' + 'T' + + + + + + + + + + + + + 'CouchServer.GetDatabaseNames()' + + + + + + + + + 'url' + 'CouchServer.Request(CouchDatabase, string, string)' + + + + + + + + + 'url' + 'CouchServer.Request(CouchDatabase, string, string, string, string)' + + + + + + + + + 'url' + 'CouchServer.RequestStream(CouchDatabase, string, string, string, string)' + + + + + + + + + + + + + SetUp + 'CouchTest.SetUp()' + Setup + + + + + + + + + + + + + + TearDown + 'CouchTest.TearDown()' + Teardown + + + + + + + + + + + + + + + + + + + + + NUnit + NUnit + + + + + Sign {0} with a strong name key. + + + The compound word '{0}' in member name {1} exists as a discrete term. If your usage is intended to be single word, case it as '{2}' or strip the first token entirely if it represents any sort of Hungarian notation. + + + Consider a design where {0} doesn't require explicit type parameter {1} in any call to it. + + + Change the type of parameter {0} of method {1} from string to System.Uri, or provide an overload of {1}, that allows {0} to be passed as a System.Uri object. + + + Change {0} to a property if appropriate. + + + + diff --git a/src/Divan.csproj b/src/Divan.csproj new file mode 100644 index 0000000..fe5cc85 --- /dev/null +++ b/src/Divan.csproj @@ -0,0 +1,80 @@ + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {37AC0B66-5340-4B81-BC62-3EE80233A011} + Library + Properties + Divan + Divan + v3.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\lib\Newtonsoft.Json.dll + + + False + ..\lib\nunit.framework.dll + + + + 3.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ICanJSON.cs b/src/ICanJSON.cs new file mode 100644 index 0000000..02d7d4f --- /dev/null +++ b/src/ICanJSON.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Divan +{ + /// + /// Basic capability to write and read myself using Newtonsoft.JSON. + /// Writing is done using JsonWriter in a fast streaming fashion. + /// Reading is done using JObject "DOM style". + /// + public interface ICanJson + { + void WriteJson(JsonWriter writer); + void ReadJson(JObject obj); + } +} \ No newline at end of file diff --git a/src/ICouchDocument.cs b/src/ICouchDocument.cs new file mode 100644 index 0000000..d6b8999 --- /dev/null +++ b/src/ICouchDocument.cs @@ -0,0 +1,14 @@ +namespace Divan +{ + /// + /// An ICouchDocument needs to have a Rev and an Id. It also needs to implement ICanJson + /// which means it can read and write itself as JSON. Either you let your domain objects + /// that you want to store in CouchDB implement this interface or if you are free to pick + /// your own base class you can subclass from CouchDocument (or even CouchJsonDocument). + /// + public interface ICouchDocument : ICanJson + { + string Rev { get; set; } + string Id { get; set; } + } +} \ No newline at end of file diff --git a/src/Properties/AssemblyInfo.cs b/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..dc865a7 --- /dev/null +++ b/src/Properties/AssemblyInfo.cs @@ -0,0 +1,39 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. + +[assembly: AssemblyTitle("Divan")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Foretagsplatsen AB")] +[assembly: AssemblyProduct("Divan")] +[assembly: AssemblyCopyright("Copyright © Foretagsplatsen AB 2009")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. + +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM + +[assembly: Guid("83fd2c08-3765-4908-a901-a9ce4f6d05c0")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file