FormBuilderJson.cs
1 //
2 // Copyright (c) Microsoft. All rights reserved.
3 // Licensed under the MIT license.
4 //
5 // Microsoft Bot Framework: http://botframework.com
6 //
7 // Bot Builder SDK GitHub:
8 // https://github.com/Microsoft/BotBuilder
9 //
10 // Copyright (c) Microsoft Corporation
11 // All rights reserved.
12 //
13 // MIT License:
14 // Permission is hereby granted, free of charge, to any person obtaining
15 // a copy of this software and associated documentation files (the
16 // "Software"), to deal in the Software without restriction, including
17 // without limitation the rights to use, copy, modify, merge, publish,
18 // distribute, sublicense, and/or sell copies of the Software, and to
19 // permit persons to whom the Software is furnished to do so, subject to
20 // the following conditions:
21 //
22 // The above copyright notice and this permission notice shall be
23 // included in all copies or substantial portions of the Software.
24 //
25 // THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
26 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
27 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
29 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
30 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32 //
33 
36 using Microsoft.CodeAnalysis.CSharp.Scripting;
37 using Microsoft.CodeAnalysis.Scripting;
38 using Newtonsoft.Json.Linq;
39 using System;
40 using System.Collections.Generic;
41 using System.IO;
42 using System.Linq;
43 using System.Reflection;
44 using System.Text;
45 using System.Threading.Tasks;
46 
47 namespace Microsoft.Bot.Builder.FormFlow.Json
48 {
49  // No need to document overrides of interface methods
50 #pragma warning disable CS1591
51 
52  #region Documentation
53  #endregion
109  public sealed class FormBuilderJson : FormBuilderBase<JObject>
110  {
115  public FormBuilderJson(JObject schema)
116  {
117  _schema = schema;
118  ProcessOptions();
119  ProcessOnCompletion();
120  }
121 
122  public override IForm<JObject> Build(Assembly resourceAssembly = null, string resourceName = null)
123  {
124  if (!_form.Fields.Any())
125  {
126  // No fieldss means add default field and confirmation
127  AddRemainingFields();
128  Confirm(new PromptAttribute(Configuration.Template(TemplateUsage.Confirmation)));
129  }
130  // Build all code into a single assembly and cache because assemblies have no GC.
131  var builder = new StringBuilder("switch (choice) {");
132  int choice = 1;
133  var entries = new List<CallScript>();
134  lock (_scripts)
135  {
136  foreach (var entry in _scripts)
137  {
138  if (entry.Value.Script == null)
139  {
140  entry.Value.Choice = choice++;
141  builder.AppendLine();
142  builder.Append($"case {entry.Value.Choice}: {{{entry.Key}}}; break;");
143  entries.Add(entry.Value);
144  }
145  }
146  if (entries.Any())
147  {
148  // Define does not need to return a result.
149  builder.AppendLine();
150  builder.AppendLine("}");
151  builder.Append("return null;");
152  var fun = Compile<ScriptGlobals, object>(builder.ToString());
153  foreach (var entry in entries)
154  {
155  entry.Script = fun;
156  }
157  }
158  }
159  return base.Build(resourceAssembly, resourceName);
160  }
161 
162  public override IFormBuilder<JObject> Field(string name, ActiveDelegate<JObject> active = null, ValidateAsyncDelegate<JObject> validate = null)
163  {
164  var field = new FieldJson(this, name);
165  field.SetActive(active);
166  field.SetValidate(validate);
167  AddSteps(field.Before);
168  Field(field);
169  AddSteps(field.After);
170  return this;
171  }
172 
173  public override IFormBuilder<JObject> Field(string name, string prompt, ActiveDelegate<JObject> active = null, ValidateAsyncDelegate<JObject> validate = null)
174  {
175  return Field(name, new PromptAttribute(prompt), active, validate);
176  }
177 
178  public override IFormBuilder<JObject> Field(string name, PromptAttribute prompt, ActiveDelegate<JObject> active = null, ValidateAsyncDelegate<JObject> validate = null)
179  {
180  var field = new FieldJson(this, name);
181  field.SetPrompt(prompt);
182  if (active != null)
183  {
184  field.SetActive(active);
185  }
186  if (validate != null)
187  {
188  field.SetValidate(validate);
189  }
190  return Field(field);
191  }
192 
193  public override IFormBuilder<JObject> AddRemainingFields(IEnumerable<string> exclude = null)
194  {
195  var exclusions = (exclude == null ? Array.Empty<string>() : exclude.ToArray());
196  var fields = new List<string>();
197  Fields(_schema, null, fields);
198  foreach (var field in fields)
199  {
200  if (!exclusions.Contains(field) && !HasField(field))
201  {
202  Field(field);
203  }
204  }
205  return this;
206  }
207 
208  #region Class specific methods
209  public JObject Schema
210  {
211  get { return _schema; }
212  }
213 
214  internal MessageDelegate<JObject> MessageScript(string script)
215  {
216  return script != null ? new MessageDelegate<JObject>(AddScript(null, script).MessageScript) : null;
217  }
218 
219  internal ActiveDelegate<JObject> ActiveScript(IField<JObject> field, string script)
220  {
221  return script != null ? new ActiveDelegate<JObject>(AddScript(field, script).ActiveScript) : null;
222  }
223 
224  internal DefineAsyncDelegate<JObject> DefineScript(IField<JObject> field, string script)
225  {
226  return script != null ? new DefineAsyncDelegate<JObject>(AddScript(field, script).DefineScriptAsync) : null;
227  }
228 
229  internal ValidateAsyncDelegate<JObject> ValidateScript(IField<JObject> field, string script)
230  {
231  return script != null ? new ValidateAsyncDelegate<JObject>(AddScript(field, script).ValidateScriptAsync) : null;
232  }
233 
234  internal NextDelegate<JObject> NextScript(IField<JObject> field, string script)
235  {
236  return script != null ? new NextDelegate<JObject>(AddScript(field, script).NextScript) : null;
237  }
238 
239  internal OnCompletionAsyncDelegate<JObject> OnCompletionScript(string script)
240  {
241  return script != null ? new OnCompletionAsyncDelegate<JObject>(AddScript(null, script).OnCompletionAsync) : null;
242  }
243  #endregion
244 
245  #region Implementation
246  private void ProcessOptions()
247  {
248  JToken references;
249  var assemblies = new List<string>() { "Microsoft.Bot.Builder.dll" };
250  if (_schema.TryGetValue("References", out references))
251  {
252  foreach (JToken reference in references.Children())
253  {
254  assemblies.Add((string)reference);
255  }
256  }
257  JToken importsChildren;
258  var imports = new List<string>();
259  if (_schema.TryGetValue("Imports", out importsChildren))
260  {
261  foreach (JToken import in importsChildren.Children())
262  {
263  imports.Add((string)import);
264  }
265  }
266  var dir = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath);
267  _options = CodeAnalysis.Scripting.ScriptOptions.Default
268  .AddReferences((from assembly in assemblies select System.IO.Path.Combine(dir, assembly)).ToArray())
269  .AddImports("Microsoft.Bot.Builder", "Microsoft.Bot.Builder.Dialogs",
270  "Microsoft.Bot.Builder.FormFlow", "Microsoft.Bot.Builder.FormFlow.Advanced",
271  "System.Collections.Generic", "System.Linq")
272  .AddImports(imports.ToArray());
273  }
274 
275  private void ProcessOnCompletion()
276  {
277  if (_schema["OnCompletion"] != null)
278  {
279  OnCompletion(OnCompletionScript((string)_schema["OnCompletion"]));
280  }
281  }
282 
283  private CallScript AddScript(IField<JObject> field, string script)
284  {
285  CallScript call;
286  lock (_scripts)
287  {
288  if (!_scripts.TryGetValue(script, out call))
289  {
290  call = new CallScript { Field = field };
291  _scripts[script] = call;
292  }
293  }
294  return call;
295  }
296 
297  // NOTE: Compiling code creates an assembly which cannot be GC whereas EvaluateAsync does not.
298  private ScriptRunner<R> Compile<G, R>(string code)
299  {
300  try
301  {
302  var script = CSharpScript.Create<R>(code, _options, typeof(G));
303  return script.CreateDelegate();
304  }
305  catch (Microsoft.CodeAnalysis.Scripting.CompilationErrorException ex)
306  {
307  throw CompileException(ex, code);
308  }
309  }
310 
311  private async Task<T> EvaluateAsync<T>(string code, object globals)
312  {
313  try
314  {
315  return await CSharpScript.EvaluateAsync<T>(code, _options, globals);
316  }
317  catch (Microsoft.CodeAnalysis.Scripting.CompilationErrorException ex)
318  {
319  throw CompileException(ex, code);
320  }
321  }
322 
323  private Exception CompileException(CompilationErrorException ex, string code)
324  {
325  Exception result = ex;
326  var match = System.Text.RegularExpressions.Regex.Match(ex.Message, @"\(\s*(?<line>\d+)\s*,\s*(?<column>\d+)\s*\)\s*:\s*(?<message>.*)");
327  if (match.Success)
328  {
329  var lineNumber = int.Parse(match.Groups["line"].Value) - 1;
330  var column = int.Parse(match.Groups["column"].Value) - 1;
331  var line = code.Split('\n')[lineNumber];
332  var minCol = Math.Max(0, column - 20);
333  var maxCol = Math.Min(line.Length, column + 20);
334  var msg = line.Substring(minCol, column - minCol) + "^" + line.Substring(column, maxCol - column);
335  result = new ArgumentException(match.Groups["message"].Value + ": " + msg);
336  }
337  return result;
338  }
339 
340  private void AddSteps(IEnumerable<FieldJson.MessageOrConfirmation> steps)
341  {
342  foreach (var step in steps)
343  {
344  var active = ActiveScript(null, step.ActiveScript);
345  if (step.IsMessage)
346  {
347  if (step.MessageScript != null)
348  {
349  Message(MessageScript(step.MessageScript), active, step.Dependencies);
350  }
351  else
352  {
353  Message(step.Prompt, active, step.Dependencies);
354  }
355  }
356  else
357  {
358  if (step.MessageScript != null)
359  {
360  Confirm(MessageScript(step.MessageScript), active, step.Dependencies);
361  }
362  else
363  {
364  Confirm(step.Prompt, active, step.Dependencies);
365  }
366  }
367  }
368  }
369 
370  private void Fields(JObject schema, string prefix, IList<string> fields)
371  {
372  if (schema["properties"] != null)
373  {
374  foreach (JProperty property in schema["properties"])
375  {
376  var path = (prefix == null ? property.Name : $"{prefix}.{property.Name}");
377  var childSchema = (JObject)property.Value;
378  var eltSchema = FieldJson.ElementSchema(childSchema);
379  if (FieldJson.IsPrimitiveType(eltSchema))
380  {
381  fields.Add(path);
382  }
383  else
384  {
385  Fields(childSchema, path, fields);
386  }
387  }
388  }
389  }
390 
391  private void FieldPaths(Type type, string path, List<string> paths)
392  {
393  var newPath = (path == "" ? path : path + ".");
394  foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
395  {
396  TypePaths(field.FieldType, newPath + field.Name, paths);
397  }
398 
399  foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
400  {
401  if (property.CanRead && property.CanWrite)
402  {
403  TypePaths(property.PropertyType, newPath + property.Name, paths);
404  }
405  }
406  }
407 
408  private void TypePaths(Type type, string path, List<string> paths)
409  {
410  if (type.IsClass)
411  {
412  if (type == typeof(string))
413  {
414  paths.Add(path);
415  }
416  else if (type.IsIEnumerable())
417  {
418  var elt = type.GetGenericElementType();
419  if (elt.IsEnum)
420  {
421  paths.Add(path);
422  }
423  else
424  {
425  // TODO: What to do about enumerations of things other than enums?
426  }
427  }
428  else
429  {
430  FieldPaths(type, path, paths);
431  }
432  }
433  else if (type.IsEnum)
434  {
435  paths.Add(path);
436  }
437  else if (type == typeof(bool))
438  {
439  paths.Add(path);
440  }
441  else if (type.IsIntegral())
442  {
443  paths.Add(path);
444  }
445  else if (type.IsDouble())
446  {
447  paths.Add(path);
448  }
449  else if (type.IsNullable() && type.IsValueType)
450  {
451  paths.Add(path);
452  }
453  else if (type == typeof(DateTime))
454  {
455  paths.Add(path);
456  }
457  }
458 
459  private delegate Task<object> CallAsyncDelegate(ScriptGlobals globals);
460  private class CallScript
461  {
462  public int Choice;
463  public ScriptRunner<object> Script;
464  public IField<JObject> Field;
465 
466  public async Task<PromptAttribute> MessageScript(JObject state)
467  {
468  return (PromptAttribute)await Script(new ScriptGlobals { choice = Choice, state = state, ifield = Field });
469  }
470 
471  public bool ActiveScript(JObject state)
472  {
473  return (bool)Script(new ScriptGlobals { choice = Choice, state = state, ifield = Field }).Result;
474  }
475 
476  public async Task<ValidateResult> ValidateScriptAsync(JObject state, object value)
477  {
478  return (ValidateResult)await Script(new ScriptGlobals { choice = Choice, state = state, value = value, ifield = Field });
479  }
480 
481  public async Task<bool> DefineScriptAsync(JObject state, Field<JObject> field)
482  {
483  return (bool)await Script(new ScriptGlobals { choice = Choice, state = state, field = field, ifield = Field });
484  }
485 
486  public NextStep NextScript(object value, JObject state)
487  {
488  return (NextStep)Script(new ScriptGlobals { choice = Choice, value = value, state = state, ifield = Field }).Result;
489  }
490 
491  public async Task OnCompletionAsync(IDialogContext context, JObject state)
492  {
493  await Script(new ScriptGlobals { choice = Choice, context = context, state = state, ifield = Field });
494  }
495  }
496 
497  private readonly JObject _schema;
498  private ScriptOptions _options;
499  private static Dictionary<string, CallScript> _scripts = new Dictionary<string, CallScript>();
500  #endregion
501  }
502 }
503 
504 namespace Microsoft.Bot.Builder.FormFlow.Advanced
505 {
509  public class ScriptGlobals
510  {
514  public int choice;
515 
519  public JObject state;
520 
524  public object value;
525 
530 
535 
540  }
541 }
Core namespace for FormFlow and associated infrastructure.
Definition: Attributes.cs:39
The context for the execution of a dialog&#39;s conversational process.
FormBuilderJson(JObject schema)
Create a JSON form builder.
Dictionary of all fields indexed by name.
Definition: Field.cs:785
Global values to pass into scripts defined using Microsoft.Bot.Builder.FormFlow.Json.FormBuilderJson.
Define the prompt used when asking about a field.
Definition: Attributes.cs:294
TemplateUsage
All of the built-in templates.
Definition: Attributes.cs:321
Abstract base class for Form Builders.
Definition: FormBuilder.cs:55
IField< JObject > ifield
Current field if any.
Interface for building a form.
Definition: IFormBuilder.cs:77
Namespace for FormFlow advanced building blocks.
Definition: Attributes.cs:672
Form definition interface.
Definition: IForm.cs:47
Field defined through JSON Schema.
Definition: FieldJson.cs:48
IDialogContext context
Dialog context for OnCompletionAsync handlers.
override IFormBuilder< JObject > Field(string name, PromptAttribute prompt, ActiveDelegate< JObject > active=null, ValidateAsyncDelegate< JObject > validate=null)
override IFormBuilder< JObject > Field(string name, string prompt, ActiveDelegate< JObject > active=null, ValidateAsyncDelegate< JObject > validate=null)
Field< JObject > field
Field to be dynamically defined.
Encapsulates the result of a ValidateAsyncDelegate<T>
Definition: IFormDialog.cs:79
Base class with declarative implementation of IField.
Definition: Field.cs:67
Build a form by specifying messages, fields and confirmations through JSON Schema or programatically...
override IFormBuilder< JObject > Field(string name, ActiveDelegate< JObject > active=null, ValidateAsyncDelegate< JObject > validate=null)
Field is used to confirm some settings during the dialog.
override IForm< JObject > Build(Assembly resourceAssembly=null, string resourceName=null)
Build the form based on the methods called on the builder.
Core namespace for Dialogs and associated infrastructure.
Choice for clarifying an ambiguous value in ValidateResult.
Definition: IFormDialog.cs:54
override IFormBuilder< JObject > AddRemainingFields(IEnumerable< string > exclude=null)
Add all fields not already added to the form.
Root namespace for the Microsoft Bot Builder SDK.