FieldJson.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 
35 using Newtonsoft.Json.Linq;
36 using System;
37 using System.Collections.Generic;
38 using System.Linq;
39 
40 namespace Microsoft.Bot.Builder.FormFlow.Json
41 {
42  // No need to document overrides of interface methods
43 #pragma warning disable CS1591
44 
48  public class FieldJson : Field<JObject>
49  {
55  public FieldJson(FormBuilderJson builder, string name)
56  : base(name, FieldRole.Value)
57  {
58  _builder = builder;
59  bool optional;
60  var fieldSchema = FieldSchema(name, out optional);
61  var eltSchema = ElementSchema(fieldSchema);
62  ProcessAnnotations(fieldSchema, eltSchema);
63  var fieldName = name.Split('.').Last();
64  JToken date;
65  if (eltSchema.TryGetValue("DateTime", out date) && date.Value<bool>())
66  {
67  SetType(typeof(DateTime));
68  }
69  else
70  {
71  SetType(eltSchema["enum"] != null && eltSchema["enum"].Any() ? null : ToType(eltSchema));
72  }
73  SetAllowsMultiple(IsType(fieldSchema, "array"));
74  SetFieldDescription(ProcessDescription(fieldSchema, Language.CamelCase(fieldName)));
75  var terms = Strings(fieldSchema, "Terms");
76  JToken maxPhrase;
77  if (terms != null && fieldSchema.TryGetValue("MaxPhrase", out maxPhrase))
78  {
79  terms = (from seed in terms
80  from gen in Language.GenerateTerms(seed, (int)maxPhrase)
81  select gen).ToArray<string>();
82  }
83  SetFieldTerms(terms ?? Language.GenerateTerms(Language.CamelCase(fieldName), 3));
84  ProcessEnum(eltSchema);
85  SetOptional(optional);
86  SetIsNullable(IsType(fieldSchema, "null"));
87  }
88 
89  #region IFieldState
90  public override object GetValue(JObject state)
91  {
92  object result = null;
93  var val = state.SelectToken(_name);
94  if (val != null)
95  {
96  if (_type == null)
97  {
98  if (_allowsMultiple)
99  {
100  result = val.ToObject<string[]>();
101  }
102  else
103  {
104  result = (string)val;
105  }
106  }
107  else
108  {
109  result = val.ToObject(_type);
110  }
111  }
112  return result;
113  }
114 
115  public override void SetValue(JObject state, object value)
116  {
117  var jvalue = JToken.FromObject(value);
118  var current = state.SelectToken(_name);
119  if (current == null)
120  {
121  var step = state;
122  var steps = _name.Split('.');
123  foreach (var part in steps.Take(steps.Count() - 1))
124  {
125  var next = step.GetValue(part);
126  if (next == null)
127  {
128  var nextStep = new JObject();
129  step.Add(part, nextStep);
130  step = nextStep;
131  }
132  else
133  {
134  step = (JObject)next;
135  }
136  }
137  step.Add(steps.Last(), jvalue);
138  }
139  else
140  {
141  current.Replace(jvalue);
142  }
143  }
144 
145  public override bool IsUnknown(JObject state)
146  {
147  return state.SelectToken(_name) == null;
148  }
149 
150  public override void SetUnknown(JObject state)
151  {
152  var token = state.SelectToken(_name);
153  if (token != null)
154  {
155  token.Parent.Remove();
156  }
157  }
158 
159  #endregion
160 
161  internal IEnumerable<MessageOrConfirmation> Before { get; set; }
162 
163  internal IEnumerable<MessageOrConfirmation> After { get; set; }
164 
165  protected JObject FieldSchema(string path, out bool optional)
166  {
167  var schema = _builder.Schema;
168  var parts = path.Split('.');
169  var required = true;
170  foreach (var part in parts)
171  {
172  required = required && (schema["required"] == null || ((JArray)schema["required"]).Any((val) => (string)val == part));
173  schema = (JObject)((JObject)schema["properties"])[part];
174  if (part == null)
175  {
176  throw new MissingFieldException($"{part} is not a property in your schema.");
177  }
178  }
179  optional = !required;
180  return schema;
181  }
182 
183  protected Type ToType(JObject schema)
184  {
185  Type type = null;
186  if (IsType(schema, "boolean")) type = typeof(bool);
187  else if (IsType(schema, "integer")) type = typeof(long);
188  else if (IsType(schema, "number")) type = typeof(double);
189  else if (IsType(schema, "string")) type = typeof(string);
190  else
191  {
192  throw new ArgumentException($"{schema} does not have a valid C# type.");
193  }
194  return type;
195  }
196 
197  protected string[] Strings(JObject schema, string field)
198  {
199  string[] result = null;
200  JToken array;
201  if (schema.TryGetValue(field, out array))
202  {
203  result = array.ToObject<string[]>();
204  }
205  return result;
206  }
207 
208  protected string AString(JObject schema, string field)
209  {
210  string result = null;
211  JToken astring;
212  if (schema.TryGetValue(field, out astring))
213  {
214  result = (string)astring;
215  }
216  return result;
217  }
218 
219  protected void ProcessAnnotations(JObject fieldSchema, JObject eltSchema)
220  {
221  ProcessTemplates(_builder.Schema);
222  Before = ProcessMessages("Before", fieldSchema);
223  ProcessTemplates(fieldSchema);
224  ProcessPrompt(fieldSchema);
225  ProcessNumeric(fieldSchema);
226  ProcessPattern(fieldSchema);
227  ProcessActive(fieldSchema);
228  ProcessDefine(fieldSchema);
229  ProcessValidation(fieldSchema);
230  ProcessNext(fieldSchema);
231  After = ProcessMessages("After", fieldSchema);
232  }
233 
234  protected void ProcessDefine(JObject schema)
235  {
236  if (schema["Define"] != null)
237  {
238  SetDefine(_builder.DefineScript(this, (string)schema["Define"]));
239  }
240  }
241 
242  protected void ProcessValidation(JObject schema)
243  {
244  if (schema["Validate"] != null)
245  {
246  SetValidate(_builder.ValidateScript(this, (string)schema["Validate"]));
247  }
248  }
249 
250  protected void ProcessNext(JObject schema)
251  {
252  if (schema["Next"] != null)
253  {
254  SetNext(_builder.NextScript(this, (string)schema["Next"]));
255  }
256  }
257 
258  protected void ProcessActive(JObject schema)
259  {
260  if (schema["Active"] != null)
261  {
262  var script = (string)schema["Active"];
263  SetActive(_builder.ActiveScript(this, (string)schema["Active"]));
264  }
265  }
266 
267  internal class MessageOrConfirmation
268  {
269  public bool IsMessage;
270  public PromptAttribute Prompt;
271  public string ActiveScript;
272  public string MessageScript;
273  public IEnumerable<string> Dependencies;
274  }
275 
276  internal IEnumerable<MessageOrConfirmation> ProcessMessages(string fieldName, JObject fieldSchema)
277  {
278  JToken array;
279  if (fieldSchema.TryGetValue(fieldName, out array))
280  {
281  foreach (var message in array.Children<JObject>())
282  {
283  var info = new MessageOrConfirmation();
284  if (GetPrompt("Message", message, info))
285  {
286  info.IsMessage = true;
287  yield return info;
288  }
289  else if (GetPrompt("Confirm", message, info))
290  {
291  info.IsMessage = false;
292  yield return info;
293  }
294  else
295  {
296  throw new ArgumentException($"{message} is not Message or Confirm");
297  }
298  }
299  }
300  }
301 
302  internal bool GetPrompt(string fieldName, JObject message, MessageOrConfirmation info)
303  {
304  bool found = false;
305  JToken val;
306  if (message.TryGetValue(fieldName, out val))
307  {
308  if (val is JValue)
309  {
310  info.MessageScript = (string)val;
311  }
312  else if (val is JArray)
313  {
314  info.Prompt = (PromptAttribute)ProcessTemplate(message, new PromptAttribute((from msg in val select (string)msg).ToArray()));
315  }
316  else
317  {
318  throw new ArgumentException($"{val} must be string or array of strings.");
319  }
320  if (message["Active"] != null)
321  {
322  info.ActiveScript = (string)message["Active"];
323  }
324  if (message["Dependencies"] != null)
325  {
326  info.Dependencies = (from dependent in message["Dependencies"] select (string)dependent);
327  }
328  found = true;
329  }
330  return found;
331  }
332 
333  protected void ProcessTemplates(JObject schema)
334  {
335  JToken templates;
336  if (schema.TryGetValue("Templates", out templates))
337  {
338  foreach (JProperty template in templates.Children())
339  {
340  TemplateUsage usage;
341  if (Enum.TryParse<TemplateUsage>(template.Name, out usage))
342  {
343  ReplaceTemplate((TemplateAttribute)ProcessTemplate(template.Value, new TemplateAttribute(usage)));
344  }
345  }
346  }
347  }
348 
349  protected void ProcessPrompt(JObject schema)
350  {
351  JToken prompt;
352  if (schema.TryGetValue("Prompt", out prompt))
353  {
354  SetPrompt((PromptAttribute)ProcessTemplate(prompt, new PromptAttribute()));
355  }
356  }
357 
358  protected void ProcessNumeric(JObject schema)
359  {
360  JToken token;
361  double min = -double.MaxValue, max = double.MaxValue;
362  if (schema.TryGetValue("minimum", out token)) min = (double)token;
363  if (schema.TryGetValue("maximum", out token)) max = (double)token;
364  if (min != -double.MaxValue || max != double.MaxValue)
365  {
366  SetLimits(min, max);
367  }
368  }
369 
370  protected void ProcessPattern(JObject schema)
371  {
372  JToken token;
373  if (schema.TryGetValue("pattern", out token))
374  {
375  SetPattern((string)token);
376  }
377  }
378 
379  protected void ProcessEnum(JObject schema)
380  {
381  if (schema["enum"] != null)
382  {
383  var enums = (from val in (JArray)schema["enum"] select (string)val);
384  var toDescription = new Dictionary<string, DescribeAttribute>();
385  var toTerms = new Dictionary<string, string[]>();
386  var toMaxPhrase = new Dictionary<string, int>();
387  JToken values;
388  if (schema.TryGetValue("Values", out values))
389  {
390  foreach (JProperty prop in values.Children())
391  {
392  var key = prop.Name;
393  if (!enums.Contains(key))
394  {
395  throw new ArgumentException($"{key} is not in enumeration.");
396  }
397  var desc = (JObject)prop.Value;
398  JToken description;
399  if (desc.TryGetValue("Describe", out description))
400  {
401  toDescription.Add(key, ProcessDescription(desc, Language.CamelCase(key)));
402  }
403  JToken terms;
404  if (desc.TryGetValue("Terms", out terms))
405  {
406  toTerms.Add(key, terms.ToObject<string[]>());
407  }
408  JToken maxPhrase;
409  if (desc.TryGetValue("MaxPhrase", out maxPhrase))
410  {
411  toMaxPhrase.Add(key, (int)maxPhrase);
412  }
413  }
414  }
415  foreach (var key in enums)
416  {
417  DescribeAttribute description;
418  if (!toDescription.TryGetValue(key, out description))
419  {
420  description = new DescribeAttribute(Language.CamelCase(key));
421  }
422  AddDescription(key, description);
423 
424  string[] terms;
425  int maxPhrase;
426  if (!toTerms.TryGetValue(key, out terms))
427  {
428  terms = Language.GenerateTerms(description.Description, 3);
429  }
430  else if (toMaxPhrase.TryGetValue(key, out maxPhrase))
431  {
432  terms = (from seed in terms
433  from gen in Language.GenerateTerms(seed, maxPhrase)
434  select gen).ToArray<string>();
435  }
436  AddTerms(key, terms);
437  }
438  }
439  }
440 
441  protected TemplateBaseAttribute ProcessTemplate(JToken template, TemplateBaseAttribute attribute)
442  {
443  if (template["Patterns"] != null)
444  {
445  attribute.Patterns = template["Patterns"].ToObject<string[]>();
446  }
447  attribute.AllowDefault = ProcessEnum<BoolDefault>(template, "AllowDefault");
448  attribute.ChoiceCase = ProcessEnum<CaseNormalization>(template, "ChoiceCase");
449  attribute.ChoiceFormat = (string)template["ChoiceFormat"];
450  attribute.ChoiceLastSeparator = (string)template["ChoiceLastSeparator"];
451  attribute.ChoiceParens = ProcessEnum<BoolDefault>(template, "ChoiceParens");
452  attribute.ChoiceSeparator = (string)template["ChoiceSeparator"];
453  attribute.ChoiceStyle = ProcessEnum<ChoiceStyleOptions>(template, "ChoiceStyle");
454  attribute.Feedback = ProcessEnum<FeedbackOptions>(template, "Feedback");
455  attribute.FieldCase = ProcessEnum<CaseNormalization>(template, "FieldCase");
456  attribute.LastSeparator = (string)template["LastSeparator"];
457  attribute.Separator = (string)template["Separator"];
458  attribute.ValueCase = ProcessEnum<CaseNormalization>(template, "ValueCase");
459  return attribute;
460  }
461 
462  protected DescribeAttribute ProcessDescription(JObject schema, string defaultDesc)
463  {
464  // Simple string or object
465  // {Description=, Image=, Title=, SubTitle=}
466  var desc = new DescribeAttribute();
467  JToken jdesc;
468  if (schema.TryGetValue("Describe", out jdesc))
469  {
470  if (jdesc.Type == JTokenType.String)
471  {
472  desc.Description = jdesc.Value<string>();
473  }
474  else
475  {
476  var jdescription = jdesc["Description"];
477  if (jdescription != null)
478  {
479  desc.Description = jdescription.Value<string>();
480  }
481  else
482  {
483  desc.Description = defaultDesc;
484  }
485 
486  var jimage = jdesc["Image"];
487  if (jimage != null)
488  {
489  desc.Image = jimage.Value<string>();
490  }
491 
492  var jtitle = jdesc["Title"];
493  if (jtitle != null)
494  {
495  desc.Title = jtitle.Value<string>();
496  }
497 
498  var jsubTitle = jdesc["SubTitle"];
499  if (jsubTitle != null)
500  {
501  desc.SubTitle = jsubTitle.Value<string>();
502  }
503  }
504  }
505  else
506  {
507  desc.Description = defaultDesc;
508  }
509  return desc;
510  }
511 
512  protected T ProcessEnum<T>(JToken template, string name)
513  {
514  T result = default(T);
515  var value = template[name];
516  if (value != null)
517  {
518  result = (T)Enum.Parse(typeof(T), (string)value);
519  }
520  return result;
521  }
522 
523 
524  internal static bool IsType(JObject schema, string type)
525  {
526  bool isType = false;
527  var jtype = schema["type"];
528  if (jtype != null)
529  {
530  if (jtype is JArray)
531  {
532  isType = jtype.Values().Contains(type);
533  }
534  else
535  {
536  isType = (string)jtype == type;
537  }
538  }
539  return isType;
540  }
541 
542  internal static bool IsPrimitiveType(JObject schema)
543  {
544  var isPrimitive = schema["enum"] != null && schema["enum"].Any();
545  if (!isPrimitive)
546  {
547  isPrimitive =
548  IsType(schema, "boolean")
549  || IsType(schema, "integer")
550  || IsType(schema, "number")
551  || IsType(schema, "string")
552  || (schema["DateTime"] != null && (bool)schema["DateTime"]);
553  }
554  return isPrimitive;
555  }
556 
557  internal static JObject ElementSchema(JObject schema)
558  {
559  JObject result = schema;
560  if (IsType(schema, "array"))
561  {
562  var items = schema["items"];
563  if (items is JArray)
564  {
565  result = (JObject)((JArray)items).First();
566  }
567  else
568  {
569  result = (JObject)items;
570  }
571  }
572  return result;
573  }
574 
576  }
577 }
Core namespace for FormFlow and associated infrastructure.
Definition: Attributes.cs:39
override void SetUnknown(JObject state)
Definition: FieldJson.cs:150
override void SetValue(JObject state, object value)
Definition: FieldJson.cs:115
string LastSeparator
When constructing lists using {[]} in a Pattern Language string, the string used before the last valu...
Definition: Attributes.cs:730
FieldRole
The role the field plays in a form.
Definition: IField.cs:129
FieldJson(FormBuilderJson builder, string name)
Construct a field from a JSON schema.
Definition: FieldJson.cs:55
CaseNormalization ValueCase
Control case when showing {} value references in a Pattern Language string.
Definition: Attributes.cs:740
override bool IsUnknown(JObject state)
Definition: FieldJson.cs:145
FeedbackOptions Feedback
Control what kind of feedback the user gets after each input.
Definition: Attributes.cs:720
Define a template for generating strings.
Definition: Attributes.cs:571
BoolDefault ChoiceParens
When constructing inline choice lists for {||} in a Pattern Language string controls whether to inclu...
Definition: Attributes.cs:705
DescribeAttribute ProcessDescription(JObject schema, string defaultDesc)
Definition: FieldJson.cs:462
Define the prompt used when asking about a field.
Definition: Attributes.cs:294
static string[] GenerateTerms(string phrase, int maxLength)
Generate regular expressions to match word sequences in original string.
Definition: Language.cs:215
TemplateUsage
All of the built-in templates.
Definition: Attributes.cs:321
string ChoiceFormat
Format string used for presenting each choice when showing {||} choices in a Pattern Language string...
Definition: Attributes.cs:695
JObject FieldSchema(string path, out bool optional)
Definition: FieldJson.cs:165
string[] Strings(JObject schema, string field)
Definition: FieldJson.cs:197
string ChoiceLastSeparator
When constructing inline lists of choices using {||} in a Pattern Language string, the string used before the last choice.
Definition: Attributes.cs:700
string ChoiceSeparator
When constructing inline lists using {||} in a Pattern Language string, the string used between all c...
Definition: Attributes.cs:710
ChoiceStyleOptions ChoiceStyle
How to display choices {||} when processed in a Pattern Language string.
Definition: Attributes.cs:715
static string CamelCase(string original)
Break a string into words based on _ and case changes.
Definition: Language.cs:146
Namespace for FormFlow advanced building blocks.
Definition: Attributes.cs:672
Field defined through JSON Schema.
Definition: FieldJson.cs:48
string Separator
When constructing lists using {[]} in a Pattern Language string, the string used between all values e...
Definition: Attributes.cs:735
Abstract base class used by all attributes that use Pattern Language.
Definition: Attributes.cs:677
TemplateBaseAttribute ProcessTemplate(JToken template, TemplateBaseAttribute attribute)
Definition: FieldJson.cs:441
Field is used to get a value to set in the form state.
override object GetValue(JObject state)
Definition: FieldJson.cs:90
Base class with declarative implementation of IField.
Definition: Field.cs:67
string AString(JObject schema, string field)
Definition: FieldJson.cs:208
void ProcessAnnotations(JObject fieldSchema, JObject eltSchema)
Definition: FieldJson.cs:219
Build a form by specifying messages, fields and confirmations through JSON Schema or programatically...
Core namespace for Dialogs and associated infrastructure.
BoolDefault AllowDefault
When processing choices {||} in a Pattern Language string, provide a choice for the default value if ...
Definition: Attributes.cs:684
CaseNormalization FieldCase
Control case when showing {&} field name references in a Pattern Language string. ...
Definition: Attributes.cs:725
CaseNormalization ChoiceCase
Control case when showing choices in {||} references in a Pattern Language string.
Definition: Attributes.cs:689
Root namespace for the Microsoft Bot Builder SDK.
Attribute to override the default description of a field, property or enum value. ...
Definition: Attributes.cs:62