Looking for a clean way to parse out data from an API.
The API returns data for creating “Boat” and “Car” models wrapped up in metadata.
{
“timestamp” “0000”,
“data”: {
“count” “1”,
“results” [
{
“name”: “boatyMcBoatFace”
}
]
}
}
I want to create 2 classes for the metadata and also the boat/car data using custom fromJson() methods.
Current implementation is something like this
Class Boat {
String name;
Boat.fromJson(json) {
this.name = json[‘name’]
}
static List<Boat> listFromJson(List<dynamic> json) {
return json.map((c) => Boat.fromJson(c)).toList();
}
}
ResponseModel<T> {
String timestamp
DataModel data
ResponseModel.fromJson(json) {
this.timestamp = json[‘timestamp’]
this.data = DataModel<T>.fromJson(json[‘data’])
}
}
DataModel<T> {
String count
List<T> results
DataModel.fromJson(json) {
this.count = json[‘count’]
this.results = json[‘results’] // change this, see below
}
}
Currently I'm creating a ResponseModel, which in turn creates a DataModel.
But then I'm manually creating and setting Boats using:
// yes
final res = methodThatMakesHttpRequest();
final apiResponse = ResponseModel<Boat>.fromJson(res);
// no
final boats = Boat.listFromJson(apiResponse.data.results);
apiResponse.data.results = boats;
Ideally I would remove those last two lines, and instead have Boats get created dynamically within DataModel.fromJson with something like
DataModel.fromJson(json) {
this.count = json[‘count’]
T.listFromJson(json[‘results’])
}
But this of course does not work as T.listFromJson does not exist.
Related
I have the following code where I want to make sure my status will be localized in app, so I wanted to parse current locale and get value in needed language, but what it basically do, is simply remembering first language (locale) value parsed in it and staying with it till I restart my app
enum BillStatus {
sent,
processed,
...
}
extension BillStatusExtension on BillStatus {
static Map<BillStatus, String> names = {
BillStatus.sent: S.current.sent,
BillStatus.processed: S.current.processed,
...
};
String? get name => names[this];
}
S.current means AppLocalizationDelegate provided with intl package
When you do:
extension BillStatusExtension on BillStatus {
static Map<BillStatus, String> names = {
BillStatus.sent: S.current.sent,
BillStatus.processed: S.current.processed,
...
};
String? get name => names[this];
}
You're creating a names Map that's lazily initialized with whatever the values are of S.current.sent, S.current.processed, etc. are at the time names is first accessed. When the locale changes, that Map will never be updated and still will be referencing the same String objects.
You should do one of:
Look up S.current.sent, S.current.processed, etc. dynamically. An easy way to do this would be to change your Map to store Functions instead of Strings:
extension BillStatusExtension on BillStatus {
static Map<BillStatus, String Function()> names = {
BillStatus.sent: () => S.current.sent,
BillStatus.processed: () => S.current.processed,
...
};
String? get name => names[this]?.call();
}
Alternatively explicitly reinitialize the Map when the locale changes. This is more work and likely is overkill in this case, but it's a technique that you can use if recomputing values on every lookup is too expensive:
extension BillStatusExtension on BillStatus {
static Map<BillStatus, String> names = {};
static String _lastLocale = '';
static void _initializeNames() {
names[BillStatus.sent] = S.current.sent;
names[BillStatus.processed] = S.current.processed;
...
}
String? get name {
if (locale != _lastLocale) {
_initializeNames();
_lastLocale = locale;
}
return names[this];
}
}
Instead of reinitializing your Map lazily, you also could register a callback to do it immediately when the locale changes. See Is it possible to listen for system language change in Flutter?
Your code should look like this:
void main() {
final status = BillStatus.sent;
print(status.name); // Sent
}
enum BillStatus { sent }
class BillStatusHelper {
static const Map<BillStatus, String> names = {
BillStatus.sent: 'Sent',
};
}
extension BillStatusExtension on BillStatus {
String get name => BillStatusHelper.names[this] ?? 'Unknown';
}
My model properties definition is coming from a json file so using reflection to write the classes to be shown under schema on resulting swagger page.
foreach (var model in Models)
{
if (!ModelTypes.ContainsKey(model.Key))
{
anyNonCompiledModel = true;
BuildModelCodeClass(modelComponentBuilder, model.Value);//Build model classes
}
}
BuildModelCodeEnd(modelComponentBuilder);
if (anyNonCompiledModel)
{
CSharpCompiler compiler = new CSharpCompiler();
compiler.AddReference(typeof(object));
compiler.AddReference(typeof(ResourceFactory));
compiler.AddReference(typeof(System.Runtime.Serialization.DataContractResolver));
compiler.AddReference(typeof(System.Runtime.Serialization.DataContractAttribute));
var types = compiler.Compiler(modelComponentBuilder.ToString()); //write model classes
foreach (var type in types)
{
ModelTypes.Add(type.Name, type);
}
}
public void BuildModelCodeClass(StringBuilder modelComponentBuilder, MetadataModelEntity model)
{
modelComponentBuilder.AppendLine($"public class {model.Name} {{");
foreach (var p in model.Data.Properties)
{
if (p.Obsoleted) continue;
if (p.Type.Type == "array")
{
modelComponentBuilder.AppendLine($" public {p.Type.ArrayType.ObjectName}[] {p.Name} {{get;set;}}");
}
else
{
//primitive types
modelComponentBuilder.AppendLine($" public {p.Type.ObjectName} {p.Name} {{get;set;}}");
}
}
modelComponentBuilder.AppendLine(
#"}
");
}
If i provide the description and example like following (in BuildModelCodeClass, inside the loop) then the example and description displays for me.
if (!string.IsNullOrWhiteSpace((string)p.Example))
{
modelComponentBuilder.AppendLine($" ///<example>{p.Example}</example>");
}
if (!string.IsNullOrWhiteSpace((string)p.Description))
{
modelComponentBuilder.AppendLine($" ///<description>{p.Description}</description>");
}
However, i dont want to do above.
I want to write my models via the open api and not via the C# Compiler, is it possible?
I want to show example and description via schema (may be under paths some where). How can i do this? Context has my models info available that i can interact with here.
public class SwaggerDocumentFilter : IDocumentFilter
{
SwaggerDocument _swaggerDocument;
public SwaggerDocumentFilter(object apiConfigure)
{
_swaggerDocument = ((ApiGatewayConfiguration)apiConfigure).SwaggerDocument;
}
public void Apply(OpenApiDocument document, DocumentFilterContext context)
{
if (document.Info.Extensions == null || !document.Info.Extensions.ContainsKey(SwaggerEndpoint.ExtensionDocName)) return;
var openIdString = document.Info.Extensions[SwaggerEndpoint.ExtensionDocName] as OpenApiString;
if (openIdString == null) return;
var docName = openIdString.Value;
SwaggerEndpoint endpoint = _swaggerDocument.SwaggerEndpoints.SingleOrDefault(x => x.Name == docName);
if (endpoint == null) return;
//Add server objects
document.Servers = endpoint.ServerObjects;
//Add Tags objects
document.Tags = endpoint.Tags;
//Set swagger paths objects
var pathsObjects = _swaggerDocument.GetPathsObject(docName, context);
if (pathsObjects.IsValid())
{
pathsObjects.ToList().ForEach(
item => document.Paths.Add(item.Key, item.Value)
);
}
//Add Schema components
//Add Example/Examples
}
}
Following helped
https://github.com/domaindrivendev/Swashbuckle.WebApi/issues/162
AddSchemaExamples.cs
public class AddSchemaExamples : ISchemaFilter
{
public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type)
{
if (type == typeof(Product))
{
schema.example = new Product
{
Id = 123,
Type = ProductType.Book,
Description = "Treasure Island",
UnitPrice = 10.0M
};
}
}
}
SwaggerConfig.cs
httpConfig
.EnableSwagger(c =>
{
c.SchemaFilter<AddSchemaExamples>()
});
My implementation for the Apply since model is dynamic
if (model != null)
{
schema.Description = model.Description;
foreach (var p in schema.Properties)
{
var mp = model.Data.Properties.SingleOrDefault(x => x.Name == p.Key);
if (mp != null)
{
if (!string.IsNullOrWhiteSpace(mp.Description))
{
p.Value.Description = mp.Description;
}
if(!string.IsNullOrWhiteSpace(mp.Example))
{
p.Value.Example =
new Microsoft.OpenApi.Any.OpenApiString(mp.Example.ToString());
}
}
}
}
I have 2 datamodels. the userDataModel and the PostDataModel.
Users may have multiple posts
so I would like to have the following fetching structure:
Map<userDataModel,List<PostDataModel>> = data
Can I iterate through data using indexes? (user index and then posts indexes ?)
something like for(user, data in data){}
or something like data[0][0]
In the end, I would like to have a listView of all the posts, one user at the time
I also would like to know how to print the data since printing data only returns instances.
First, using the structure you provided, Map<userDataModel,List<PostDataModel>>, you have to have two nested loops. To be able to iterate through all of them you will need to have some code like the following:
void main() {
var data = Map<UserDataModel, List<PostDataModel>>();
data.forEach((user, posts) {
for (var post in posts) {
print(post.name);
print(user.id);
}
});
}
class UserDataModel {
int id;
}
class PostDataModel {
String name;
}
However, since, as you mentioned, your posts belong to users, I would put the PostDataModel inside the UserDataModel. Then, your data will eventually look a little different:
void main() {
var data = List<UserDataModel>();
data.add(UserDataModel(1, [PostDataModel("one"),PostDataModel("two")]));
data.add(UserDataModel(2, [PostDataModel("three"),PostDataModel("four")]));
for (var user in data) {
for (var post in user.posts) {
print(user);
print(post);
}
}
}
class UserDataModel {
int id;
UserDataModel(this.id, this.posts);
List<PostDataModel> posts;
#override
String toString() {
var result = "";
result += "Id: $id\n";
var count = 0;
for(var post in posts) {
result += "Post $count, $post";
}
return result;
}
}
class PostDataModel {
String name;
PostDataModel(this.name);
#override
String toString() {
return "Name: $name\n";
}
}
It depends on your backend structure but I would suggest the second way.
As an answer to the second part of your question, look at the method toString. If you override it, you can customize the way your class is displayed when, for example, you use print(userDataModel. I created those classes as for example, so customize them for your own classes.
In fact, with the last example, you can access your posts by data[0].posts[0] as you requested.
I guess you could do something like
var data = userDataModel.map((user) => postDataModel.map((post) { post.useriD == user.userId ? post : null}).toList()).toList();
One possible solution is to iterate over the Map and over the List in a nested forEach:
data.forEach((user, posts) {
print(user);
posts.forEach((post) {
print(post);
});
print('---'); // end of the user's posts
});
I'm looking for the best approach to update an ObservableList with objects.
I've created a dummy example below with Car and a store with Cars.
In JavaScript I can simply call
const carData = {id: 1, name: "Chrysler"};
update(carData);
and in the update method:
#action
updateCar(carData) {
cars = cars.map(car => car.id === carData.id ? {...car, ...carData} : car);
}
How do one achieve the same in Flutter with MobX?
class Car {
int id;
String model;
DateTime year;
}
abstract class _CarCollectionStore with Store {
#observable
ObservableList<Car> cars = ObservableList<Car>();
#computed
get latestCar() => iterating and getting latest Car by year.
#action
updateCar(WHICH PARAMETERS?) {
...
}
}
How do I actually update the Name of the latestCar?
carCollectionStore = Provider.of<CarCollectionStore>(context, listen: false);
carNameController = TextEditingController(text: carCollectionStore.latestCar.name);
carNameController.addListener(() {
carCollectionStore.updateCar(carNameController.text);
});
I am currently implementing a RESTFUL API that provides endpoints to interface with a database .
I want to implement filtering in my API , but I need to provide an endpoint that can provide a way to apply filtering on a table using all the table's columns.
I've found some patterns such as :
GET /api/ressource?param1=value1,param2=value2...paramN=valueN
param1,param2...param N being my table columns and the values.
I've also found another pattern that consists of send a JSON object that represents the query .
To filter on a field, simply add that field and its value to the query :
GET /app/items
{
"items": [
{
"param1": "value1",
"param2": "value",
"param N": "value N"
}
]
}
I'm looking for the best practice to achieve this .
I'm using EF Core with ASP.NET Core for implementing this.
Firstly be cautious about filtering on everything/anything. Base the available filters on what users will need and expand from that depending on demand. Less code to write, less complexity, fewer indexes needed on the DB side, better performance.
That said, the approach I use for pages that have a significant number of filters is to use an enumeration server side where my criteria fields are passed back their enumeration value (number) to provide on the request. So a filter field would comprise of a name, default or applicable values, and an enumeration value to use when passing an entered or selected value back to the search. The requesting code creates a JSON object with the applied filters and Base64's it to send in the request:
I.e.
{
p1: "Jake",
p2: "8"
}
The query string looks like:
.../api/customer/search?filters=XHgde0023GRw....
On the server side I extract the Base64 then parse it as a Dictionary<string,string> to feed to the filter parsing. For example given that the criteria was for searching for a child using name and age:
// this is the search filter keys, these (int) values are passed to the search client for each filter field.
public enum FilterKeys
{
None = 0,
Name,
Age,
ParentName
}
public JsonResult Search(string filters)
{
string filterJson = Encoding.UTF8.GetString(Convert.FromBase64String(filters));
var filterData = JsonConvert.DeserializeObject<Dictionary<string, string>>(filterJson);
using (var context = new TestDbContext())
{
var query = context.Children.AsQueryable();
foreach (var filter in filterData)
query = filterChildren(query, filter.Key, filter.Value);
var results = query.ToList(); //example fetch.
// TODO: Get the results, package up view models, and return...
}
}
private IQueryable<Child> filterChildren(IQueryable<Child> query, string key, string value)
{
var filterKey = parseFilterKey(key);
if (filterKey == FilterKeys.None)
return query;
switch (filterKey)
{
case FilterKeys.Name:
query = query.Where(x => x.Name == value);
break;
case FilterKeys.Age:
DateTime birthDateStart = DateTime.Today.AddYears((int.Parse(value) + 1) * -1);
DateTime birthDateEnd = birthDateStart.AddYears(1);
query = query.Where(x => x.BirthDate <= birthDateEnd && x.BirthDate >= birthDateStart);
break;
}
return query;
}
private FilterKeys parseFilterKey(string key)
{
FilterKeys filterKey = FilterKeys.None;
Enum.TryParse(key.Substring(1), out filterKey);
return filterKey;
}
You can use strings and constants to avoid the enum parsing, however I find enums are readable and keep the sent payload a little more compact. The above is a simplified example and obviously needs error checking. The implementation code for complex filter conditions such as the age to birth date above would better be suited as a separate method, but it should give you some ideas. You can search for children by name, and/or age, and/or parent's name for example.
I have invented and found it useful to combine a few filters into one type for example CommonFilters and make this type parseable from string:
[TypeConverter(typeof(CommonFiltersTypeConverter))]
public class CommonFilters
{
public PageOptions PageOptions { get; set; }
public Range<decimal> Amount { get; set; }
//... other filters
[JsonIgnore]
public bool HasAny => Amount.HasValue || PageOptions!=null;
public static bool TryParse(string str, out CommonFilters result)
{
result = new CommonFilters();
if (string.IsNullOrEmpty(str))
return false;
var parts = str.Split(new[] { ' ', ';' }, StringSplitOptions.RemoveEmptyEntries);
foreach (var part in parts)
{
if (part.StartsWith("amount:") && Range<decimal>.TryParse(part.Substring(7), out Range<decimal> amount))
{
result.Amount = amount;
continue;
}
if (part.StartsWith("page-options:") && PageOptions.TryParse(part.Substring(13), out PageOptions pageOptions))
{
result.PageOptions = pageOptions;
continue;
}
//etc.
}
return result.HasAny;
}
public static implicit operator CommonFilters(string str)
{
if (TryParse(str, out CommonFilters res))
return res;
return null;
}
}
public class CommonFiltersTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
{
return true;
}
return base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context,
CultureInfo culture, object value)
{
if (value is string str)
{
if (CommonFilters.TryParse(str, out CommonFilters obj))
{
return obj;
}
}
return base.ConvertFrom(context, culture, value);
}
}
the request looks like this:
public class GetOrdersRequest
{
[DefaultValue("page-options:50;amount:0.001-1000;min-qty:10")]
public CommonFilters Filters { get; set; }
//...other stuff
}
In this way you reduce the number of input request parameters, especially when some queries don't care about all filters
If you use swagger map this type as string:
c.MapTypeAsString<CommonFilters>();
public static void MapTypeAsString<T>(this SwaggerGenOptions swaggerGenOptions)
{
swaggerGenOptions.MapType(typeof(T), () => new OpenApiSchema(){Type = "string"});
}