diff --git a/JsonCSharpClassGeneratorLib/CodeWriters/CSharpCodeWriter.cs b/JsonCSharpClassGeneratorLib/CodeWriters/CSharpCodeWriter.cs index e97a36f..e18a78a 100644 --- a/JsonCSharpClassGeneratorLib/CodeWriters/CSharpCodeWriter.cs +++ b/JsonCSharpClassGeneratorLib/CodeWriters/CSharpCodeWriter.cs @@ -9,6 +9,16 @@ namespace Xamasoft.JsonClassGenerator.CodeWriters { public class CSharpCodeWriter : ICodeWriter { + private static HashSet<string> _csharpKeywords = new HashSet<string> + { + "abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char", "checked", "class", "const", "continue", "decimal", "default", + "delegate", "do", "double", "else", "enum", "event", "explicit", "extern", "false", "finally", "fixed", "float", "for", "foreach", "goto", + "if", "implicit", "in", "int", "interface", "internal", "is", "lock", "long", "namespace", "new", "null", "object", "operator", "out", + "override", "params", "private", "protected", "public", "readonly", "ref", "return", "sbyte", "sealed", "short", "sizeof", "stackalloc", + "static", "string", "struct", "switch", "this", "throw", "true", "try", "typeof", "uint", "ulong", "unchecked", "unsafe", "ushort", "using", + "virtual", "void", "volatile", "while" + }; + public string FileExtension { get { return ".cs"; } @@ -46,7 +56,7 @@ public string GetTypeName(JsonType type, IJsonClassGeneratorConfig config) case JsonTypeEnum.NullableSomething: return "object"; case JsonTypeEnum.Object: return type.AssignedName; case JsonTypeEnum.String: return "string"; - default: throw new System.NotSupportedException("Unsupported json type"); + default: throw new NotSupportedException("Unsupported json type"); } } @@ -193,15 +203,14 @@ private void WriteClassMembers(IJsonClassGeneratorConfig config, TextWriter sw, sw.WriteLine(prefix + "/// </summary>"); } - if (config.UsePascalCase) + if (true || config.UsePascalCase) { - sw.WriteLine(prefix + "[JsonProperty(\"{0}\")]", field.JsonMemberName); } if (config.UseProperties) { - sw.WriteLine(prefix + "public {0} {1} {{ get; set; }}", field.Type.GetTypeName(), field.MemberName); + sw.WriteLine(prefix + "public {0} {1}{2} {{ get; set; }}", field.Type.GetTypeName(), (ShouldPrefix(field.MemberName) || field.MemberName == type.AssignedName) ? "_" : "", field.MemberName); } else { @@ -212,7 +221,16 @@ private void WriteClassMembers(IJsonClassGeneratorConfig config, TextWriter sw, } + private bool ShouldPrefix(string fieldName) + { + if (!Char.IsLetter(fieldName.ToCharArray()[0])) + return true; + if (_csharpKeywords.Contains(fieldName)) + return true; + + return false; + } @@ -234,7 +252,7 @@ private void WriteClassWithPropertiesExplicitDeserialization(TextWriter sw, Json string variable = null; if (field.Type.MustCache) { - variable = "_" + char.ToLower(field.MemberName[0]) + field.MemberName.Substring(1); + variable = "_" + Char.ToLower(field.MemberName[0]) + field.MemberName.Substring(1); sw.WriteLine(prefix + "[System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]"); sw.WriteLine(prefix + "private {0} {1};", field.Type.GetTypeName(), variable); } @@ -295,6 +313,5 @@ private void WriteClassWithFieldsExplicitDeserialization(TextWriter sw, JsonType } } #endregion - } } diff --git a/JsonCSharpClassGeneratorLib/JsonClassGenerator.cs b/JsonCSharpClassGeneratorLib/JsonClassGenerator.cs index dbeb9eb..f9b4bc7 100644 --- a/JsonCSharpClassGeneratorLib/JsonClassGenerator.cs +++ b/JsonCSharpClassGeneratorLib/JsonClassGenerator.cs @@ -249,17 +249,6 @@ private void GenerateClass(JObject[] examples, JsonType type) } } - // detect and correct when a field's name matches its containing class. - if (jsonFields.ContainsKey(type.AssignedName)) - { - var newKey = type.AssignedName + "_"; - - jsonFields.Add(newKey, jsonFields[type.AssignedName]); - fieldExamples.Add(newKey, fieldExamples[type.AssignedName]); - - jsonFields.Remove(type.AssignedName); - fieldExamples.Remove(type.AssignedName); - } type.Fields = jsonFields.Select(x => new FieldInfo(this, x.Key, x.Value, UsePascalCase, fieldExamples[x.Key])).ToArray(); @@ -302,7 +291,7 @@ internal static string ToTitleCase(string str) for (int i = 0; i < str.Length; i++) { var c = str[i]; - if (char.IsLetterOrDigit(c)) + if (char.IsLetterOrDigit(c) || c == '_') { sb.Append(flag ? char.ToUpper(c) : c); flag = false; diff --git a/JsonCSharpClassGeneratorLib/JsonType.cs b/JsonCSharpClassGeneratorLib/JsonType.cs index 5fbafe5..56623e7 100644 --- a/JsonCSharpClassGeneratorLib/JsonType.cs +++ b/JsonCSharpClassGeneratorLib/JsonType.cs @@ -76,6 +76,9 @@ internal JsonType MaybeMakeNullable(IJsonClassGeneratorConfig generator) public void AssignName(string name) { + if (!char.IsLetter(name.ToCharArray()[0])) + name = "_" + name; + AssignedName = name; } diff --git a/JsonDataContextBase/JsonDataContextBase.cs b/JsonDataContextBase/JsonDataContextBase.cs index acb6433..a4d7e91 100644 --- a/JsonDataContextBase/JsonDataContextBase.cs +++ b/JsonDataContextBase/JsonDataContextBase.cs @@ -1,22 +1,73 @@ using System; +using System.Collections; using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Net; +using System.Runtime.CompilerServices; +using System.Security.Policy; using Newtonsoft.Json; + namespace JsonDataContext { public class JsonDataContextBase { - protected static IEnumerable<T> DeserializeSequenceFromJsonFile<T>(string filePath) + public Dictionary<string, string> _jsonTextInputs = new Dictionary<string, string>(); + + protected IEnumerable<T> GetFileJsonInput<T>(string filePath) + { + var stream = File.OpenRead(filePath); + + return ReadFromJsonStream<T>(stream); + } + + protected IEnumerable<T> GetTextJsonInput<T>(string key) + { + var json = ""; + + if (!_jsonTextInputs.TryGetValue(key, out json)) + throw new Exception(String.Format("Could not find json data for key '{0}'", key)); + + if (!json.Trim().StartsWith("[")) + json = String.Format("[{0}]", json.Trim()); + + var stream = ToStream(json); + + return ReadFromJsonStream<T>(stream); + } + + protected IEnumerable<T> GetUrlParameterlessInput<T>(string url) { - using (var sr = new StreamReader(filePath)) - { + var req = (HttpWebRequest) HttpWebRequest.Create(url); + req.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip; + req.UserAgent = @"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"; + + var stream = req.GetResponse().GetResponseStream(); + + return ReadFromJsonStream<T>(stream); + } + + private static IEnumerable<T> ReadFromJsonStream<T>(Stream stream) + { + using (var sr = new StreamReader(stream)) + { using (var reader = new JsonTextReader(sr)) { var serializer = new JsonSerializer(); - if (!reader.Read() || reader.TokenType != JsonToken.StartArray) - throw new Exception("The source input did not appear to be an array of objects."); + if (!reader.Read()) + throw new InvalidDataException("Could not interpret input as JSON"); + + // not an array + if (reader.TokenType != JsonToken.StartArray) + { + var item = serializer.Deserialize<T>(reader); + yield return item; + yield break; + } + + // yes an array while (reader.Read()) { if (reader.TokenType == JsonToken.EndArray) break; @@ -26,5 +77,15 @@ protected static IEnumerable<T> DeserializeSequenceFromJsonFile<T>(string filePa } } } + + protected static Stream ToStream(string str) + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(str); + writer.Flush(); + stream.Position = 0; + return stream; + } } } \ No newline at end of file diff --git a/JsonDataContextDriver/DevDeploy.bat b/JsonDataContextDriver/DevDeploy.bat index f8fc9d3..b064a1b 100644 --- a/JsonDataContextDriver/DevDeploy.bat +++ b/JsonDataContextDriver/DevDeploy.bat @@ -2,4 +2,5 @@ taskkill /f /im:LINQPad.exe xcopy /i/y *.* "%programdata%\LINQPad\Drivers\DataContext\4.0\JsonDataContextDriver (ed22602f98bb09d6)\" powershell start-process "C:\Users\rdavis\Dropbox\code\LINQPad4\LINQPad.exe" +rem powershell start-process "C:\Users\rdavis\Desktop\LINQPad4\LINQPad.exe" exit 0 diff --git a/JsonDataContextDriver/Extensions/Extensions.cs b/JsonDataContextDriver/Extensions/Extensions.cs index 9e66154..486c9f7 100644 --- a/JsonDataContextDriver/Extensions/Extensions.cs +++ b/JsonDataContextDriver/Extensions/Extensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; namespace JsonDataContextDriver @@ -8,6 +9,9 @@ public static class Extensions { public static string ReplaceAll(this string s, IEnumerable<Tuple<string, string>> replacements) { + if (String.IsNullOrEmpty(s)) + return s; + foreach (var repl in replacements) s = s.Replace(repl.Item1, repl.Item2); @@ -22,5 +26,19 @@ public static IEnumerable<T> DoEach<T>(this IEnumerable<T> items, Action<T> acti yield return item; } } + + public static string SanitiseClassName(this string originalName) + { + var replacers = new[] { "\n", "'", " ", "*", "/", "-", "(", ")", ".", "!", "?", "#", ":", "+", "{", "}", "&", "," }; + var tuples = replacers.Select(r => Tuple.Create(r, "_")).ToList(); + + var newName = originalName.ReplaceAll(tuples); + if (char.IsNumber(newName[0])) + newName = "_" + newName; + + return newName; + } } + + } \ No newline at end of file diff --git a/JsonDataContextDriver/GeneratedClass.cs b/JsonDataContextDriver/GeneratedClass.cs deleted file mode 100644 index 430ffa8..0000000 --- a/JsonDataContextDriver/GeneratedClass.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace JsonDataContextDriver -{ - public class GeneratedClass - { - public string Namespace { get; set; } - public string ClassName { get; set; } - public string DataFilePath { get; set; } - public string ClassDefinition { get; set; } - public bool Success { get; set; } - public Exception Error { get; set; } - } -} \ No newline at end of file diff --git a/JsonDataContextDriver/Inputs/IGeneratedClass.cs b/JsonDataContextDriver/Inputs/IGeneratedClass.cs new file mode 100644 index 0000000..fc514c5 --- /dev/null +++ b/JsonDataContextDriver/Inputs/IGeneratedClass.cs @@ -0,0 +1,15 @@ +using System; + +namespace JsonDataContextDriver +{ + public interface IGeneratedClass + { + string Namespace { get; set; } + string ClassName { get; set; } + string ClassDefinition { get; set; } + bool Success { get; set; } + Exception Error { get; set; } + + IJsonInput OriginalInput { get; set; } + } +} \ No newline at end of file diff --git a/JsonDataContextDriver/Inputs/IJsonInput.cs b/JsonDataContextDriver/Inputs/IJsonInput.cs new file mode 100644 index 0000000..17ebbea --- /dev/null +++ b/JsonDataContextDriver/Inputs/IJsonInput.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using LINQPad.Extensibility.DataContext; + +namespace JsonDataContextDriver +{ + public interface IJsonInput + { + void GenerateClasses(string nameSpace); + + List<IGeneratedClass> GeneratedClasses { get; } + List<ExplorerItem> ExplorerItems { get; } + List<string> NamespacesToAdd { get; } + List<string> ContextProperties { get; } + List<string> Errors { get; } + } +} \ No newline at end of file diff --git a/JsonDataContextDriver/Inputs/JsonFileGeneratedClass.cs b/JsonDataContextDriver/Inputs/JsonFileGeneratedClass.cs new file mode 100644 index 0000000..83bb562 --- /dev/null +++ b/JsonDataContextDriver/Inputs/JsonFileGeneratedClass.cs @@ -0,0 +1,26 @@ +using System; + +namespace JsonDataContextDriver +{ + public class JsonFileGeneratedClass : IGeneratedClass + { + public JsonFileGeneratedClass(JsonFileInput input) + { + OriginalInput = input; + } + + public string Namespace { get; set; } + public string ClassName { get; set; } + public string DataFilePath { get; set; } + public string ClassDefinition { get; set; } + public bool Success { get; set; } + public Exception Error { get; set; } + + public JsonFileInput OriginalInput { get; set; } + IJsonInput IGeneratedClass.OriginalInput + { + get { return OriginalInput; } + set { OriginalInput = (JsonFileInput) value; } + } + } +} \ No newline at end of file diff --git a/JsonDataContextDriver/Inputs/JsonFileInput.cs b/JsonDataContextDriver/Inputs/JsonFileInput.cs new file mode 100644 index 0000000..bc6688d --- /dev/null +++ b/JsonDataContextDriver/Inputs/JsonFileInput.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Serialization; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; +using System.Windows.Media; +using JsonDataContextDriver; +using LINQPad.Extensibility.DataContext; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PropertyChanged; +using Xamasoft.JsonClassGenerator; + +namespace JsonDataContextDriver +{ + [ImplementPropertyChanged] + public class JsonFileInput : IJsonInput + { + public string InputPath { get; set; } + + public string Mask { get; set; } + + public bool Recursive { get; set; } + + public int NumRowsToSample { get; set; } + + public JsonInputType InputType + { + get + { + if (string.IsNullOrWhiteSpace(InputPath)) + return JsonInputType.Nothing; + if (File.Exists(InputPath)) + return JsonInputType.File; + if (Directory.Exists(InputPath)) + return JsonInputType.Directory; + return JsonInputType.Invalid; + } + } + + public bool IsDirectory + { + get { return InputType == JsonInputType.Directory; } + } + + public JsonFileInput() + { + NumRowsToSample = 50; + NamespacesToAdd = new List<string>(); + } + + public override string ToString() + { + switch (InputType) + { + case JsonInputType.File: + return InputPath; + case JsonInputType.Directory: + return Path.Combine(InputPath, Mask ?? "*.*") + (Recursive ? " + subfolders" : ""); + default: + return "ERR"; + } + } + + public void GenerateClasses(string nameSpace) + { + var numSamples = NumRowsToSample; + + _generatedClasses = + GetInputFiles() + .Select(f => + { + // TODO: Be a better error handler + try + { + var fs = new FileStream(f, FileMode.Open); + var sr = new StreamReader(fs); + var jtr = new JsonTextReader(sr); + + var examples = + Enumerable + .Range(0, numSamples) + .Select(_ => + { + while (jtr.Read()) + if (jtr.TokenType == JsonToken.StartObject) + return JObject.Load(jtr).ToString(); + return null; + }) + .Where(json => json != null); + + var examplesJson = String.Format("[{0}]", String.Join(",\r\n", examples)); + + jtr.Close(); + sr.Close(); + fs.Close(); + + var className = Path.GetFileNameWithoutExtension(f).SanitiseClassName(); + var finalNamespace = nameSpace + "." + className + "Input"; + var outputStream = new MemoryStream(); + var outputWriter = new StreamWriter(outputStream); + + var jsg = new JsonClassGenerator + { + Example = examplesJson, + Namespace = finalNamespace, + MainClass = className, + OutputStream = outputWriter, + NoHelperClass = true, + UseProperties = true + }; + + jsg.GenerateClasses(); + + outputWriter.Flush(); + outputStream.Seek(0, SeekOrigin.Begin); + + var classDef = new StreamReader(outputStream) + .ReadToEnd() + .Replace("IList<", "List<"); + + classDef = + classDef.Substring(classDef.IndexOf(String.Format("namespace {0}", nameSpace), + StringComparison.Ordinal)); + + NamespacesToAdd.Add(finalNamespace); + + return new JsonFileGeneratedClass(this) + { + Namespace = finalNamespace, + ClassName = className, + DataFilePath = f, + ClassDefinition = classDef, + Success = true + }; + } + catch (Exception e) + { + return new JsonFileGeneratedClass(this) + { + DataFilePath = f, + Success = false, + Error = e + }; + } + }) + .ToList(); + } + + private List<string> GetInputFiles() + { + switch (InputType) + { + case JsonInputType.File: + return new List<string> { InputPath }; + case JsonInputType.Directory: + return + Directory.GetFiles(InputPath, Mask, + Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).ToList(); + default: + return new List<string>(); + } + } + + private List<JsonFileGeneratedClass> _generatedClasses = new List<JsonFileGeneratedClass>(); + public List<IGeneratedClass> GeneratedClasses { get { return _generatedClasses.OfType<IGeneratedClass>().ToList(); } } + public List<ExplorerItem> ExplorerItems { get; set; } + public List<string> NamespacesToAdd { get; set; } + + public List<string> ContextProperties => _generatedClasses + .Where(c=> c.Success) + .Select(c => + String.Format( + "public IEnumerable<{0}.{1}> {2}s {{ get {{ return GetFileJsonInput<{0}.{1}>(@\"{3}\"); }} }}", + c.Namespace, c.ClassName, c.ClassName, c.DataFilePath)) + .ToList(); + + public List<string> Errors => _generatedClasses + .Where(c=> !c.Success) + .Select(e => String.Format(" {0} - {1}", e.DataFilePath, e.Error.Message)) + .ToList(); + } +} \ No newline at end of file diff --git a/JsonDataContextDriver/JsonInputType.cs b/JsonDataContextDriver/Inputs/JsonInputType.cs similarity index 100% rename from JsonDataContextDriver/JsonInputType.cs rename to JsonDataContextDriver/Inputs/JsonInputType.cs diff --git a/JsonDataContextDriver/Inputs/JsonTextGeneratedClass.cs b/JsonDataContextDriver/Inputs/JsonTextGeneratedClass.cs new file mode 100644 index 0000000..84dfe8f --- /dev/null +++ b/JsonDataContextDriver/Inputs/JsonTextGeneratedClass.cs @@ -0,0 +1,25 @@ +using System; + +namespace JsonDataContextDriver +{ + public class JsonTextGeneratedClass : IGeneratedClass + { + public JsonTextGeneratedClass(JsonTextInput input) + { + Input = input; + } + + public string Namespace { get; set; } + public string ClassName { get; set; } + public string ClassDefinition { get; set; } + public bool Success { get; set; } + public Exception Error { get; set; } + + public JsonTextInput Input { get; set; } + IJsonInput IGeneratedClass.OriginalInput + { + get { return Input; } + set { Input = (JsonTextInput)value; } + } + } +} \ No newline at end of file diff --git a/JsonDataContextDriver/Inputs/JsonTextInput.cs b/JsonDataContextDriver/Inputs/JsonTextInput.cs new file mode 100644 index 0000000..016eff6 --- /dev/null +++ b/JsonDataContextDriver/Inputs/JsonTextInput.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.IO; +using System.Runtime.Serialization; +using LINQPad.Extensibility.DataContext; +using Newtonsoft.Json; +using PropertyChanged; +using Xamasoft.JsonClassGenerator; +using JsonDataContextDriver; + +namespace JsonDataContextDriver +{ + [ImplementPropertyChanged] + public class JsonTextInput : IJsonInput + { + public string InputGuid { get; set; } + public string Name { get; set; } + public string Json { get; set; } + + private JsonTextGeneratedClass _generatedClass; + + public JsonTextInput() + { + InputGuid = Guid.NewGuid().ToString(); + NamespacesToAdd = new List<string>(); + } + + public override string ToString() + { + var summary = ""; + try { summary = String.Format(" : {0} rows of json text", JsonConvert.DeserializeObject<List<ExpandoObject>>(Json).Count); } catch { } + + return String.Format("{0}{1}", Name, summary); + } + + public void GenerateClasses(string nameSpace) + { + try + { + var className = Name.SanitiseClassName(); + + var finalNamespace = nameSpace + "." + className + "Input"; + var outputStream = new MemoryStream(); + var outputWriter = new StreamWriter(outputStream); + + var jsg = new JsonClassGenerator + { + Example = Json, + Namespace = finalNamespace, + MainClass = className, + OutputStream = outputWriter, + NoHelperClass = true, + UseProperties = true + }; + + jsg.GenerateClasses(); + + outputWriter.Flush(); + outputStream.Seek(0, SeekOrigin.Begin); + + var classDef = new StreamReader(outputStream) + .ReadToEnd() + .Replace("IList<", "List<"); + + classDef = + classDef.Substring(classDef.IndexOf(String.Format("namespace {0}", nameSpace), + StringComparison.Ordinal)); + + NamespacesToAdd.Add(finalNamespace); + + _generatedClass = new JsonTextGeneratedClass(this) + { + Namespace = finalNamespace, + ClassName = className, + ClassDefinition = classDef, + Success = true + }; + } + catch (Exception e) + { + _generatedClass = new JsonTextGeneratedClass(this) + { + Success = false, + Error = e + }; + } + } + + [JsonIgnore] + public List<IGeneratedClass> GeneratedClasses { get { return new List<IGeneratedClass> {_generatedClass}; } } + [JsonIgnore] + public List<ExplorerItem> ExplorerItems { get; set; } + [JsonIgnore] + public List<string> NamespacesToAdd { get; set; } + + [JsonIgnore] + public List<string> ContextProperties => new List<string> + { + String.Format("public IEnumerable<{0}.{1}> {1} {{ get {{ return GetTextJsonInput<{0}.{1}>(\"{2}\"); }}}}", _generatedClass.Namespace, _generatedClass.ClassName, InputGuid) + }; + + [JsonIgnore] + public List<string> Errors { get { return _generatedClass == null || _generatedClass.Success ? new List<string>() : new List<string> { _generatedClass.Error.Message }; } } + } +} \ No newline at end of file diff --git a/JsonDataContextDriver/Inputs/JsonUrlGeneratedClass.cs b/JsonDataContextDriver/Inputs/JsonUrlGeneratedClass.cs new file mode 100644 index 0000000..3a0647b --- /dev/null +++ b/JsonDataContextDriver/Inputs/JsonUrlGeneratedClass.cs @@ -0,0 +1,28 @@ +using System; + +namespace JsonDataContextDriver +{ + public class JsonUrlGeneratedClass : IGeneratedClass + { + public JsonUrlGeneratedClass(JsonUrlInput input) + { + OriginalInput = input; + } + + public string OriginalName { get; set; } + public string Url { get; set; } + + public string Namespace { get; set; } + public string ClassName { get; set; } + public string ClassDefinition { get; set; } + public bool Success { get; set; } + public Exception Error { get; set; } + + public JsonUrlInput OriginalInput; + IJsonInput IGeneratedClass.OriginalInput + { + get { return OriginalInput; } + set { OriginalInput = (JsonUrlInput) value; } + } + } +} \ No newline at end of file diff --git a/JsonDataContextDriver/Inputs/JsonUrlInput.cs b/JsonDataContextDriver/Inputs/JsonUrlInput.cs new file mode 100644 index 0000000..73859c9 --- /dev/null +++ b/JsonDataContextDriver/Inputs/JsonUrlInput.cs @@ -0,0 +1,200 @@ +using System; +using System.CodeDom; +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Web; +using LINQPad.Extensibility.DataContext; +using Newtonsoft.Json; +using PropertyChanged; +using Xamasoft.JsonClassGenerator; + +namespace JsonDataContextDriver +{ + [ImplementPropertyChanged] + public class JsonUrlInput : IJsonInput + { + public string Name { get; set; } + public string Url { get; set; } + public bool GenerateAsMethod { get; set; } + + public JsonUrlInput() + { + NamespacesToAdd = new List<string>(); + Errors = new List<string>(); + } + + public void GenerateClasses(string nameSpace) + { + try + { + var req = (HttpWebRequest) HttpWebRequest.Create(Url); + req.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip; + req.UserAgent = @"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"; + + var examplesJson = ""; + using (var sr = new StreamReader(req.GetResponse().GetResponseStream())) + examplesJson = sr.ReadToEnd(); + + var className = Name.SanitiseClassName(); + var finalNamespace = nameSpace + "." + className + "Input"; + var outputStream = new MemoryStream(); + var outputWriter = new StreamWriter(outputStream); + + var jsg = new JsonClassGenerator + { + Example = examplesJson, + Namespace = finalNamespace, + MainClass = className, + OutputStream = outputWriter, + NoHelperClass = true, + UseProperties = true + }; + + jsg.GenerateClasses(); + + outputWriter.Flush(); + outputStream.Seek(0, SeekOrigin.Begin); + + var classDef = new StreamReader(outputStream) + .ReadToEnd() + .Replace("IList<", "List<"); + + classDef = + classDef.Substring(classDef.IndexOf(String.Format("namespace {0}", nameSpace), + StringComparison.Ordinal)); + + NamespacesToAdd.Add(finalNamespace); + + GeneratedClasses = new List<IGeneratedClass> + { + new JsonUrlGeneratedClass(this) + { + Namespace = finalNamespace, + ClassName = className, + Url = Url, + ClassDefinition = classDef, + Success = true + } + }; + } + catch (Exception e) + { + GeneratedClasses = new List<IGeneratedClass> + { + new JsonUrlGeneratedClass(this) + { + Url = Url, + Success = false, + Error = e + } + }; + } + + } + + [JsonIgnore] + public List<IGeneratedClass> GeneratedClasses { get; set; } + + [JsonIgnore] + public List<ExplorerItem> ExplorerItems => + GeneratedClasses + .OfType<JsonUrlGeneratedClass>() + .Where(c => c.OriginalInput.GenerateAsMethod) + .Select( + c => new ExplorerItem(GetNameForMethod(c), ExplorerItemKind.QueryableObject, ExplorerIcon.Box) + { + Children = new List<ExplorerItem> { + new ExplorerItem("Parameters", ExplorerItemKind.Category, ExplorerIcon.Schema) + { + Children = c.OriginalInput.GetUrlQueryStringParameters() + .Select(p=> new ExplorerItem(p.Item1, ExplorerItemKind.Parameter, ExplorerIcon.Parameter)) + .ToList() + }, + } + }) + .ToList(); + + [JsonIgnore] + public List<string> NamespacesToAdd { get; set; } + + [JsonIgnore] + public List<string> ContextProperties => + GeneratedClasses + .OfType<JsonUrlGeneratedClass>() + .Select(GetContextMethod) + .ToList(); + + [JsonIgnore] + public List<string> Errors { get; set; } + + private static string GetContextMethod(JsonUrlGeneratedClass c) + { + if (c.OriginalInput.GenerateAsMethod) + { + var ns = c.Namespace; + var name = GetNameForMethod(c); + var cls = c.ClassName; + + var url = c.Url; + var ps = c.OriginalInput.GetUrlQueryStringParameters(); + + var args = String.Join(", ", ps.Select(p => String.Format("string {0} = {1}", p.Item1, ToLiteral(p.Item2)))); + var prototype = String.Format("public IEnumerable<{0}.{1}> {2}({3})", ns, cls, name, args); + + var methodBody = + String.Format("var _____uri = new UriBuilder(@\"{0}\");\r\n", url) + + String.Format("var _____q = HttpUtility.ParseQueryString(_____uri.Query);\r\n\r\n") + + String.Join(Environment.NewLine, ps.Select(q => String.Format("_____q[\"{0}\"] = {0};", q.Item1))) + "\r\n\r\n" + + String.Format("_____uri.Query = _____q.ToString();\r\n") + + String.Format("return GetUrlParameterlessInput<{0}.{1}>(_____uri.ToString());", ns, cls); + + return String.Format("{0}\r\n{{\r\n{1}\r\n}}", prototype, methodBody); + } + else + return String.Format( + "public IEnumerable<{0}.{1}> {2}s {{ get {{ return GetUrlParameterlessInput<{0}.{1}>(@\"{3}\"); }} }}", + c.Namespace, c.ClassName, c.ClassName, c.Url); + } + + private List<Tuple<string, string>> GetUrlQueryStringParameters() + { + var uri = new UriBuilder(Url); + var pcol = HttpUtility.ParseQueryString(uri.Query); + var ps = pcol + .AllKeys + .Select(k => Tuple.Create(k, pcol[k])) + .ToList(); + return ps; + } + + private static string GetNameForMethod(JsonUrlGeneratedClass c) + { + return String.Format("Get{0}", c.ClassName.SanitiseClassName()); + } + + private static string ToLiteral(string input) + { + using (var writer = new StringWriter()) + { + using (var provider = CodeDomProvider.CreateProvider("CSharp")) + { + provider.GenerateCodeFromExpression(new CodePrimitiveExpression(input), writer, new CodeGeneratorOptions { IndentString = "\t" }); + var literal = writer.ToString(); + literal = literal.Replace(string.Format("\" +{0}\t\"", Environment.NewLine), ""); + return literal; + } + } + } + + public override string ToString() + { + return GenerateAsMethod + ? String.Format("{0}: {1} with parameters ({2})", this.Name, new Uri(this.Url).AbsolutePath, + String.Join(", ", GetUrlQueryStringParameters().Select(p => p.Item1))) + : String.Format("{0}: {1}", this.Name, this.Url); + } + } +} \ No newline at end of file diff --git a/JsonDataContextDriver/JsonDataContextDriver.csproj b/JsonDataContextDriver/JsonDataContextDriver.csproj index 0205a79..42e3649 100644 --- a/JsonDataContextDriver/JsonDataContextDriver.csproj +++ b/JsonDataContextDriver/JsonDataContextDriver.csproj @@ -41,7 +41,7 @@ </PropertyGroup> <ItemGroup> <Reference Include="LINQPad"> - <HintPath>..\..\..\..\Desktop\LINQPad\LINQPad.exe</HintPath> + <HintPath>..\..\..\..\Dropbox\code\LINQPad4\LINQPad.exe</HintPath> <Private>False</Private> </Reference> <Reference Include="Newtonsoft.Json, Version=6.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> @@ -58,6 +58,7 @@ <Reference Include="System" /> <Reference Include="System.Core" /> <Reference Include="System.Drawing" /> + <Reference Include="System.Web" /> <Reference Include="System.Windows.Forms" /> <Reference Include="System.Xaml" /> <Reference Include="System.Xml.Linq" /> @@ -88,6 +89,16 @@ </ItemGroup> <ItemGroup> <Compile Include="Extensions\NotepadHelper.cs" /> + <Compile Include="Inputs\IGeneratedClass.cs" /> + <Compile Include="Inputs\IJsonInput.cs" /> + <Compile Include="Inputs\JsonTextGeneratedClass.cs" /> + <Compile Include="Inputs\JsonTextInput.cs" /> + <Compile Include="Views\AddNewUrlSourceDialog.xaml.cs"> + <DependentUpon>AddNewUrlSourceDialog.xaml</DependentUpon> + </Compile> + <Compile Include="Views\AddNewTextSourceDialog.xaml.cs"> + <DependentUpon>AddNewTextSourceDialog.xaml</DependentUpon> + </Compile> <Compile Include="Views\AddNewFolderSourceDialog.xaml.cs"> <DependentUpon>AddNewFolderSourceDialog.xaml</DependentUpon> </Compile> @@ -97,17 +108,27 @@ <Compile Include="Extensions\Extensions.cs" /> <Compile Include="Extensions\FolderSelectDialog\FolderSelectDialog.cs" /> <Compile Include="Extensions\FolderSelectDialog\Reflector.cs" /> - <Compile Include="GeneratedClass.cs" /> + <Compile Include="Inputs\JsonFileGeneratedClass.cs" /> <Compile Include="JsonDynamicDataContextDriver.cs" /> <Compile Include="Views\ConnectionDialog.xaml.cs"> <DependentUpon>ConnectionDialog.xaml</DependentUpon> </Compile> - <Compile Include="JsonInput.cs" /> - <Compile Include="JsonInputType.cs" /> + <Compile Include="Inputs\JsonFileInput.cs" /> + <Compile Include="Inputs\JsonInputType.cs" /> <Compile Include="Extensions\LinqPadSampleCode.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="Inputs\JsonUrlGeneratedClass.cs" /> + <Compile Include="Inputs\JsonUrlInput.cs" /> </ItemGroup> <ItemGroup> + <Page Include="Views\AddNewUrlSourceDialog.xaml"> + <Generator>MSBuild:Compile</Generator> + <SubType>Designer</SubType> + </Page> + <Page Include="Views\AddNewTextSourceDialog.xaml"> + <Generator>MSBuild:Compile</Generator> + <SubType>Designer</SubType> + </Page> <Page Include="Views\AddNewFolderSourceDialog.xaml"> <Generator>MSBuild:Compile</Generator> <SubType>Designer</SubType> diff --git a/JsonDataContextDriver/JsonDynamicDataContextDriver.cs b/JsonDataContextDriver/JsonDynamicDataContextDriver.cs index 1009144..91de480 100644 --- a/JsonDataContextDriver/JsonDynamicDataContextDriver.cs +++ b/JsonDataContextDriver/JsonDynamicDataContextDriver.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using System.Text; +using System.Web; using System.Windows.Input; using JsonDataContext; using JsonDataContextDriver.Notepad; @@ -46,7 +47,7 @@ public override bool ShowConnectionDialog(IConnectionInfo cxInfo, bool isNewConn public override IEnumerable<string> GetAssembliesToAdd(IConnectionInfo cxInfo) { return base.GetAssembliesToAdd(cxInfo) - .Concat(new[] {typeof (JsonDataContextBase).Assembly.Location}); + .Concat(new[] {typeof (JsonDataContextBase).Assembly.Location, typeof(HttpUtility).Assembly.Location}); } public override IEnumerable<string> GetNamespacesToAdd(IConnectionInfo cxInfo) @@ -56,30 +57,40 @@ public override IEnumerable<string> GetNamespacesToAdd(IConnectionInfo cxInfo) } private List<string> _nameSpacesToAdd = new List<string>(); + public override List<ExplorerItem> GetSchemaAndBuildAssembly(IConnectionInfo cxInfo, AssemblyName assemblyToBuild, ref string nameSpace, ref string typeName) { _nameSpacesToAdd = new List<string>(); - + var xInputs = cxInfo.DriverData.Element("inputDefs"); if (xInputs == null) return new List<ExplorerItem>(); var jss = new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.All}; - var inputDefs = JsonConvert.DeserializeObject<List<JsonInput>>(xInputs.Value, jss); + var inputDefs = JsonConvert.DeserializeObject<List<IJsonInput>>(xInputs.Value, jss).ToList(); var ns = nameSpace; // generate class definitions var classDefinitions = inputDefs - .SelectMany(i => GetClassesForInput(i, ns)) + .AsParallel() + .SelectMany(i => + { + i.GenerateClasses(ns); + return i.GeneratedClasses; + }) .ToList(); + // add namespaces + _nameSpacesToAdd.AddRange(inputDefs.SelectMany(i=>i.NamespacesToAdd)); + // remove the error'd inputs - var classGenErrors = classDefinitions.Where(c => !c.Success).ToList(); + var classGenErrors = inputDefs.SelectMany(i => i.Errors).ToList(); + classDefinitions = classDefinitions .Where(c => c.Success) @@ -98,14 +109,11 @@ public override List<ExplorerItem> GetSchemaAndBuildAssembly(IConnectionInfo cxI "using System.Collections.Generic;\r\n" + "using System.IO;\r\n" + "using Newtonsoft.Json;\r\n" + + "using System.Web;\r\n" + "using JsonDataContext;\r\n"; var contextProperties = - classDefinitions.Select( - c => - String.Format( - "public IEnumerable<{0}.{1}> {2}s {{ get {{ return DeserializeSequenceFromJsonFile<{0}.{1}>(@\"{3}\"); }} }}", - c.Namespace, c.ClassName, c.ClassName, c.DataFilePath)); + inputDefs.SelectMany(i => i.ContextProperties); var context = String.Format("namespace {1} {{\r\n\r\n public class {2} : JsonDataContextBase {{\r\n\r\n\t\t{0}\r\n\r\n}}\r\n\r\n}}", @@ -122,7 +130,10 @@ public override List<ExplorerItem> GetSchemaAndBuildAssembly(IConnectionInfo cxI ReferencedAssemblies = { typeof (JsonDataContextBase).Assembly.Location, - typeof (JsonConvert).Assembly.Location + typeof (JsonConvert).Assembly.Location, + + typeof (UriBuilder).Assembly.Location, + typeof (HttpUtility).Assembly.Location } }; @@ -133,12 +144,13 @@ public override List<ExplorerItem> GetSchemaAndBuildAssembly(IConnectionInfo cxI // Pray to the gods of UX for redemption.. // We Can Do Better if (classGenErrors.Any()) - MessageBox.Show(String.Format("Couldn't process {0} files:\r\n{1}", classGenErrors.Count, - String.Join(Environment.NewLine, - classGenErrors.Select(e => String.Format("{0} - {1}", e.DataFilePath, e.Error.Message))))); - + MessageBox.Show(String.Format("Couldn't process {0} inputs:\r\n{1}", classGenErrors.Count, + String.Join(Environment.NewLine, classGenErrors))); + return - LinqPadSampleCode.GetSchema(result.CompiledAssembly.GetType(String.Format("{0}.{1}", nameSpace, typeName))); + LinqPadSampleCode.GetSchema(result.CompiledAssembly.GetType(String.Format("{0}.{1}", nameSpace, typeName))) + .Concat(inputDefs.SelectMany(i=>i.ExplorerItems??new List<ExplorerItem>())) + .ToList(); } else { @@ -152,136 +164,35 @@ public override List<ExplorerItem> GetSchemaAndBuildAssembly(IConnectionInfo cxI if (classGenErrors.Any()) { sb.AppendLine("\r\nThis may have been caused by the following class generation errors:\r\n"); - sb.AppendLine(String.Join(Environment.NewLine, classGenErrors.Select(e => String.Format(" {0} - {1}", e.DataFilePath, e.Error.Message)))); + sb.AppendLine(String.Join(Environment.NewLine, String.Join(Environment.NewLine, classGenErrors))); } MessageBox.Show(sb.ToString()); - if (Keyboard.Modifiers == ModifierKeys.Shift) - NotepadHelper.ShowMessage(contextWithCode, "Generated source code"); + NotepadHelper.ShowMessage(contextWithCode, "Generated source code"); throw new Exception("Could not generate a typed context for the given inputs"); } } - public List<GeneratedClass> GetClassesForInput(JsonInput input, string nameSpace) + public override void InitializeContext(IConnectionInfo cxInfo, object context, QueryExecutionManager executionManager) { - var numSamples = input.NumRowsToSample; + + base.InitializeContext(cxInfo, context, executionManager); + + var ctx = (JsonDataContextBase) context; - return - GetFilesForInput(input) - .Select(f => - { - // TODO: Be a better error handler - try - { - var fs = new FileStream(f, FileMode.Open); - var sr = new StreamReader(fs); - var jtr = new JsonTextReader(sr); - - var examples = - Enumerable - .Range(0, numSamples) - .Select(_ => - { - while (jtr.Read()) - if (jtr.TokenType == JsonToken.StartObject) - return JObject.Load(jtr).ToString(); - return null; - }) - .Where(json => json != null); - - var examplesJson = String.Format("[{0}]", String.Join(",\r\n", examples)); - - jtr.Close(); - sr.Close(); - fs.Close(); - - var sanitise = new Func<Func<string, string>>(() => - { - var replacers = new[] - { - "\n", "'", " ", "*", "/", "-", "(", ")", ".", "!", "?", "#", ":", "+", "{", "}", "&", - "," - }; - var tuples = replacers.Select(r => Tuple.Create(r, "_")).ToList(); - - return originalName => - { - var newName = originalName.ReplaceAll(tuples); - if (char.IsNumber(newName[0])) - newName = "_" + newName; - - return newName; - }; - })(); - - var className = sanitise(Path.GetFileNameWithoutExtension(f)); - var finalNamespace = nameSpace + "." + className + "Input"; - var outputStream = new MemoryStream(); - var outputWriter = new StreamWriter(outputStream); - - var jsg = new JsonClassGenerator - { - Example = examplesJson, - Namespace = finalNamespace, - MainClass = className, - OutputStream = outputWriter, - NoHelperClass = true, - }; - - jsg.GenerateClasses(); - - outputWriter.Flush(); - outputStream.Seek(0, SeekOrigin.Begin); - - var classDef = new StreamReader(outputStream) - .ReadToEnd() - .Replace("IList<", "List<") - .Replace(";\r\n", " { get; set; }\r\n"); - - classDef = - classDef.Substring(classDef.IndexOf(String.Format("namespace {0}", nameSpace), - StringComparison.Ordinal)); - - _nameSpacesToAdd.Add(finalNamespace); - - return new GeneratedClass - { - Namespace = finalNamespace, - ClassName = className, - DataFilePath = f, - ClassDefinition = classDef, - Success = true - }; - } - catch (Exception e) - { - return new GeneratedClass - { - DataFilePath = f, - Success = false, - Error = e - }; - } - }) - .ToList(); - } + var xInputs = cxInfo.DriverData.Element("inputDefs"); + if (xInputs == null) + return; - public List<string> GetFilesForInput(JsonInput input) - { - switch (input.InputType) - { - case JsonInputType.File: - return new List<string> {input.InputPath}; - case JsonInputType.Directory: - return - Directory.GetFiles(input.InputPath, input.Mask, - input.Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).ToList(); - default: - return new List<string>(); - } + var jss = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }; + var inputs = JsonConvert.DeserializeObject<List<IJsonInput>>(xInputs.Value, jss).ToList(); + + inputs + .OfType<JsonTextInput>() + .ToList() + .ForEach(c=> ctx._jsonTextInputs.Add(c.InputGuid, c.Json)); } } - } \ No newline at end of file diff --git a/JsonDataContextDriver/JsonInput.cs b/JsonDataContextDriver/JsonInput.cs deleted file mode 100644 index 2a4996b..0000000 --- a/JsonDataContextDriver/JsonInput.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.IO; -using System.Runtime.Serialization; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Media; -using JsonDataContextDriver; -using PropertyChanged; - -namespace JsonDataContextDriver -{ - [ImplementPropertyChanged] - public class JsonInput - { - public string InputPath { get; set; } - - public string Mask { get; set; } - - public bool Recursive { get; set; } - - public int NumRowsToSample { get; set; } - - public JsonInputType InputType - { - get - { - if (string.IsNullOrWhiteSpace(InputPath)) - return JsonInputType.Nothing; - if (File.Exists(InputPath)) - return JsonInputType.File; - if (Directory.Exists(InputPath)) - return JsonInputType.Directory; - return JsonInputType.Invalid; - } - } - - public bool IsDirectory => InputType == JsonInputType.Directory; - - public JsonInput() - { - NumRowsToSample = 1000; - } - - public override string ToString() - { - switch (InputType) - { - case JsonInputType.File: - return InputPath; - case JsonInputType.Directory: - return Path.Combine(InputPath, Mask ?? "*.*") + (Recursive ? " + subfolders" : ""); - default: - return "ERR"; - } - } - } -} \ No newline at end of file diff --git a/JsonDataContextDriver/Properties/AssemblyInfo.cs b/JsonDataContextDriver/Properties/AssemblyInfo.cs index b87dc98..a1b45f3 100644 --- a/JsonDataContextDriver/Properties/AssemblyInfo.cs +++ b/JsonDataContextDriver/Properties/AssemblyInfo.cs @@ -36,5 +36,5 @@ // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.0.2")] -[assembly: AssemblyFileVersion("0.0.2")] \ No newline at end of file +[assembly: AssemblyVersion("0.0.3")] +[assembly: AssemblyFileVersion("0.0.3")] \ No newline at end of file diff --git a/JsonDataContextDriver/Views/AddNewFileSourceDialog.xaml b/JsonDataContextDriver/Views/AddNewFileSourceDialog.xaml index 0ec742a..ca3f00e 100644 --- a/JsonDataContextDriver/Views/AddNewFileSourceDialog.xaml +++ b/JsonDataContextDriver/Views/AddNewFileSourceDialog.xaml @@ -25,7 +25,7 @@ <TextBlock Grid.Row="1" Grid.Column="0" Text="Rows to sample:" Margin="5" HorizontalAlignment="Right" /> <TextBox Name="NumRowsToSampleTextBox" Grid.Row="1" Grid.Column="1" Margin="5" Width="50" - HorizontalAlignment="Left" HorizontalContentAlignment="Center" Text="1000" /> + HorizontalAlignment="Left" HorizontalContentAlignment="Center" Text="50" /> </Grid> <StackPanel Orientation="Horizontal" HorizontalAlignment="Right"> diff --git a/JsonDataContextDriver/Views/AddNewFileSourceDialog.xaml.cs b/JsonDataContextDriver/Views/AddNewFileSourceDialog.xaml.cs index 4822a8a..d686363 100644 --- a/JsonDataContextDriver/Views/AddNewFileSourceDialog.xaml.cs +++ b/JsonDataContextDriver/Views/AddNewFileSourceDialog.xaml.cs @@ -23,13 +23,13 @@ public partial class AddNewFileSourceDialog : Window private readonly SolidColorBrush _goodBrush = new SolidColorBrush(Colors.White); private readonly SolidColorBrush _badBrush = new SolidColorBrush(Colors.IndianRed) {Opacity = .5}; - private readonly JsonInput _input; + private readonly JsonFileInput _input; - public JsonInput Input { get; set; } + public JsonFileInput Input { get; set; } public AddNewFileSourceDialog() { - _input = new JsonInput(); + _input = new JsonFileInput(); InitializeComponent(); @@ -77,9 +77,11 @@ public AddNewFileSourceDialog() }; doValidation(); + + PathTextBox.Focus(); } - public AddNewFileSourceDialog(JsonInput input) : this() + public AddNewFileSourceDialog(JsonFileInput input) : this() { _input = input; diff --git a/JsonDataContextDriver/Views/AddNewFolderSourceDialog.xaml b/JsonDataContextDriver/Views/AddNewFolderSourceDialog.xaml index d5d60ca..b8c2584 100644 --- a/JsonDataContextDriver/Views/AddNewFolderSourceDialog.xaml +++ b/JsonDataContextDriver/Views/AddNewFolderSourceDialog.xaml @@ -3,7 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - mc:Ignorable="d" Height="220" Width="450" + mc:Ignorable="d" Height="200" Width="450" WindowStartupLocation="CenterOwner" Title="Json Folder Source"> @@ -37,7 +37,7 @@ <TextBlock Grid.Row="3" Grid.Column="0" Text="Rows to sample:" Margin="5" HorizontalAlignment="Right" /> <TextBox Name="NumRowsToSampleTextBox" Grid.Row="3" Grid.Column="1" Margin="5" Width="50" - HorizontalAlignment="Left" HorizontalContentAlignment="Center" Text="1000" /> + HorizontalAlignment="Left" HorizontalContentAlignment="Center" Text="50" /> </Grid> diff --git a/JsonDataContextDriver/Views/AddNewFolderSourceDialog.xaml.cs b/JsonDataContextDriver/Views/AddNewFolderSourceDialog.xaml.cs index 5f8a49c..aaebbb8 100644 --- a/JsonDataContextDriver/Views/AddNewFolderSourceDialog.xaml.cs +++ b/JsonDataContextDriver/Views/AddNewFolderSourceDialog.xaml.cs @@ -23,13 +23,13 @@ public partial class AddNewFolderSourceDialog : Window private readonly SolidColorBrush _goodBrush = new SolidColorBrush(Colors.White); private readonly SolidColorBrush _badBrush = new SolidColorBrush(Colors.IndianRed) {Opacity = .5}; - private readonly JsonInput _input; + private readonly JsonFileInput _input; - public JsonInput Input { get; set; } + public JsonFileInput Input { get; set; } public AddNewFolderSourceDialog() { - _input = new JsonInput(); + _input = new JsonFileInput(); InitializeComponent(); @@ -74,9 +74,11 @@ public AddNewFolderSourceDialog() }; doValidation(); + + PathTextBox.Focus(); } - public AddNewFolderSourceDialog(JsonInput input) : this() + public AddNewFolderSourceDialog(JsonFileInput input) : this() { _input = input; diff --git a/JsonDataContextDriver/Views/AddNewTextSourceDialog.xaml b/JsonDataContextDriver/Views/AddNewTextSourceDialog.xaml new file mode 100644 index 0000000..10aebfa --- /dev/null +++ b/JsonDataContextDriver/Views/AddNewTextSourceDialog.xaml @@ -0,0 +1,35 @@ +<Window x:Class="JsonDataContextDriver.AddNewTextSourceDialog" + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + mc:Ignorable="d" Height="320" Width="450" + WindowStartupLocation="CenterOwner" + Title="Json Text Source"> + <StackPanel Margin="10,10,10,10" VerticalAlignment="Stretch"> + <Grid VerticalAlignment="Stretch"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + + <Grid.ColumnDefinitions> + <ColumnDefinition Width="Auto" /> + <ColumnDefinition Width="*" /> + </Grid.ColumnDefinitions> + + <TextBlock Grid.Row="0" Grid.Column="0" Text="Name:" Margin="5" HorizontalAlignment="Right" /> + <TextBox Name="NameTextBox" Grid.Row="0" Grid.Column="1" Margin="5" Width="200" HorizontalAlignment="Left" /> + + <TextBlock Grid.Row="1" Grid.Column="0" Text="Json:" Margin="5" HorizontalAlignment="Right" /> + <TextBox Name="JsonTextBox" FontFamily="Consolas" Grid.Row="1" Grid.Column="1" Margin="5" TextWrapping="Wrap" AcceptsReturn="True" AcceptsTab="True" + HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" Text="" Height="180"/> + </Grid> + + <StackPanel Orientation="Horizontal" HorizontalAlignment="Right"> + <Button Name="OkButton" Width="60" Content="OK" Margin="5" /> + <Button Name="CancelButton" Width="60" Content="Cancel" Margin="5" /> + </StackPanel> + + </StackPanel> +</Window> \ No newline at end of file diff --git a/JsonDataContextDriver/Views/AddNewTextSourceDialog.xaml.cs b/JsonDataContextDriver/Views/AddNewTextSourceDialog.xaml.cs new file mode 100644 index 0000000..e44b31f --- /dev/null +++ b/JsonDataContextDriver/Views/AddNewTextSourceDialog.xaml.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; +using JsonDataContextDriver; +using Microsoft.Win32; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace JsonDataContextDriver +{ + public partial class AddNewTextSourceDialog : Window + { + private readonly SolidColorBrush _goodBrush = new SolidColorBrush(Colors.White); + private readonly SolidColorBrush _badBrush = new SolidColorBrush(Colors.IndianRed) {Opacity = .5}; + + private readonly JsonTextInput _input; + + public JsonTextInput Input { get; set; } + + public AddNewTextSourceDialog() + { + _input = new JsonTextInput(); + + InitializeComponent(); + + // sigh, too bad reactiveui doesn't support .net 4 + Action doValidation = () => + { + var name = NameTextBox.Text; + var json = JsonTextBox.Text; + + bool validJson = false; + try + { + var obj = JContainer.Parse(json); + validJson = true; + } catch { } + + var nameOk = String.IsNullOrEmpty(name) || char.IsLetter(name.ToCharArray()[0]); + var jsonOk = validJson || String.IsNullOrEmpty(json); + + OkButton.IsEnabled = (nameOk && jsonOk && !String.IsNullOrEmpty(json) && !string.IsNullOrWhiteSpace(name)); + + NameTextBox.Background = nameOk ? _goodBrush : _badBrush; + JsonTextBox.Background = jsonOk ? _goodBrush : _badBrush; + }; + + NameTextBox.TextChanged += (sender, args) => doValidation(); + JsonTextBox.TextChanged += (sender, args) => doValidation(); + + CancelButton.Click += (sender, args) => DialogResult = false; + OkButton.Click += (sender, args) => + { + _input.Name = NameTextBox.Text; + _input.Json = JsonTextBox.Text; + + Input = _input; + DialogResult = true; + }; + + doValidation(); + + NameTextBox.Focus(); + } + + public AddNewTextSourceDialog(JsonTextInput input) : this() + { + _input = input; + + NameTextBox.Text = _input.Name; + JsonTextBox.Text = _input.Json; + } + } +} \ No newline at end of file diff --git a/JsonDataContextDriver/Views/AddNewUrlSourceDialog.xaml b/JsonDataContextDriver/Views/AddNewUrlSourceDialog.xaml new file mode 100644 index 0000000..a582ba8 --- /dev/null +++ b/JsonDataContextDriver/Views/AddNewUrlSourceDialog.xaml @@ -0,0 +1,55 @@ +<Window x:Class="JsonDataContextDriver.AddNewUrlSourceDialog" + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:jsonDataContextDriver="clr-namespace:JsonDataContextDriver" + mc:Ignorable="d" Height="290" Width="450" + WindowStartupLocation="CenterOwner" + Title="Json Web Source"> + <Window.Resources> + <jsonDataContextDriver:MockXViewModel x:Key="DesignViewModel"/> + </Window.Resources> + <StackPanel Margin="10,10,10,10" VerticalAlignment="Stretch"> + <Grid VerticalAlignment="Stretch"> + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + + <Grid.ColumnDefinitions> + <ColumnDefinition Width="Auto" /> + <ColumnDefinition Width="*" /> + </Grid.ColumnDefinitions> + + <TextBlock Grid.Row="0" Grid.Column="0" Text="Name:" Margin="5" HorizontalAlignment="Right" /> + <TextBox Name="NameTextBox" Grid.Row="0" Grid.Column="1" Margin="5" Width="200" HorizontalAlignment="Left" /> + + <TextBlock Grid.Row="1" Grid.Column="0" Text="Url:" Margin="5" HorizontalAlignment="Right" /> + <TextBox Name="UrlTextBox" Grid.Row="1" Grid.Column="1" Margin="5" HorizontalAlignment="Stretch" /> + + <TextBlock Text="Expose as:" Grid.Column="0" Grid.Row="2" Margin="5" HorizontalAlignment="Right"></TextBlock> + <StackPanel Grid.Row="2" Grid.Column="1" Margin="5" HorizontalAlignment="Stretch" Orientation="Horizontal"> + <RadioButton Name="ExposeAsPropertyCheckBox" Content="Property" Margin="0,0,10,0" IsChecked="True"/> + <RadioButton Name="ExposeAsMethodCheckbox" Content="Method with parameters"/> + </StackPanel> + + <ListView Name="ParametersListView" Grid.Row="3" Grid.Column="1" HorizontalAlignment="Stretch" ItemsSource="{Binding Source={StaticResource DesignViewModel}, Path=Items}" Height="100"> + <ListView.View> + <GridView> + <GridViewColumn DisplayMemberBinding="{Binding Item1}" Header="Parameter" Width="120"/> + <GridViewColumn DisplayMemberBinding="{Binding Item2}" Header="Default Value" Width="120"/> + </GridView> + </ListView.View> + </ListView> + </Grid> + + <StackPanel Orientation="Horizontal" HorizontalAlignment="Right"> + <Button Name="OkButton" Width="60" Content="OK" Margin="5" /> + <Button Name="CancelButton" Width="60" Content="Cancel" Margin="5" /> + </StackPanel> + + </StackPanel> +</Window> \ No newline at end of file diff --git a/JsonDataContextDriver/Views/AddNewUrlSourceDialog.xaml.cs b/JsonDataContextDriver/Views/AddNewUrlSourceDialog.xaml.cs new file mode 100644 index 0000000..72f34e4 --- /dev/null +++ b/JsonDataContextDriver/Views/AddNewUrlSourceDialog.xaml.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; +using JsonDataContextDriver; +using Microsoft.Win32; +using Newtonsoft.Json.Linq; +using Path = System.IO.Path; + +namespace JsonDataContextDriver +{ + public partial class AddNewUrlSourceDialog : Window + { + private readonly SolidColorBrush _goodBrush = new SolidColorBrush(Colors.White); + private readonly SolidColorBrush _badBrush = new SolidColorBrush(Colors.IndianRed) {Opacity = .5}; + + private readonly JsonUrlInput _input; + + public JsonUrlInput Input { get; set; } + + public AddNewUrlSourceDialog() + { + _input = new JsonUrlInput(); + + InitializeComponent(); + + // sigh, too bad reactiveui doesn't support .net 4 + Action doValidation = () => + { + var name = NameTextBox.Text; + var uri = UrlTextBox.Text; + + bool validUri = false; + try + { + var obj = new Uri(uri, UriKind.Absolute); + validUri = true; + } catch { } + + var nameOk = String.IsNullOrEmpty(name) || char.IsLetter(name.ToCharArray()[0]); + var jsonOk = validUri || String.IsNullOrEmpty(uri); + + OkButton.IsEnabled = (nameOk && jsonOk && !String.IsNullOrEmpty(name)); + + NameTextBox.Background = nameOk ? _goodBrush : _badBrush; + UrlTextBox.Background = jsonOk ? _goodBrush : _badBrush; + }; + + NameTextBox.TextChanged += (sender, args) => doValidation(); + UrlTextBox.TextChanged += (sender, args) => doValidation(); + + UrlTextBox.TextChanged += (sender, args) => + { + try + { + var uri = new Uri(UrlTextBox.Text); + var pcol = HttpUtility.ParseQueryString(uri.Query); + var ps = pcol + .AllKeys + .Select(k => Tuple.Create(k, pcol[k])) + .ToList(); + + ParametersListView.ItemsSource = ps; + + UrlTextBox.Background = _goodBrush; + } + catch + { + UrlTextBox.Background = _badBrush; + ParametersListView.ItemsSource = null; + } + + if (ParametersListView.Items.Count > 0) + ExposeAsMethodCheckbox.IsEnabled = true; + else + { + ExposeAsMethodCheckbox.IsEnabled = false; + ExposeAsPropertyCheckBox.IsChecked = true; + } + }; + + ExposeAsMethodCheckbox.Checked += (sender, args) => ParametersListView.Visibility = Visibility.Visible; + ExposeAsMethodCheckbox.Unchecked += (sender, args) => ParametersListView.Visibility = Visibility.Collapsed; + + + CancelButton.Click += (sender, args) => DialogResult = false; + OkButton.Click += (sender, args) => + { + _input.Name = NameTextBox.Text; + _input.Url = UrlTextBox.Text; + _input.GenerateAsMethod = ExposeAsMethodCheckbox.IsChecked == true; + + Input = _input; + DialogResult = true; + }; + + doValidation(); + + NameTextBox.Focus(); + } + + public AddNewUrlSourceDialog(JsonUrlInput input) : this() + { + _input = input; + + NameTextBox.Text = _input.Name; + UrlTextBox.Text = _input.Url; + ExposeAsMethodCheckbox.IsChecked = _input.GenerateAsMethod; + ExposeAsPropertyCheckBox.IsChecked = !_input.GenerateAsMethod; + } + + } + + public class MockXViewModel + { + public List<Tuple<string, string>> Items = new List<Tuple<string, string>> + { + Tuple.Create("param1", "value1"), + Tuple.Create("param2", "value2"), + Tuple.Create("param3", "value3"), + }; + + } + + +} \ No newline at end of file diff --git a/JsonDataContextDriver/Views/ConnectionDialog.xaml b/JsonDataContextDriver/Views/ConnectionDialog.xaml index c50d4b7..707172b 100644 --- a/JsonDataContextDriver/Views/ConnectionDialog.xaml +++ b/JsonDataContextDriver/Views/ConnectionDialog.xaml @@ -28,7 +28,11 @@ <xctk:DropDownButton Content=" Add" Margin="10,0,5,0"> <xctk:DropDownButton.DropDownContent> - <UniformGrid Rows="2" Columns="1"> + <UniformGrid Rows="4" Columns="1"> + <DockPanel Background="White"> + <TextBlock Name="NewTextMenuItem" Text="Text..." Margin="10,2,10,2" /> + </DockPanel> + <DockPanel Background="White"> <TextBlock Name="NewFileMenuItem" Text="File..." Margin="10,2,10,2" /> </DockPanel> @@ -37,7 +41,7 @@ <TextBlock Name="NewFolderMenuItem" Text="Folder..." Margin="10,2,10,2" /> </DockPanel> - <DockPanel Background="White" Visibility="Hidden"> + <DockPanel Background="White"> <TextBlock Name="NewWebMenuItem" Text="Web call..." Margin="10,2,10,2" /> </DockPanel> diff --git a/JsonDataContextDriver/Views/ConnectionDialog.xaml.cs b/JsonDataContextDriver/Views/ConnectionDialog.xaml.cs index 1435594..f64aabc 100644 --- a/JsonDataContextDriver/Views/ConnectionDialog.xaml.cs +++ b/JsonDataContextDriver/Views/ConnectionDialog.xaml.cs @@ -20,7 +20,7 @@ namespace JsonDataContextDriver public partial class ConnectionDialog { private IConnectionInfo _connectionInfo; - private readonly ObservableCollection<JsonInput> _jsonInputs = new ObservableCollection<JsonInput>(); + private readonly ObservableCollection<IJsonInput> _jsonInputs = new ObservableCollection<IJsonInput>(); private readonly SolidColorBrush _highlightedBrush = new SolidColorBrush(Colors.LightBlue); private readonly SolidColorBrush _standardBrush = new SolidColorBrush(Colors.White); @@ -30,7 +30,7 @@ public ConnectionDialog() RemoveButton.Click += (sender, args) => { - var input = InputsListView.SelectedItem as JsonInput; + var input = InputsListView.SelectedItem as IJsonInput; if (input == null) return; @@ -38,6 +38,17 @@ public ConnectionDialog() _jsonInputs.Remove(input); }; + NewTextMenuItem.MouseUp += (sender, args) => + { + var dialog = new AddNewTextSourceDialog() { Owner = this }; + var result = dialog.ShowDialog(); + + if (!(result.HasValue && result.Value)) + return; + + _jsonInputs.Add(dialog.Input); + }; + NewFileMenuItem.MouseUp += (sender, args) => { var dialog = new AddNewFileSourceDialog() { Owner = this }; @@ -60,14 +71,23 @@ public ConnectionDialog() _jsonInputs.Add(dialog.Input); }; - NewWebMenuItem.MouseUp += (sender, args) => MessageBox.Show("NOT IMPLEMENTED, EXCEPTION! >:O"); + NewWebMenuItem.MouseUp += (sender, args) => + { + var dialog = new AddNewUrlSourceDialog() { Owner = this }; + var result = dialog.ShowDialog(); + + if (!(result.HasValue && result.Value)) + return; + + _jsonInputs.Add(dialog.Input); + }; foreach ( var panel in new[] { (DockPanel) NewFileMenuItem.Parent, (DockPanel) NewFolderMenuItem.Parent, - (DockPanel) NewWebMenuItem.Parent + (DockPanel) NewWebMenuItem.Parent, (DockPanel) NewTextMenuItem.Parent }) { var p = panel; @@ -90,30 +110,50 @@ var panel in InputsListView.MouseDoubleClick += (sender, args) => { - var selectedItem = InputsListView.SelectedItem as JsonInput; + var selectedItem = InputsListView.SelectedItem; if (selectedItem == null) return; - if (selectedItem.IsDirectory) + if (selectedItem is JsonTextInput) { - var dialog = new AddNewFolderSourceDialog(selectedItem) { Owner = this }; + var jti = selectedItem as JsonTextInput; + var dialog = new AddNewTextSourceDialog(jti) { Owner = this }; dialog.ShowDialog(); } - else + else if (selectedItem is JsonUrlInput) { - var dialog = new AddNewFileSourceDialog(selectedItem) { Owner = this }; + var jui = selectedItem as JsonUrlInput; + var dialog = new AddNewUrlSourceDialog(jui) { Owner = this }; dialog.ShowDialog(); } - + else if (selectedItem is JsonFileInput) + { + var jfi = selectedItem as JsonFileInput; + if (jfi.IsDirectory) + { + var dialog = new AddNewFolderSourceDialog(jfi) { Owner = this }; + dialog.ShowDialog(); + } + else + { + var dialog = new AddNewFileSourceDialog(jfi) { Owner = this }; + dialog.ShowDialog(); + } + } }; Action checkCanOk = () => OkButton.IsEnabled = _jsonInputs.Count > 0; + Action checkCanRemove = () => RemoveButton.IsEnabled = InputsListView.SelectedItem != null; _jsonInputs.CollectionChanged += (sender, args) => checkCanOk(); + InputsListView.SelectionChanged += (sender, args) => checkCanRemove(); InputsListView.ItemsSource = _jsonInputs; checkCanOk(); + checkCanRemove(); + + ConnectionNameTextBox.Focus(); } public void SetContext(IConnectionInfo cxInfo, bool isNewConnection) @@ -126,7 +166,7 @@ public void SetContext(IConnectionInfo cxInfo, bool isNewConnection) if (xInputs == null) return; var jss = new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.All}; - var inputDefs = JsonConvert.DeserializeObject<List<JsonInput>>(xInputs.Value, jss); + var inputDefs = JsonConvert.DeserializeObject<List<IJsonInput>>(xInputs.Value, jss); _jsonInputs.Clear(); inputDefs.ForEach(_jsonInputs.Add); diff --git a/README.md b/README.md index 61c0c47..083ae20 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ A dynamic data context driver for querying JSON data with LINQPad. See [here](ht This driver allows you to select a set of JSON inputs from which a LINQPad data context will be generated for strongly typed access. A context can currently be built from any combination of: +- plain text inputs (for example, pasted in or typed by hand) - individual file paths -- directories to search, with a filemask (e.g. \*.json, \*.\*) and the option for recursive enumeration. - -For each input, you can also control the number of rows the driver samples when attempting to determine types. +- directories to search, with a filemask (e.g. \*.json, \*.\*) and the option for recursive enumeration +- GET calls to urls that return json, exposed either as properties on the context, or methods with parameters mapped to querystring components in the url ####Screenshots @@ -16,7 +16,7 @@ For each input, you can also control the number of rows the driver samples when ####Planned Features: -* support for grabbing JSON from the world wide web (GET or POST with parameters and/or request body) +* enhanced support for grabbing JSON from the world wide web (basic GET is currently supported, support for POST, basic request customisation (headers, etc.) planned) * support for caching of deserialised data, as well as programmatic invalidation of cached data * support for persisting new data to the context (for example, written to path in the context's search definitions), allowing you to build out your context as you go * better support wrapped JSON, as outlined below @@ -40,7 +40,7 @@ For example, one set of objects contained in a root object: or many sets of different objects, contained in a root object: ``` { - "cats": [ { ... } { ... } ], + "cats": [ { ... }, { ... } ], "dogs": [ { ... }, { ... } ], "trucks": [ { ... }, { ... } ], } @@ -49,7 +49,7 @@ In the future, this should be optionally detected and unwrapped (in the first ca #####Errors: -Errors encountered when processing individual sources will not typically prevent the construction of a full context; that is, 'bad' inputs will be ignored and the context is generated with 'good ones. If the driver fails on inputs that you are able to share, please include them when [filing an issue](https://github.com/rdavisau/jsondatacontext-linqpad/issues). +Errors encountered when processing individual sources will not typically prevent the construction of a full context; that is, 'bad' inputs will be ignored and the context is generated with 'good' ones. If the driver fails on inputs that you are able to share, please include them when [filing an issue](https://github.com/rdavisau/jsondatacontext-linqpad/issues). ####Contributing: