mirror of
https://github.com/foss42/apidash.git
synced 2025-12-02 18:57:05 +08:00
fix: base url creation for openapi spec
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:openapi_spec/openapi_spec.dart';
|
import 'package:openapi_spec/openapi_spec.dart';
|
||||||
|
|
||||||
@@ -55,6 +57,11 @@ class DashbotImportNowButton extends ConsumerWidget with DashbotActionMixin {
|
|||||||
method: s.method,
|
method: s.method,
|
||||||
op: s.op,
|
op: s.op,
|
||||||
);
|
);
|
||||||
|
log("SorceName: $sourceName");
|
||||||
|
payload['sourceName'] =
|
||||||
|
(sourceName != null && sourceName.trim().isNotEmpty)
|
||||||
|
? sourceName
|
||||||
|
: spec.info.title;
|
||||||
await chatNotifier.applyAutoFix(ChatAction.fromJson({
|
await chatNotifier.applyAutoFix(ChatAction.fromJson({
|
||||||
'action': 'apply_openapi',
|
'action': 'apply_openapi',
|
||||||
'actionType': 'apply_openapi',
|
'actionType': 'apply_openapi',
|
||||||
|
|||||||
@@ -65,4 +65,110 @@ class UrlEnvService {
|
|||||||
final normalized = path.startsWith('/') ? path : '/$path';
|
final normalized = path.startsWith('/') ? path : '/$path';
|
||||||
return '{{$key}}$normalized';
|
return '{{$key}}$normalized';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensure (or create) an environment variable for a base URL coming from an
|
||||||
|
/// OpenAPI spec import. If the spec had no concrete host (thus parsing the
|
||||||
|
/// base URL yields no host) we derive a key using the first word of the spec
|
||||||
|
/// title to avoid every unrelated spec collapsing to BASE_URL_API.
|
||||||
|
///
|
||||||
|
/// Behaviour:
|
||||||
|
/// - If [baseUrl] is empty: returns a derived key `BASE_URL_<TITLEWORD>` but
|
||||||
|
/// does NOT create an env var (there is no value to store yet).
|
||||||
|
/// - If [baseUrl] has a host: behaves like [ensureBaseUrlEnv]. If the host
|
||||||
|
/// itself cannot be determined, it substitutes the first title word slug.
|
||||||
|
Future<String> ensureBaseUrlEnvForOpenApi(
|
||||||
|
String baseUrl, {
|
||||||
|
required String title,
|
||||||
|
required Map<String, EnvironmentModel>? Function() readEnvs,
|
||||||
|
required String? Function() readActiveEnvId,
|
||||||
|
required void Function(String id, {List<EnvironmentVariableModel>? values})
|
||||||
|
updateEnv,
|
||||||
|
}) async {
|
||||||
|
// Derive slug from title's first word upfront (used as fallback)
|
||||||
|
final titleSlug = _slugFromOpenApiTitleFirstWord(title);
|
||||||
|
final trimmedBase = baseUrl.trim();
|
||||||
|
final isTrivial = trimmedBase.isEmpty ||
|
||||||
|
trimmedBase == '/' ||
|
||||||
|
// path-only or variable server (no scheme and no host component)
|
||||||
|
(!trimmedBase.startsWith('http://') &&
|
||||||
|
!trimmedBase.startsWith('https://') &&
|
||||||
|
!trimmedBase.contains('://'));
|
||||||
|
if (isTrivial) {
|
||||||
|
final key = 'BASE_URL_$titleSlug';
|
||||||
|
|
||||||
|
final envs = readEnvs();
|
||||||
|
String? activeId = readActiveEnvId();
|
||||||
|
activeId ??= kGlobalEnvironmentId;
|
||||||
|
final envModel = envs?[activeId];
|
||||||
|
if (envModel != null) {
|
||||||
|
final exists = envModel.values.any((v) => v.key == key);
|
||||||
|
if (!exists) {
|
||||||
|
final values = [...envModel.values];
|
||||||
|
values.add(
|
||||||
|
EnvironmentVariableModel(
|
||||||
|
key: key,
|
||||||
|
value: trimmedBase == '/' ? '' : trimmedBase,
|
||||||
|
enabled: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
updateEnv(activeId, values: values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
String host = 'API';
|
||||||
|
try {
|
||||||
|
final u = Uri.parse(baseUrl);
|
||||||
|
if (u.hasAuthority && u.host.isNotEmpty) host = u.host;
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
// If host could not be determined (remains 'API'), use title-based slug.
|
||||||
|
final slug = (host == 'API')
|
||||||
|
? titleSlug
|
||||||
|
: host
|
||||||
|
.replaceAll(RegExp(r'[^A-Za-z0-9]+'), '_')
|
||||||
|
.replaceAll(RegExp(r'_+'), '_')
|
||||||
|
.replaceAll(RegExp(r'^_|_$'), '')
|
||||||
|
.toUpperCase();
|
||||||
|
final key = 'BASE_URL_$slug';
|
||||||
|
|
||||||
|
final envs = readEnvs();
|
||||||
|
String? activeId = readActiveEnvId();
|
||||||
|
activeId ??= kGlobalEnvironmentId;
|
||||||
|
final envModel = envs?[activeId];
|
||||||
|
|
||||||
|
if (envModel != null) {
|
||||||
|
final exists = envModel.values.any((v) => v.key == key);
|
||||||
|
if (!exists) {
|
||||||
|
final values = [...envModel.values];
|
||||||
|
values.add(EnvironmentVariableModel(
|
||||||
|
key: key,
|
||||||
|
value: baseUrl,
|
||||||
|
enabled: true,
|
||||||
|
));
|
||||||
|
updateEnv(activeId, values: values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a slug from the first word of an OpenAPI spec title.
|
||||||
|
/// Example: "Pet Store API" -> "PET"; " My-Orders Service" -> "MY".
|
||||||
|
/// Falls back to 'API' if no alphanumeric characters are present.
|
||||||
|
String _slugFromOpenApiTitleFirstWord(String title) {
|
||||||
|
final trimmed = title.trim();
|
||||||
|
if (trimmed.isEmpty) return 'API';
|
||||||
|
// Split on whitespace, take first non-empty token
|
||||||
|
final firstToken = trimmed.split(RegExp(r'\s+')).firstWhere(
|
||||||
|
(t) => t.trim().isNotEmpty,
|
||||||
|
orElse: () => 'API',
|
||||||
|
);
|
||||||
|
final cleaned = firstToken
|
||||||
|
.replaceAll(RegExp(r'[^A-Za-z0-9]+'), '_')
|
||||||
|
.replaceAll(RegExp(r'_+'), '_')
|
||||||
|
.replaceAll(RegExp(r'^_|_$'), '')
|
||||||
|
.toUpperCase();
|
||||||
|
return cleaned.isEmpty ? 'API' : cleaned;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'package:openapi_spec/openapi_spec.dart';
|
||||||
import 'package:apidash/dashbot/features/chat/models/chat_message.dart';
|
import 'package:apidash/dashbot/features/chat/models/chat_message.dart';
|
||||||
import 'package:apidash_core/apidash_core.dart';
|
import 'package:apidash_core/apidash_core.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
@@ -38,9 +39,7 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
|
|
||||||
List<ChatMessage> get currentMessages {
|
List<ChatMessage> get currentMessages {
|
||||||
final id = _currentRequest?.id ?? 'global';
|
final id = _currentRequest?.id ?? 'global';
|
||||||
debugPrint('[Chat] Getting messages for request ID: $id');
|
|
||||||
final messages = state.chatSessions[id] ?? const [];
|
final messages = state.chatSessions[id] ?? const [];
|
||||||
debugPrint('[Chat] Found ${messages.length} messages');
|
|
||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,8 +48,6 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
ChatMessageType type = ChatMessageType.general,
|
ChatMessageType type = ChatMessageType.general,
|
||||||
bool countAsUser = true,
|
bool countAsUser = true,
|
||||||
}) async {
|
}) async {
|
||||||
debugPrint(
|
|
||||||
'[Chat] sendMessage start: type=$type, countAsUser=$countAsUser');
|
|
||||||
final ai = _selectedAIModel;
|
final ai = _selectedAIModel;
|
||||||
if (text.trim().isEmpty && countAsUser) return;
|
if (text.trim().isEmpty && countAsUser) return;
|
||||||
if (ai == null &&
|
if (ai == null &&
|
||||||
@@ -66,7 +63,6 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
|
|
||||||
final requestId = _currentRequest?.id ?? 'global';
|
final requestId = _currentRequest?.id ?? 'global';
|
||||||
final existingMessages = state.chatSessions[requestId] ?? const [];
|
final existingMessages = state.chatSessions[requestId] ?? const [];
|
||||||
debugPrint('[Chat] using requestId=$requestId');
|
|
||||||
|
|
||||||
if (countAsUser) {
|
if (countAsUser) {
|
||||||
_addMessage(
|
_addMessage(
|
||||||
@@ -211,8 +207,6 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
userPrompt: userPrompt,
|
userPrompt: userPrompt,
|
||||||
stream: false,
|
stream: false,
|
||||||
);
|
);
|
||||||
debugPrint(
|
|
||||||
'[Chat] prompts prepared: system=${systemPrompt.length} chars, user=${userPrompt.length} chars');
|
|
||||||
|
|
||||||
state = state.copyWith(isGenerating: true, currentStreamingResponse: '');
|
state = state.copyWith(isGenerating: true, currentStreamingResponse: '');
|
||||||
try {
|
try {
|
||||||
@@ -297,17 +291,20 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
|
|
||||||
Future<void> applyAutoFix(ChatAction action) async {
|
Future<void> applyAutoFix(ChatAction action) async {
|
||||||
try {
|
try {
|
||||||
|
if (action.actionType == ChatActionType.applyOpenApi) {
|
||||||
|
await _applyOpenApi(action);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action.actionType == ChatActionType.applyCurl) {
|
||||||
|
await _applyCurl(action);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final msg = await _ref.read(autoFixServiceProvider).apply(action);
|
final msg = await _ref.read(autoFixServiceProvider).apply(action);
|
||||||
if (msg != null && msg.isNotEmpty) {
|
if (msg != null && msg.isNotEmpty) {
|
||||||
// Message type depends on action context; choose sensible defaults
|
final t = ChatMessageType.general;
|
||||||
final t = (action.actionType == ChatActionType.applyCurl)
|
|
||||||
? ChatMessageType.importCurl
|
|
||||||
: (action.actionType == ChatActionType.applyOpenApi)
|
|
||||||
? ChatMessageType.importOpenApi
|
|
||||||
: ChatMessageType.general;
|
|
||||||
_appendSystem(msg, t);
|
_appendSystem(msg, t);
|
||||||
}
|
}
|
||||||
// Only target-specific 'other' actions remain here
|
|
||||||
if (action.actionType == ChatActionType.other) {
|
if (action.actionType == ChatActionType.other) {
|
||||||
await _applyOtherAction(action);
|
await _applyOtherAction(action);
|
||||||
}
|
}
|
||||||
@@ -344,7 +341,6 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _applyOpenApi(ChatAction action) async {
|
Future<void> _applyOpenApi(ChatAction action) async {
|
||||||
final requestId = _currentRequest?.id;
|
|
||||||
final collection = _ref.read(collectionStateNotifierProvider.notifier);
|
final collection = _ref.read(collectionStateNotifierProvider.notifier);
|
||||||
final payload = action.value is Map<String, dynamic>
|
final payload = action.value is Map<String, dynamic>
|
||||||
? (action.value as Map<String, dynamic>)
|
? (action.value as Map<String, dynamic>)
|
||||||
@@ -413,28 +409,26 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final withEnvUrl = await _maybeSubstituteBaseUrl(url, baseUrl);
|
|
||||||
if (action.field == 'apply_to_selected') {
|
String sourceTitle = (payload['sourceName'] as String?) ?? '';
|
||||||
if (requestId == null) return;
|
if (sourceTitle.trim().isEmpty) {
|
||||||
final replacingBody =
|
final specObj = payload['spec'];
|
||||||
(formFlag || formData.isNotEmpty) ? '' : (body ?? '');
|
if (specObj is OpenApi) {
|
||||||
final replacingFormData =
|
try {
|
||||||
formData.isEmpty ? const <FormDataModel>[] : formData;
|
final t = specObj.info.title.trim();
|
||||||
collection.update(
|
if (t.isNotEmpty) sourceTitle = t;
|
||||||
method: method,
|
} catch (_) {}
|
||||||
url: withEnvUrl,
|
}
|
||||||
headers: headers,
|
}
|
||||||
isHeaderEnabledList: List<bool>.filled(headers.length, true),
|
debugPrint('[OpenAPI] baseUrl="$baseUrl" title="$sourceTitle" url="$url"');
|
||||||
body: replacingBody,
|
final withEnvUrl = await _maybeSubstituteBaseUrlForOpenApi(
|
||||||
bodyContentType: bodyContentType,
|
url,
|
||||||
formData: replacingFormData,
|
baseUrl,
|
||||||
params: const [],
|
sourceTitle,
|
||||||
isParamEnabledList: const [],
|
|
||||||
authModel: null,
|
|
||||||
);
|
);
|
||||||
_appendSystem('Applied OpenAPI operation to the selected request.',
|
debugPrint('[OpenAPI] withEnvUrl="$withEnvUrl');
|
||||||
ChatMessageType.importOpenApi);
|
if (action.field == 'apply_to_new') {
|
||||||
} else if (action.field == 'apply_to_new') {
|
debugPrint('[OpenAPI] withEnvUrl="$withEnvUrl');
|
||||||
final model = HttpRequestModel(
|
final model = HttpRequestModel(
|
||||||
method: method,
|
method: method,
|
||||||
url: withEnvUrl,
|
url: withEnvUrl,
|
||||||
@@ -936,8 +930,6 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
void _addMessage(String requestId, ChatMessage m) {
|
void _addMessage(String requestId, ChatMessage m) {
|
||||||
debugPrint(
|
|
||||||
'[Chat] Adding message to request ID: $requestId, actions: ${m.actions?.map((e) => e.toJson()).toList()}');
|
|
||||||
final msgs = state.chatSessions[requestId] ?? const [];
|
final msgs = state.chatSessions[requestId] ?? const [];
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
chatSessions: {
|
chatSessions: {
|
||||||
@@ -945,8 +937,6 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
requestId: [...msgs, m],
|
requestId: [...msgs, m],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
debugPrint(
|
|
||||||
'[Chat] Message added, total messages for $requestId: ${(state.chatSessions[requestId]?.length ?? 0)}');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _appendSystem(String text, ChatMessageType type) {
|
void _appendSystem(String text, ChatMessageType type) {
|
||||||
@@ -1004,6 +994,24 @@ class ChatViewmodel extends StateNotifier<ChatState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String> _maybeSubstituteBaseUrlForOpenApi(
|
||||||
|
String url, String baseUrl, String title) async {
|
||||||
|
final svc = _ref.read(urlEnvServiceProvider);
|
||||||
|
return svc.maybeSubstituteBaseUrl(
|
||||||
|
url,
|
||||||
|
baseUrl,
|
||||||
|
ensure: (b) => svc.ensureBaseUrlEnvForOpenApi(
|
||||||
|
b,
|
||||||
|
title: title,
|
||||||
|
readEnvs: () => _ref.read(environmentsStateNotifierProvider),
|
||||||
|
readActiveEnvId: () => _ref.read(activeEnvironmentIdStateProvider),
|
||||||
|
updateEnv: (id, {values}) => _ref
|
||||||
|
.read(environmentsStateNotifierProvider.notifier)
|
||||||
|
.updateEnvironment(id, values: values),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
HttpRequestModel _getSubstitutedHttpRequestModel(
|
HttpRequestModel _getSubstitutedHttpRequestModel(
|
||||||
HttpRequestModel httpRequestModel) {
|
HttpRequestModel httpRequestModel) {
|
||||||
final envMap = _ref.read(availableEnvironmentVariablesStateProvider);
|
final envMap = _ref.read(availableEnvironmentVariablesStateProvider);
|
||||||
|
|||||||
Reference in New Issue
Block a user