forked from ashtonian/RQL.NET
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ClassSpecBuilder.cs
156 lines (134 loc) · 5.91 KB
/
ClassSpecBuilder.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace RQL.NET
{
/// <summary>
/// FieldSpec contains all the field (class property) meta data required to parse information for that field from json.
/// </summary>
public class FieldSpec
{
/// <summary>
/// json field name.
/// </summary>
/// <value></value>
public string Name { get; set; }
public string ColumnName { get; set; }
public HashSet<string> Ops { get; set; }
public Type PropType { get; set; }
public bool IsSortable { get; set; }
public Func<string, Type, object, IError> Validator { get; set; }
public Func<string, Type, object, (object, IError)> Converter { get; set; }
}
/// <summary>
/// Contains all the required metadata to parse an object.
/// </summary>
public class ClassSpec
{
public Dictionary<string, FieldSpec> Fields { get; set; }
public Func<string, Type, object, IError> Validator { get; set; }
public Func<string, Type, object, (object, IError)> Converter { get; set; }
}
public interface ClassSpecCache
{
ClassSpec Get(Type t);
void Set(Type t, ClassSpec spec);
}
public class InMemoryClassSpecCache : ClassSpecCache
{
private static ConcurrentDictionary<Type, ClassSpec> TypeCache = new ConcurrentDictionary<Type, ClassSpec>();
public ClassSpec Get(Type t)
{
return TypeCache.ContainsKey(t) ? TypeCache[t] : null;
}
public void Set(Type t, ClassSpec spec)
{
var result = TypeCache.AddOrUpdate(t, spec, (key, spec2) => { return spec2; });
}
}
/// <summary>
/// Uses reflection (once, then cached) to generate required parse metadata.
/// </summary>
public class ClassSpecBuilder
{
private readonly Func<string, string> _columnNamer;
private readonly Func<string, Type, object, (object, IError)> _converter;
private readonly Func<string, string> _fieldNamer;
private readonly IOpMapper _opMapper;
private readonly Func<string, Type, object, IError> _validator;
public ClassSpecBuilder(
Func<string, string> columnNamer = null,
Func<string, string> fieldNamer = null,
IOpMapper opMapper = null,
Func<string, Type, object, IError> validator = null,
Func<string, Type, object, (object, IError)> converter = null
)
{
_columnNamer = columnNamer;
_fieldNamer = fieldNamer;
_opMapper = opMapper;
_validator = validator;
_converter = converter;
}
/// <summary>
/// Generates and caches all required metadata to parse an object.
/// </summary>
/// <param name="t"></param>
/// <returns></returns>
public ClassSpec Build(Type t)
{
var spec = Defaults.SpecCache.Get(t);
if (spec != null) return spec;
var fields = new Dictionary<string, FieldSpec>();
var properties = t.GetProperties(BindingFlags.Instance | BindingFlags.Public);
var classAttributes = t.GetCustomAttributes(true).ToList();
// TODO: implement class props
// var classFilter = classAttributes?.OfType<Filterable>()?.FirstOrDefault();
// var classSortable = classAttributes?.OfType<Filterable>()?.FirstOrDefault();
// TODO: is class use name resolver and concat fields to class
// v1 flat table nested class
// v2 assume table is joined, or do a sub query, or both?
// loop through class properties and generate field spec
foreach (var p in properties)
{
var attributes = p.GetCustomAttributes(true);
var ignore = attributes?.OfType<Ignore>()?.FirstOrDefault();
var ignoreSort = attributes?.OfType<Ignore.Sort>()?.FirstOrDefault();
var ignoreFilter = attributes?.OfType<Ignore.Filter>()?.FirstOrDefault();
if (ignore != null || ignoreFilter != null) continue;
var opMapper = _opMapper ?? Defaults.DefaultOpMapper;
var ops = opMapper.GetSupportedOps(p.PropertyType);
var opsBlacklist = attributes?.OfType<Ops.Disallowed>()?.FirstOrDefault();
if (opsBlacklist != null)
ops.ToList().RemoveAll(opsBlacklist.Ops.Contains); // TODO: don't think this modifies the ops ref
var columnName = attributes?.OfType<ColumnName>()?.FirstOrDefault();
var fieldName = attributes?.OfType<FieldName>()?.FirstOrDefault();
var field = new FieldSpec
{
IsSortable = ignoreSort == null,
Ops = ops,
PropType = p.PropertyType,
ColumnName = columnName?.Name ??
(_columnNamer != null ? _columnNamer(p.Name) : Defaults.DefaultColumnNamer(p.Name)),
Name = fieldName?.Name ??
(fieldName != null ? _fieldNamer(p.Name) : Defaults.DefaultFieldNamer(p.Name)),
Converter = p.PropertyType == typeof(DateTime)
? Defaults.DefaultConverter
: null, // ?? attribute that looks for IConverter on field, then class
Validator = null // ?? attribute that looks for IValidator on field, then class
};
fields.Add(field.Name, field);
}
spec = new ClassSpec
{
Fields = fields,
Converter = _converter,
Validator = _validator ?? Defaults.DefaultValidator
};
Defaults.SpecCache.Set(t, spec);
return spec;
}
}
}