From a54f54ef20da6433962d2960b31eb358834826b3 Mon Sep 17 00:00:00 2001 From: toki Date: Sat, 4 Apr 2026 17:18:17 +0900 Subject: [PATCH] feat: add tag_system.dart and update command.dart & to-do --- lib/oto/commands/command.dart | 188 ++------------------------- lib/oto/core/tag_system.dart | 236 ++++++++++++++++++++++++++++++++++ refactoring/to-do.md | 23 ++-- 3 files changed, 258 insertions(+), 189 deletions(-) create mode 100644 lib/oto/core/tag_system.dart diff --git a/lib/oto/commands/command.dart b/lib/oto/commands/command.dart index c189f14..315a305 100644 --- a/lib/oto/commands/command.dart +++ b/lib/oto/commands/command.dart @@ -3,9 +3,9 @@ import 'dart:async'; import 'dart:io'; import 'package:dart_framework/platform/process.dart'; -import 'package:dart_framework/utils/path.dart'; import 'package:dart_framework/utils/system_util.dart'; import 'package:oto_cli/oto/application.dart'; +import 'package:oto_cli/oto/core/tag_system.dart'; import 'package:oto_cli/oto/data/command_data.dart'; import 'package:oto_cli/oto/data/jenkins_data.dart'; import 'package:oto_cli/oto/pipeline/pipeline_exe_handle.dart'; @@ -108,188 +108,28 @@ abstract class Command { Command(); late String id; - static RegExp regEx = RegExp(r'()'); - static RegExp regSetEx = RegExp(r'(<@)([_a-zA-Z0-9.-]+)(>)'); + static RegExp get regEx => TagSystem.regEx; + static RegExp get regSetEx => TagSystem.regSetEx; static Map replaceAllTagsMap(Map map, - {bool replace = false}) { - var newMap = {}; - var deleteKeys = []; - for (var pair in map.entries) { - var key = pair.key; - var keyReplacedTag = Command.replaceTagValue(key); - if (keyReplacedTag != key) { - deleteKeys.add(key); - } - if (map[key] is String) { - newMap[keyReplacedTag] = Command.replaceTagValue(map[key]); - } else if (map[key] is List) { - newMap[keyReplacedTag] = replaceAllTagsList(map[key], replace: replace); - } else if (map[key] is Map) { - newMap[keyReplacedTag] = replaceAllTagsMap(map[key], replace: replace); - } else { - newMap[keyReplacedTag] = map[key]; - } - if (replace) { - map[keyReplacedTag] = newMap[keyReplacedTag]; - } - } - if (replace) { - for (var key in deleteKeys) { - map.remove(key); - } - return map; - } - return newMap; - } + {bool replace = false}) => + TagSystem.replaceAllTagsMap(map, replace: replace); - static List replaceAllTagsList(List list, {bool replace = false}) { - var newList = []; - for (var i = 0; i < list.length; ++i) { - if (list[i] is String) { - newList.add(Command.replaceTagValue(list[i])); - } else if (list[i] is List) { - newList.add(replaceAllTagsList(list[i])); - } else if (list[i] is Map) { - newList.add(replaceAllTagsMap(list[i])); - } else { - newList.add(list[i]); - } - if (replace) { - list[i] = newList[i]; - } - } - if (replace) { - return newList; - } - return newList; - } + static List replaceAllTagsList(List list, {bool replace = false}) => + TagSystem.replaceAllTagsList(list, replace: replace); static Future setPropertyValue(String tag, dynamic value, - {bool? showPrint}) async { - var match = regSetEx.firstMatch(tag); - if (match == null) { - throw Exception('Please put the correct value. (ex: <@property.value>)'); - } - tag = match.group(2)!; - if (tag.startsWith('property')) { - var key = tag.replaceFirst('property.', ''); - Application.instance.property[key] = value; - // await CLI.println('[Return] $key : $value', color: Color.yellowStrong); - if (showPrint ?? true) { - if (value is List) { - Application.log('[Return] $key :\n'); - var index = 0; - var length = value.length.toString().length; - for (var item in value) { - var indexStr = index.toString().padLeft(length); - Application.log('$indexStr - $item'); - index++; - } - } else { - Application.log('[Return] $key : $value'); - } - } - } - return simpleFuture; - } + {bool? showPrint}) => + TagSystem.setPropertyValue(tag, value, showPrint: showPrint); - static dynamic _getTagValue(String value) { - var setted = false; - dynamic data; - if (value.startsWith('property.')) { - var map = Application.instance.property; - var param = value.replaceFirst('property.', ''); - if (map.containsKey(param)) { - data = map[param]; - setted = true; - } else { - var params = param.split('.'); - if (params.length > 1) { - var subMap = map; - for (var index = 0; index < params.length; index++) { - if (subMap.containsKey(params[index])) { - var value = subMap[params[index]]; - if (value is Map) { - subMap = value; - } else { - data = value; - setted = true; - } - } - } - } - } + static String getAbs(String path) => TagSystem.getAbs(path); - if (!setted) { - Application.log('"$value" is not exist in property'); - data = null; - setted = true; - } - } + static String getPathNormal(String path) => TagSystem.getPathNormal(path); - if (value.startsWith('state.')) { - var map = Application.instance.commandStates; - var param = value.replaceFirst('state.', ''); - if (map.containsKey(param)) { - var value = map[param].toString().replaceAll('CommandState.', ''); - data = value == '' ? null : value; - } else { - data = null; - } - setted = true; - } + static dynamic replaceTagValue(String raw) => TagSystem.replaceTagValue(raw); - if (!setted) { - throw Exception( - 'Command.getTagValue: "$value" is not defined in property.'); - } - return data; - } - - static String getAbs(String path) { - path = getPathNormal(path); - if (path == '.' || path.startsWith('./') || path.startsWith('.\\')) { - path = path.replaceFirst('.', getPathNormal(Application.current)); - } - if (path == '..' || path.startsWith('../') || path.startsWith('..\\')) { - path = - path.replaceFirst('..', '${getPathNormal(Application.current)}/..'); - } - return File(path).absolute.path; - } - - static String getPathNormal(String path) { - return replaceTagValue(Path.getNormal(path)); - } - - static dynamic replaceTagValue(String raw) { - var list = regEx.allMatches(raw); - if (list.length == 1) { - var item = list.first; - var sub = list.first.input.substring(item.start, item.end); - if (sub == raw) { - return replaceSingleTagValue(raw); - } - } - - for (var match in list) { - var param = match.group(2); - var target = ''; - raw = raw.replaceAll(target, _getTagValue(param).toString()); - } - return raw; - } - - static dynamic replaceSingleTagValue(String raw) { - dynamic value; - var list = regEx.allMatches(raw); - for (var match in list) { - var param = match.group(2); - value = _getTagValue(param!); - } - return value; - } + static dynamic replaceSingleTagValue(String raw) => + TagSystem.replaceSingleTagValue(raw); late ExeHandleData? exeHandle = null; diff --git a/lib/oto/core/tag_system.dart b/lib/oto/core/tag_system.dart new file mode 100644 index 0000000..2ba6728 --- /dev/null +++ b/lib/oto/core/tag_system.dart @@ -0,0 +1,236 @@ +// ignore_for_file: avoid_init_to_null + +import 'dart:io'; +import 'package:dart_framework/utils/path.dart'; +import 'package:oto_cli/oto/application.dart'; + +/// OTO 태그 시스템 +/// +/// YAML 파이프라인에서 동적 값을 참조하기 위한 태그 문법을 처리한다. +/// +/// ## 읽기 태그 (값 치환) +/// +/// ```yaml +/// exe: BuildiOS +/// param: +/// workspace: # property 직접 접근 +/// version: # property 중첩 접근 +/// state: # commandStates 접근 (ready/progress/complete) +/// ``` +/// +/// 태그가 문자열 전체인 경우 원본 타입 그대로 반환 (String→Any). +/// 문자열 일부인 경우 toString()으로 삽입. +/// +/// ## 쓰기 태그 (값 저장) +/// +/// ```yaml +/// exe: SetValue +/// param: +/// set-version: <@property.appVersion> # property에 값 저장 +/// ``` +/// +/// `setPropertyValue()` 또는 `setProperty()` 호출 시 사용. +class TagSystem { + /// 읽기 태그 패턴: `` + static RegExp regEx = RegExp(r'()'); + + /// 쓰기 태그 패턴: `<@namespace.key>` + static RegExp regSetEx = RegExp(r'(<@)([_a-zA-Z0-9.-]+)(>)'); + + /// Map의 모든 키/값에서 태그를 재귀적으로 치환한다. + /// + /// [replace]가 true이면 원본 [map]을 직접 수정하고 반환한다. + static Map replaceAllTagsMap(Map map, + {bool replace = false}) { + var newMap = {}; + var deleteKeys = []; + for (var pair in map.entries) { + var key = pair.key; + var keyReplacedTag = TagSystem.replaceTagValue(key); + if (keyReplacedTag != key) { + deleteKeys.add(key); + } + if (map[key] is String) { + newMap[keyReplacedTag] = TagSystem.replaceTagValue(map[key]); + } else if (map[key] is List) { + newMap[keyReplacedTag] = replaceAllTagsList(map[key], replace: replace); + } else if (map[key] is Map) { + newMap[keyReplacedTag] = + replaceAllTagsMap(map[key], replace: replace); + } else { + newMap[keyReplacedTag] = map[key]; + } + if (replace) { + map[keyReplacedTag] = newMap[keyReplacedTag]; + } + } + if (replace) { + for (var key in deleteKeys) { + map.remove(key); + } + return map; + } + return newMap; + } + + /// List의 모든 요소에서 태그를 재귀적으로 치환한다. + static List replaceAllTagsList(List list, {bool replace = false}) { + var newList = []; + for (var i = 0; i < list.length; ++i) { + if (list[i] is String) { + newList.add(TagSystem.replaceTagValue(list[i])); + } else if (list[i] is List) { + newList.add(replaceAllTagsList(list[i])); + } else if (list[i] is Map) { + newList.add(replaceAllTagsMap(list[i])); + } else { + newList.add(list[i]); + } + if (replace) { + list[i] = newList[i]; + } + } + if (replace) { + return newList; + } + return newList; + } + + /// 쓰기 태그(`<@property.key>`)로 지정된 property에 값을 저장한다. + static Future setPropertyValue(String tag, dynamic value, + {bool? showPrint}) async { + var match = regSetEx.firstMatch(tag); + if (match == null) { + throw Exception('Please put the correct value. (ex: <@property.value>)'); + } + tag = match.group(2)!; + if (tag.startsWith('property')) { + var key = tag.replaceFirst('property.', ''); + Application.instance.property[key] = value; + if (showPrint ?? true) { + if (value is List) { + Application.log('[Return] $key :\n'); + var index = 0; + var length = value.length.toString().length; + for (var item in value) { + var indexStr = index.toString().padLeft(length); + Application.log('$indexStr - $item'); + index++; + } + } else { + Application.log('[Return] $key : $value'); + } + } + } + return Future.value(); + } + + /// 태그 이름으로 실제 값을 조회한다. + /// + /// - `property.key` → `Application.instance.property[key]` + /// - `property.nested.key` → 중첩 Map 순회 + /// - `state.commandId` → `Application.instance.commandStates[commandId]` + static dynamic _getTagValue(String value) { + var setted = false; + dynamic data; + if (value.startsWith('property.')) { + var map = Application.instance.property; + var param = value.replaceFirst('property.', ''); + if (map.containsKey(param)) { + data = map[param]; + setted = true; + } else { + var params = param.split('.'); + if (params.length > 1) { + var subMap = map; + for (var index = 0; index < params.length; index++) { + if (subMap.containsKey(params[index])) { + var value = subMap[params[index]]; + if (value is Map) { + subMap = value; + } else { + data = value; + setted = true; + } + } + } + } + } + + if (!setted) { + Application.log('"$value" is not exist in property'); + data = null; + setted = true; + } + } + + if (value.startsWith('state.')) { + var map = Application.instance.commandStates; + var param = value.replaceFirst('state.', ''); + if (map.containsKey(param)) { + var value = map[param].toString().replaceAll('CommandState.', ''); + data = value == '' ? null : value; + } else { + data = null; + } + setted = true; + } + + if (!setted) { + throw Exception( + 'TagSystem.getTagValue: "$value" is not defined in property.'); + } + return data; + } + + /// 상대경로(`.`, `..`)를 절대경로로 변환한다. 경로 내 태그도 치환된다. + static String getAbs(String path) { + path = getPathNormal(path); + if (path == '.' || path.startsWith('./') || path.startsWith('.\\')) { + path = path.replaceFirst('.', getPathNormal(Application.current)); + } + if (path == '..' || path.startsWith('../') || path.startsWith('..\\')) { + path = + path.replaceFirst('..', '${getPathNormal(Application.current)}/..'); + } + return File(path).absolute.path; + } + + /// 경로 구분자를 정규화하고 태그를 치환한다. + static String getPathNormal(String path) { + return replaceTagValue(Path.getNormal(path)); + } + + /// 문자열에서 태그(``)를 치환한다. + /// + /// 태그가 문자열 전체인 경우 원본 타입(dynamic)을 반환한다. + /// 태그가 문자열 일부인 경우 문자열로 삽입한다. + static dynamic replaceTagValue(String raw) { + var list = regEx.allMatches(raw); + if (list.length == 1) { + var item = list.first; + var sub = list.first.input.substring(item.start, item.end); + if (sub == raw) { + return replaceSingleTagValue(raw); + } + } + + for (var match in list) { + var param = match.group(2); + var target = ''; + raw = raw.replaceAll(target, _getTagValue(param).toString()); + } + return raw; + } + + /// 태그 하나만 포함된 문자열에서 값을 원본 타입으로 반환한다. + static dynamic replaceSingleTagValue(String raw) { + dynamic value; + var list = regEx.allMatches(raw); + for (var match in list) { + var param = match.group(2); + value = _getTagValue(param!); + } + return value; + } +} diff --git a/refactoring/to-do.md b/refactoring/to-do.md index acfff0d..985cf31 100644 --- a/refactoring/to-do.md +++ b/refactoring/to-do.md @@ -35,25 +35,18 @@ AI(Claude 등)가 코드베이스를 더 잘 이해하고 수정할 수 있도 2. `command.dart` enum에 1줄 추가 → `build_runner` 재실행 3. `command_registry.dart`에 import + 함수 호출 추가 (신규 파일인 경우) +### 4. 태그 시스템 중앙화 +- **작업**: 태그 파싱/치환 로직을 `command.dart`에서 분리 +- **결과**: + - `lib/oto/core/tag_system.dart` 신규 생성 — `TagSystem` 클래스에 전체 구현 이동 + - 태그 문법 레퍼런스 주석 포함 (``, ``, `<@property.key>`) + - `command.dart`의 태그 메서드는 `TagSystem`으로의 얇은 위임(delegate)만 유지 + - 기존 `Command.replaceTagValue()` 등 외부 호출부는 수정 없이 그대로 동작 + --- ## 📋 할 일 -### 4. 태그 시스템 중앙화 -**우선순위: 중간 / 난이도: 낮음** - -현재 태그 처리 로직(`` 등)이 `commands/command.dart` 내부에 -흩어져 있다. 별도 파일로 분리하면 AI가 "어떤 태그를 쓸 수 있나?" 질문에 -코드 한 곳만 읽어도 답할 수 있다. - -작업 내용: -- `lib/oto/core/tag_system.dart` 파일 생성 -- 태그 파싱/치환 로직을 `command.dart`에서 이동 -- 태그 종류 명세를 주석으로 정리 (`property`, `state`, `common` 등) -- `command.dart`는 `tag_system.dart`를 import해서 사용 - ---- - ### 5. Pipeline Executor에 YAML 예제 주석 추가 **우선순위: 중간 / 난이도: 낮음**