Skip to content

Commit 15821a0

Browse files
committed
fix: base url creation for openapi spec
1 parent d2a5a77 commit 15821a0

File tree

3 files changed

+162
-41
lines changed

3 files changed

+162
-41
lines changed

lib/dashbot/core/common/widgets/dashbot_action_buttons/dashbot_import_now_button.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'dart:developer';
2+
13
import 'package:flutter/material.dart';
24
import 'package:openapi_spec/openapi_spec.dart';
35

@@ -55,6 +57,11 @@ class DashbotImportNowButton extends ConsumerWidget with DashbotActionMixin {
5557
method: s.method,
5658
op: s.op,
5759
);
60+
log("SorceName: $sourceName");
61+
payload['sourceName'] =
62+
(sourceName != null && sourceName.trim().isNotEmpty)
63+
? sourceName
64+
: spec.info.title;
5865
await chatNotifier.applyAutoFix(ChatAction.fromJson({
5966
'action': 'apply_openapi',
6067
'actionType': 'apply_openapi',

lib/dashbot/core/services/base/url_env_service.dart

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,110 @@ class UrlEnvService {
6565
final normalized = path.startsWith('/') ? path : '/$path';
6666
return '{{$key}}$normalized';
6767
}
68+
69+
/// Ensure (or create) an environment variable for a base URL coming from an
70+
/// OpenAPI spec import. If the spec had no concrete host (thus parsing the
71+
/// base URL yields no host) we derive a key using the first word of the spec
72+
/// title to avoid every unrelated spec collapsing to BASE_URL_API.
73+
///
74+
/// Behaviour:
75+
/// - If [baseUrl] is empty: returns a derived key `BASE_URL_<TITLEWORD>` but
76+
/// does NOT create an env var (there is no value to store yet).
77+
/// - If [baseUrl] has a host: behaves like [ensureBaseUrlEnv]. If the host
78+
/// itself cannot be determined, it substitutes the first title word slug.
79+
Future<String> ensureBaseUrlEnvForOpenApi(
80+
String baseUrl, {
81+
required String title,
82+
required Map<String, EnvironmentModel>? Function() readEnvs,
83+
required String? Function() readActiveEnvId,
84+
required void Function(String id, {List<EnvironmentVariableModel>? values})
85+
updateEnv,
86+
}) async {
87+
// Derive slug from title's first word upfront (used as fallback)
88+
final titleSlug = _slugFromOpenApiTitleFirstWord(title);
89+
final trimmedBase = baseUrl.trim();
90+
final isTrivial = trimmedBase.isEmpty ||
91+
trimmedBase == '/' ||
92+
// path-only or variable server (no scheme and no host component)
93+
(!trimmedBase.startsWith('http://') &&
94+
!trimmedBase.startsWith('https://') &&
95+
!trimmedBase.contains('://'));
96+
if (isTrivial) {
97+
final key = 'BASE_URL_$titleSlug';
98+
99+
final envs = readEnvs();
100+
String? activeId = readActiveEnvId();
101+
activeId ??= kGlobalEnvironmentId;
102+
final envModel = envs?[activeId];
103+
if (envModel != null) {
104+
final exists = envModel.values.any((v) => v.key == key);
105+
if (!exists) {
106+
final values = [...envModel.values];
107+
values.add(
108+
EnvironmentVariableModel(
109+
key: key,
110+
value: trimmedBase == '/' ? '' : trimmedBase,
111+
enabled: true,
112+
),
113+
);
114+
updateEnv(activeId, values: values);
115+
}
116+
}
117+
return key;
118+
}
119+
120+
String host = 'API';
121+
try {
122+
final u = Uri.parse(baseUrl);
123+
if (u.hasAuthority && u.host.isNotEmpty) host = u.host;
124+
} catch (_) {}
125+
126+
// If host could not be determined (remains 'API'), use title-based slug.
127+
final slug = (host == 'API')
128+
? titleSlug
129+
: host
130+
.replaceAll(RegExp(r'[^A-Za-z0-9]+'), '_')
131+
.replaceAll(RegExp(r'_+'), '_')
132+
.replaceAll(RegExp(r'^_|_$'), '')
133+
.toUpperCase();
134+
final key = 'BASE_URL_$slug';
135+
136+
final envs = readEnvs();
137+
String? activeId = readActiveEnvId();
138+
activeId ??= kGlobalEnvironmentId;
139+
final envModel = envs?[activeId];
140+
141+
if (envModel != null) {
142+
final exists = envModel.values.any((v) => v.key == key);
143+
if (!exists) {
144+
final values = [...envModel.values];
145+
values.add(EnvironmentVariableModel(
146+
key: key,
147+
value: baseUrl,
148+
enabled: true,
149+
));
150+
updateEnv(activeId, values: values);
151+
}
152+
}
153+
return key;
154+
}
155+
156+
/// Build a slug from the first word of an OpenAPI spec title.
157+
/// Example: "Pet Store API" -> "PET"; " My-Orders Service" -> "MY".
158+
/// Falls back to 'API' if no alphanumeric characters are present.
159+
String _slugFromOpenApiTitleFirstWord(String title) {
160+
final trimmed = title.trim();
161+
if (trimmed.isEmpty) return 'API';
162+
// Split on whitespace, take first non-empty token
163+
final firstToken = trimmed.split(RegExp(r'\s+')).firstWhere(
164+
(t) => t.trim().isNotEmpty,
165+
orElse: () => 'API',
166+
);
167+
final cleaned = firstToken
168+
.replaceAll(RegExp(r'[^A-Za-z0-9]+'), '_')
169+
.replaceAll(RegExp(r'_+'), '_')
170+
.replaceAll(RegExp(r'^_|_$'), '')
171+
.toUpperCase();
172+
return cleaned.isEmpty ? 'API' : cleaned;
173+
}
68174
}

lib/dashbot/features/chat/viewmodel/chat_viewmodel.dart

Lines changed: 49 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:convert';
2+
import 'package:openapi_spec/openapi_spec.dart';
23
import 'package:apidash/dashbot/features/chat/models/chat_message.dart';
34
import 'package:apidash_core/apidash_core.dart';
45
import 'package:flutter/foundation.dart';
@@ -38,9 +39,7 @@ class ChatViewmodel extends StateNotifier<ChatState> {
3839

3940
List<ChatMessage> get currentMessages {
4041
final id = _currentRequest?.id ?? 'global';
41-
debugPrint('[Chat] Getting messages for request ID: $id');
4242
final messages = state.chatSessions[id] ?? const [];
43-
debugPrint('[Chat] Found ${messages.length} messages');
4443
return messages;
4544
}
4645

@@ -49,8 +48,6 @@ class ChatViewmodel extends StateNotifier<ChatState> {
4948
ChatMessageType type = ChatMessageType.general,
5049
bool countAsUser = true,
5150
}) async {
52-
debugPrint(
53-
'[Chat] sendMessage start: type=$type, countAsUser=$countAsUser');
5451
final ai = _selectedAIModel;
5552
if (text.trim().isEmpty && countAsUser) return;
5653
if (ai == null &&
@@ -66,7 +63,6 @@ class ChatViewmodel extends StateNotifier<ChatState> {
6663

6764
final requestId = _currentRequest?.id ?? 'global';
6865
final existingMessages = state.chatSessions[requestId] ?? const [];
69-
debugPrint('[Chat] using requestId=$requestId');
7066

7167
if (countAsUser) {
7268
_addMessage(
@@ -211,8 +207,6 @@ class ChatViewmodel extends StateNotifier<ChatState> {
211207
userPrompt: userPrompt,
212208
stream: false,
213209
);
214-
debugPrint(
215-
'[Chat] prompts prepared: system=${systemPrompt.length} chars, user=${userPrompt.length} chars');
216210

217211
state = state.copyWith(isGenerating: true, currentStreamingResponse: '');
218212
try {
@@ -297,17 +291,20 @@ class ChatViewmodel extends StateNotifier<ChatState> {
297291

298292
Future<void> applyAutoFix(ChatAction action) async {
299293
try {
294+
if (action.actionType == ChatActionType.applyOpenApi) {
295+
await _applyOpenApi(action);
296+
return;
297+
}
298+
if (action.actionType == ChatActionType.applyCurl) {
299+
await _applyCurl(action);
300+
return;
301+
}
302+
300303
final msg = await _ref.read(autoFixServiceProvider).apply(action);
301304
if (msg != null && msg.isNotEmpty) {
302-
// Message type depends on action context; choose sensible defaults
303-
final t = (action.actionType == ChatActionType.applyCurl)
304-
? ChatMessageType.importCurl
305-
: (action.actionType == ChatActionType.applyOpenApi)
306-
? ChatMessageType.importOpenApi
307-
: ChatMessageType.general;
305+
final t = ChatMessageType.general;
308306
_appendSystem(msg, t);
309307
}
310-
// Only target-specific 'other' actions remain here
311308
if (action.actionType == ChatActionType.other) {
312309
await _applyOtherAction(action);
313310
}
@@ -344,7 +341,6 @@ class ChatViewmodel extends StateNotifier<ChatState> {
344341
}
345342

346343
Future<void> _applyOpenApi(ChatAction action) async {
347-
final requestId = _currentRequest?.id;
348344
final collection = _ref.read(collectionStateNotifierProvider.notifier);
349345
final payload = action.value is Map<String, dynamic>
350346
? (action.value as Map<String, dynamic>)
@@ -413,28 +409,26 @@ class ChatViewmodel extends StateNotifier<ChatState> {
413409
}
414410
}
415411

416-
final withEnvUrl = await _maybeSubstituteBaseUrl(url, baseUrl);
417-
if (action.field == 'apply_to_selected') {
418-
if (requestId == null) return;
419-
final replacingBody =
420-
(formFlag || formData.isNotEmpty) ? '' : (body ?? '');
421-
final replacingFormData =
422-
formData.isEmpty ? const <FormDataModel>[] : formData;
423-
collection.update(
424-
method: method,
425-
url: withEnvUrl,
426-
headers: headers,
427-
isHeaderEnabledList: List<bool>.filled(headers.length, true),
428-
body: replacingBody,
429-
bodyContentType: bodyContentType,
430-
formData: replacingFormData,
431-
params: const [],
432-
isParamEnabledList: const [],
433-
authModel: null,
434-
);
435-
_appendSystem('Applied OpenAPI operation to the selected request.',
436-
ChatMessageType.importOpenApi);
437-
} else if (action.field == 'apply_to_new') {
412+
413+
String sourceTitle = (payload['sourceName'] as String?) ?? '';
414+
if (sourceTitle.trim().isEmpty) {
415+
final specObj = payload['spec'];
416+
if (specObj is OpenApi) {
417+
try {
418+
final t = specObj.info.title.trim();
419+
if (t.isNotEmpty) sourceTitle = t;
420+
} catch (_) {}
421+
}
422+
}
423+
debugPrint('[OpenAPI] baseUrl="$baseUrl" title="$sourceTitle" url="$url"');
424+
final withEnvUrl = await _maybeSubstituteBaseUrlForOpenApi(
425+
url,
426+
baseUrl,
427+
sourceTitle,
428+
);
429+
debugPrint('[OpenAPI] withEnvUrl="$withEnvUrl');
430+
if (action.field == 'apply_to_new') {
431+
debugPrint('[OpenAPI] withEnvUrl="$withEnvUrl');
438432
final model = HttpRequestModel(
439433
method: method,
440434
url: withEnvUrl,
@@ -936,17 +930,13 @@ class ChatViewmodel extends StateNotifier<ChatState> {
936930

937931
// Helpers
938932
void _addMessage(String requestId, ChatMessage m) {
939-
debugPrint(
940-
'[Chat] Adding message to request ID: $requestId, actions: ${m.actions?.map((e) => e.toJson()).toList()}');
941933
final msgs = state.chatSessions[requestId] ?? const [];
942934
state = state.copyWith(
943935
chatSessions: {
944936
...state.chatSessions,
945937
requestId: [...msgs, m],
946938
},
947939
);
948-
debugPrint(
949-
'[Chat] Message added, total messages for $requestId: ${(state.chatSessions[requestId]?.length ?? 0)}');
950940
}
951941

952942
void _appendSystem(String text, ChatMessageType type) {
@@ -1004,6 +994,24 @@ class ChatViewmodel extends StateNotifier<ChatState> {
1004994
);
1005995
}
1006996

997+
Future<String> _maybeSubstituteBaseUrlForOpenApi(
998+
String url, String baseUrl, String title) async {
999+
final svc = _ref.read(urlEnvServiceProvider);
1000+
return svc.maybeSubstituteBaseUrl(
1001+
url,
1002+
baseUrl,
1003+
ensure: (b) => svc.ensureBaseUrlEnvForOpenApi(
1004+
b,
1005+
title: title,
1006+
readEnvs: () => _ref.read(environmentsStateNotifierProvider),
1007+
readActiveEnvId: () => _ref.read(activeEnvironmentIdStateProvider),
1008+
updateEnv: (id, {values}) => _ref
1009+
.read(environmentsStateNotifierProvider.notifier)
1010+
.updateEnvironment(id, values: values),
1011+
),
1012+
);
1013+
}
1014+
10071015
HttpRequestModel _getSubstitutedHttpRequestModel(
10081016
HttpRequestModel httpRequestModel) {
10091017
final envMap = _ref.read(availableEnvironmentVariablesStateProvider);

0 commit comments

Comments
 (0)