Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/tonik/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
<a href="https://pub.dev/packages/very_good_analysis"><img src="https://img.shields.io/badge/style-very_good_analysis-B22C89.svg"></a>
<a href="https://github.com/invertase/melos"><img src="https://img.shields.io/badge/maintained%20with-melos-f700ff.svg?style=flat-square"></a>
<a href="https://zread.ai/t-unit/tonik" target="_blank"><img src="https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff" alt="zread"/></a>
<a href="https://codecov.io/gh/t-unit/tonik" >
<img src="https://codecov.io/gh/t-unit/tonik/graph/badge.svg?token=RWSKCAYLO1"/>
</a>
</p>


Expand Down
22 changes: 4 additions & 18 deletions packages/tonik_core/lib/src/model/request_body.dart
Original file line number Diff line number Diff line change
Expand Up @@ -115,30 +115,16 @@ class RequestBodyObject extends RequestBody {
'isRequired: $isRequired, content: $content)';
}

@immutable
class RequestContent {
const RequestContent({
RequestContent({
required this.model,
required this.contentType,
required this.rawContentType,
});

final Model model;
final ContentType contentType;
final String rawContentType;

@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is! RequestContent) return false;

return model == other.model &&
contentType == other.contentType &&
rawContentType == other.rawContentType;
}

@override
int get hashCode => Object.hash(model, contentType, rawContentType);
Model model;
ContentType contentType;
String rawContentType;

@override
String toString() =>
Expand Down
22 changes: 4 additions & 18 deletions packages/tonik_core/lib/src/model/response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'package:tonik_core/tonik_core.dart';

@immutable
sealed class Response {
const Response({required this.name, required this.context});

Expand Down Expand Up @@ -128,29 +127,16 @@ class ResponseObject extends Response {
'description: $description, bodies: $bodies)';
}

@immutable
class ResponseBody {
const ResponseBody({
ResponseBody({
required this.model,
required this.rawContentType,
required this.contentType,
});

final Model model;
final String rawContentType;
final ContentType contentType;

@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is! ResponseBody) return false;
return model == other.model &&
rawContentType == other.rawContentType &&
contentType == other.contentType;
}

@override
int get hashCode => Object.hash(model, rawContentType);
Model model;
String rawContentType;
ContentType contentType;

@override
String toString() => 'ResponseBody(model: $model, contentType: $contentType)';
Expand Down
39 changes: 6 additions & 33 deletions packages/tonik_core/lib/src/model/response_header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,8 @@ class ResponseHeaderAlias extends ResponseHeader {
int get hashCode => Object.hash(name, header, description);
}

@immutable
class ResponseHeaderObject extends ResponseHeader {
const ResponseHeaderObject({
ResponseHeaderObject({
required super.name,
required super.context,
required this.description,
Expand All @@ -85,41 +84,15 @@ class ResponseHeaderObject extends ResponseHeader {
@override
final String? description;

final bool explode;
final Model model;
final bool isRequired;
final bool isDeprecated;
final ResponseHeaderEncoding encoding;
bool explode;
Model model;
bool isRequired;
bool isDeprecated;
ResponseHeaderEncoding encoding;

@override
String toString() =>
'HeaderObject{name: $name, description: $description, '
'explode: $explode, model: $model, isRequired: $isRequired, '
'isDeprecated: $isDeprecated, encoding: $encoding, context: $context}';

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ResponseHeaderObject &&
runtimeType == other.runtimeType &&
name == other.name &&
description == other.description &&
explode == other.explode &&
model == other.model &&
isRequired == other.isRequired &&
isDeprecated == other.isDeprecated &&
encoding == other.encoding &&
context == other.context;

@override
int get hashCode => Object.hash(
name,
description,
explode,
model,
isRequired,
isDeprecated,
encoding,
context,
);
}
252 changes: 252 additions & 0 deletions packages/tonik_core/lib/src/transformer/allof_normalizer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import 'package:meta/meta.dart';
import 'package:tonik_core/tonik_core.dart';

/// Normalizes AllOfModel instances with a single contained model to AliasModel.
///
/// This transformer performs in-place replacement throughout the entire
/// document, ensuring referential consistency by memoizing transformations.
@immutable
class AllOfNormalizer {
const AllOfNormalizer();

/// Normalizes allOf schemas with a single model to type aliases.
///
/// This simplifies patterns like `allOf: [$ref, {description: ...}]` used
/// by Spotify and others to add descriptions to referenced schemas.
ApiDocument apply(ApiDocument document) {
final cache = <Model, Model>{};

final transformedModels = <Model>{};
for (final model in document.models) {
transformedModels.add(_transformModel(model, cache));
}
document.models = transformedModels;

for (final model in document.models) {
if (model is ClassModel) {
for (final prop in model.properties) {
final transformed = cache[prop.model];
if (transformed != null && transformed != prop.model) {
prop.model = transformed;
}
}
}
}

for (final response in document.responses) {
_updateResponseModels(response, cache);
}

for (final operation in document.operations) {
for (final response in operation.responses.values) {
_updateResponseModels(response, cache);
}

final requestBody = operation.requestBody;
if (requestBody != null) {
_updateRequestBodyModels(requestBody, cache);
}

for (final header in operation.headers) {
_updateRequestHeaderModel(header, cache);
}

for (final param in operation.queryParameters) {
_updateQueryParameterModel(param, cache);
}

for (final param in operation.pathParameters) {
_updatePathParameterModel(param, cache);
}
}

for (final requestBody in document.requestBodies) {
_updateRequestBodyModels(requestBody, cache);
}

for (final header in document.responseHeaders) {
_updateResponseHeaderModel(header, cache);
}

for (final header in document.requestHeaders) {
_updateRequestHeaderModel(header, cache);
}

for (final param in document.queryParameters) {
_updateQueryParameterModel(param, cache);
}
for (final param in document.pathParameters) {
_updatePathParameterModel(param, cache);
}

return document;
}

/// Transforms a model, normalizing single-model AllOfModels to AliasModels.
Model _transformModel(Model model, Map<Model, Model> cache) {
if (cache.containsKey(model)) {
return cache[model]!;
}

// Placeholder to handle cycles
cache[model] = model;

final Model result;

if (model is AllOfModel && model.models.length == 1) {
final containedModel = _transformModel(model.models.first, cache);
result = AliasModel(
name: model.name,
model: containedModel,
context: model.context,
description: model.description,
isDeprecated: model.isDeprecated,
isNullable: model.isNullable,
nameOverride: model.nameOverride,
);
} else if (model is AllOfModel) {
final newModels = <Model>{};
for (final m in model.models) {
newModels.add(_transformModel(m, cache));
}
model.models
..clear()
..addAll(newModels);
result = model;
} else if (model is ClassModel) {
for (final prop in model.properties) {
prop.model = _transformModel(prop.model, cache);
}
result = model;
} else if (model is OneOfModel) {
final newModels = <({String? discriminatorValue, Model model})>{};
for (final m in model.models) {
newModels.add((
discriminatorValue: m.discriminatorValue,
model: _transformModel(m.model, cache),
));
}
model.models
..clear()
..addAll(newModels);
result = model;
} else if (model is AnyOfModel) {
final newModels = <({String? discriminatorValue, Model model})>{};
for (final m in model.models) {
newModels.add((
discriminatorValue: m.discriminatorValue,
model: _transformModel(m.model, cache),
));
}
model.models
..clear()
..addAll(newModels);
result = model;
} else if (model is ListModel) {
model.content = _transformModel(model.content, cache);
result = model;
} else if (model is AliasModel) {
model.model = _transformModel(model.model, cache);
result = model;
} else {
result = model;
}

cache[model] = result;
return result;
}

void _updateResponseModels(Response response, Map<Model, Model> cache) {
switch (response) {
case ResponseAlias():
_updateResponseModels(response.response, cache);
case ResponseObject():
for (final body in response.bodies) {
final transformed = cache[body.model];
if (transformed != null && transformed != body.model) {
body.model = transformed;
}
}
for (final header in response.headers.values) {
_updateResponseHeaderModel(header, cache);
}
}
}

void _updateRequestBodyModels(
RequestBody requestBody,
Map<Model, Model> cache,
) {
switch (requestBody) {
case RequestBodyAlias():
_updateRequestBodyModels(requestBody.requestBody, cache);
case RequestBodyObject():
for (final content in requestBody.content) {
final transformed = cache[content.model];
if (transformed != null && transformed != content.model) {
content.model = transformed;
}
}
}
}

void _updateResponseHeaderModel(
ResponseHeader header,
Map<Model, Model> cache,
) {
switch (header) {
case ResponseHeaderAlias():
_updateResponseHeaderModel(header.header, cache);
case ResponseHeaderObject():
final transformed = cache[header.model];
if (transformed != null && transformed != header.model) {
header.model = transformed;
}
}
}

void _updateRequestHeaderModel(
RequestHeader header,
Map<Model, Model> cache,
) {
switch (header) {
case RequestHeaderAlias():
_updateRequestHeaderModel(header.header, cache);
case RequestHeaderObject():
final transformed = cache[header.model];
if (transformed != null && transformed != header.model) {
header.model = transformed;
}
}
}

void _updateQueryParameterModel(
QueryParameter param,
Map<Model, Model> cache,
) {
switch (param) {
case QueryParameterAlias():
_updateQueryParameterModel(param.parameter, cache);
case QueryParameterObject():
final transformed = cache[param.model];
if (transformed != null && transformed != param.model) {
param.model = transformed;
}
}
}

void _updatePathParameterModel(
PathParameter param,
Map<Model, Model> cache,
) {
switch (param) {
case PathParameterAlias():
_updatePathParameterModel(param.parameter, cache);
case PathParameterObject():
final transformed = cache[param.model];
if (transformed != null && transformed != param.model) {
param.model = transformed;
}
}
}
}
Loading