Skip to content

Commit 0a9ac70

Browse files
committed
checkpoint: TS agent templates
1 parent 338a698 commit 0a9ac70

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+947
-722
lines changed

cli/golem-cli/src/app/template/apply_plan.rs

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
// limitations under the License.
1414

1515
use crate::app::template::generator::InMemoryFs;
16-
use crate::edit::{golem_yaml, json};
16+
use crate::edit::{gitignore, golem_yaml, json, main_ts};
1717
use crate::fs;
1818
use crate::log::{log_action, log_skipping_up_to_date};
19+
use anyhow::Context;
1920
use std::collections::BTreeMap;
2021
use std::path::{Path, PathBuf};
22+
use tracing::warn;
2123

2224
#[derive(Debug, Clone, PartialEq, Eq)]
2325
pub enum TemplateApplyPlanEntry {
@@ -168,10 +170,24 @@ impl TemplateApplyPlanEntry {
168170

169171
fn try_merge(path: &Path, current: &str, new: &str) -> anyhow::Result<Option<String>> {
170172
let file_name = fs::file_name_to_str(path)?;
171-
Ok(match file_name {
172-
"golem.yaml" => Some(golem_yaml::merge_documents(current, new)?),
173-
"tsconfig.json" => Some(json::merge_object(current, new)?),
174-
"package.json" => Some(json::merge_object(current, new)?),
175-
_ => None,
176-
})
173+
174+
fn merge(file_name: &str, current: &str, new: &str) -> anyhow::Result<Option<String>> {
175+
Ok(match file_name {
176+
".gitignore" => Some(gitignore::merge(current, new)),
177+
"golem.yaml" => Some(golem_yaml::merge_documents(current, new)?),
178+
"main.ts" => Some(main_ts::merge_reexports(current, new)?),
179+
"package.json" => Some(json::merge_object(current, new)?),
180+
"tsconfig.json" => Some(json::merge_object(current, new)?),
181+
_ => None,
182+
})
183+
}
184+
185+
merge(file_name, current, new)
186+
.map_err(|err| {
187+
warn!("merge: file name: {}", file_name);
188+
warn!("merge: current:\n{}\n", current);
189+
warn!("merge: new:\n{}\n", new);
190+
err
191+
})
192+
.with_context(|| format!("Failed to merge '{}'", file_name))
177193
}

cli/golem-cli/src/app/template/generator.rs

Lines changed: 46 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -21,36 +21,17 @@ use golem_common::base_model::application::ApplicationName;
2121
use golem_common::base_model::component::ComponentName;
2222
use heck::{ToKebabCase, ToSnakeCase};
2323
use include_dir::{Dir, DirEntry};
24-
use itertools::Itertools;
25-
use std::borrow::Cow;
26-
use std::collections::{BTreeMap, BTreeSet};
24+
use std::collections::BTreeMap;
2725
use std::path::{Path, PathBuf};
2826

2927
const ON_DEMAND_COMMON_HASH_FILE_NAME: &str = ".golem-template-content-hash";
3028

31-
#[derive(Debug, Copy, Clone)]
32-
enum TargetExistsResolveMode {
33-
#[allow(dead_code)]
34-
Skip,
35-
MergeOrSkip,
36-
Fail,
37-
MergeOrFail,
38-
}
39-
40-
type MergeContents = Box<dyn FnOnce(&[u8]) -> anyhow::Result<Vec<u8>>>;
41-
42-
enum TargetExistsResolveDecision {
43-
Skip,
44-
Merge(MergeContents),
45-
}
46-
4729
pub trait TemplateGeneratorTargetFs {
4830
type Output;
4931

50-
fn ensure_dir(&self, path: &Path) -> anyhow::Result<()>;
32+
fn exists(&self, path: &Path) -> bool;
5133
fn write_file(&mut self, path: &Path, contents: String) -> anyhow::Result<()>;
5234
fn finish(self) -> Self::Output;
53-
fn is_in_memory(&self) -> bool;
5435
}
5536

5637
#[derive(Debug, Default)]
@@ -59,19 +40,15 @@ pub struct StdFs;
5940
impl TemplateGeneratorTargetFs for StdFs {
6041
type Output = ();
6142

62-
fn ensure_dir(&self, path: &Path) -> anyhow::Result<()> {
63-
fs::create_dir_all(path).map(|_| ())
43+
fn exists(&self, path: &Path) -> bool {
44+
path.exists()
6445
}
6546

6647
fn write_file(&mut self, path: &Path, contents: String) -> anyhow::Result<()> {
6748
fs::write_str(path, contents)
6849
}
6950

7051
fn finish(self) -> Self::Output {}
71-
72-
fn is_in_memory(&self) -> bool {
73-
false
74-
}
7552
}
7653

7754
#[derive(Debug, Default)]
@@ -96,8 +73,8 @@ impl InMemoryFs {
9673
impl TemplateGeneratorTargetFs for InMemoryFs {
9774
type Output = InMemoryFs;
9875

99-
fn ensure_dir(&self, _path: &Path) -> anyhow::Result<()> {
100-
Ok(())
76+
fn exists(&self, path: &Path) -> bool {
77+
self.files.contains_key(path)
10178
}
10279

10380
fn write_file(&mut self, path: &Path, contents: String) -> anyhow::Result<()> {
@@ -108,10 +85,6 @@ impl TemplateGeneratorTargetFs for InMemoryFs {
10885
fn finish(self) -> Self::Output {
10986
self
11087
}
111-
112-
fn is_in_memory(&self) -> bool {
113-
true
114-
}
11588
}
11689

11790
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -130,7 +103,6 @@ struct GeneratorContext<'a> {
130103
component_name: Option<&'a ComponentName>,
131104
target_path: &'a Path,
132105
sdk_overrides: &'a SdkOverrides,
133-
resolve_mode: TargetExistsResolveMode,
134106
}
135107

136108
pub fn generate_commons_by_template<T: TemplateGeneratorTargetFs>(
@@ -152,7 +124,6 @@ pub fn generate_commons_by_template<T: TemplateGeneratorTargetFs>(
152124
component_name: None,
153125
target_path,
154126
sdk_overrides,
155-
resolve_mode: TargetExistsResolveMode::MergeOrSkip,
156127
},
157128
)?;
158129
Ok(target.finish())
@@ -191,7 +162,6 @@ pub fn generate_on_demand_commons_by_template<T: TemplateGeneratorTargetFs>(
191162
component_name: None,
192163
target_path,
193164
sdk_overrides,
194-
resolve_mode: TargetExistsResolveMode::Fail,
195165
},
196166
)?;
197167

@@ -223,7 +193,31 @@ pub fn generate_component_by_template<T: TemplateGeneratorTargetFs>(
223193
component_name: Some(component_name),
224194
target_path,
225195
sdk_overrides,
226-
resolve_mode: TargetExistsResolveMode::MergeOrFail,
196+
},
197+
)?;
198+
Ok(target.finish())
199+
}
200+
201+
pub fn generate_agent_by_template<T: TemplateGeneratorTargetFs>(
202+
template: &AppTemplate,
203+
target_path: &Path,
204+
application_name: &ApplicationName,
205+
component_name: &ComponentName,
206+
sdk_overrides: &SdkOverrides,
207+
mut target: T,
208+
) -> anyhow::Result<T::Output> {
209+
if !template.metadata.is_agent() {
210+
bail!("Template {} is not an agent template", template.name);
211+
}
212+
213+
generate_root_directory(
214+
&mut target,
215+
&GeneratorContext {
216+
template,
217+
application_name: Some(application_name),
218+
component_name: Some(component_name),
219+
target_path,
220+
sdk_overrides,
227221
},
228222
)?;
229223
Ok(target.finish())
@@ -249,7 +243,6 @@ fn generate_directory<T: TemplateGeneratorTargetFs>(
249243
source: &Path,
250244
target_path: &Path,
251245
) -> anyhow::Result<()> {
252-
target.ensure_dir(target_path)?;
253246
for entry in templates_dir
254247
.get_dir(source)
255248
.unwrap_or_else(|| panic!("Could not find entry {source:?}"))
@@ -311,29 +304,18 @@ fn instantiate_file<T: TemplateGeneratorTargetFs>(
311304
target_path: &Path,
312305
content_transforms: Vec<Transform>,
313306
) -> anyhow::Result<()> {
314-
match get_resolved_contents(target, ctx, dir, source, target_path)? {
315-
Some(contents) => {
316-
let contents = std::str::from_utf8(contents.as_ref()).map_err(|err| {
317-
anyhow!(
318-
"Failed to decode as utf8, source: {}, err: {}",
319-
source.display(),
320-
err
321-
)
322-
})?;
323-
324-
let rendered = if content_transforms.is_empty() {
325-
contents.to_string()
326-
} else {
327-
transform(ctx, contents, &content_transforms)
328-
};
329-
330-
target.write_file(target_path, rendered)
331-
}
332-
None => Err(anyhow!(
333-
"Failed to resolve template contents for {}",
334-
source.display()
335-
)),
307+
if target.exists(target_path) {
308+
bail!("Target {} already exists", target_path.display());
336309
}
310+
311+
let contents = get_contents(dir, source)?;
312+
let rendered = if content_transforms.is_empty() {
313+
contents.to_string()
314+
} else {
315+
transform(ctx, contents, &content_transforms)
316+
};
317+
318+
target.write_file(target_path, rendered)
337319
}
338320

339321
fn transform(ctx: &GeneratorContext<'_>, str: impl AsRef<str>, transforms: &[Transform]) -> String {
@@ -408,100 +390,9 @@ fn transform_file_name(ctx: &GeneratorContext<'_>, file_name: impl AsRef<str>) -
408390
transform(ctx, file_name, &[Transform::ComponentName]).replace("Cargo.toml._", "Cargo.toml")
409391
}
410392

411-
fn check_target<T: TemplateGeneratorTargetFs>(
412-
target_kind: &T,
413-
target: &Path,
414-
resolve_mode: TargetExistsResolveMode,
415-
) -> anyhow::Result<Option<TargetExistsResolveDecision>> {
416-
if target_kind.is_in_memory() {
417-
return Ok(None);
418-
}
419-
420-
if !target.exists() {
421-
return Ok(None);
422-
}
423-
424-
let get_merge = || -> anyhow::Result<Option<TargetExistsResolveDecision>> {
425-
let file_name = target
426-
.file_name()
427-
.ok_or_else(|| anyhow!("Failed to get file name for target: {}", target.display()))
428-
.and_then(|file_name| {
429-
file_name.to_str().ok_or_else(|| {
430-
anyhow!(
431-
"Failed to convert file name to string: {}",
432-
file_name.to_string_lossy()
433-
)
434-
})
435-
})?;
436-
437-
match file_name {
438-
".gitignore" => {
439-
let target = target.to_path_buf();
440-
let current_content = fs::read_to_string(&target)?;
441-
Ok(Some(TargetExistsResolveDecision::Merge(Box::new(
442-
move |new_content: &[u8]| -> anyhow::Result<Vec<u8>> {
443-
Ok(current_content
444-
.lines()
445-
.chain(
446-
std::str::from_utf8(new_content).map_err(|err| {
447-
anyhow!(
448-
"Failed to decode new content for merge as utf8, target: {}, err: {}",
449-
target.display(),
450-
err
451-
)
452-
})?.lines(),
453-
)
454-
.collect::<BTreeSet<&str>>()
455-
.iter()
456-
.join("\n")
457-
.into_bytes())
458-
},
459-
))))
460-
}
461-
_ => Ok(None),
462-
}
463-
};
464-
465-
let target_already_exists = || {
466-
Err(anyhow!(format!(
467-
"Target ({}) already exists!",
468-
target.display()
469-
)))
470-
};
471-
472-
match resolve_mode {
473-
TargetExistsResolveMode::Skip => Ok(Some(TargetExistsResolveDecision::Skip)),
474-
TargetExistsResolveMode::MergeOrSkip => match get_merge()? {
475-
Some(merge) => Ok(Some(merge)),
476-
None => Ok(Some(TargetExistsResolveDecision::Skip)),
477-
},
478-
TargetExistsResolveMode::Fail => target_already_exists(),
479-
TargetExistsResolveMode::MergeOrFail => match get_merge()? {
480-
Some(merge) => Ok(Some(merge)),
481-
None => target_already_exists(),
482-
},
483-
}
484-
}
485-
486-
fn get_contents<'a>(dir: &Dir<'a>, source: &'a Path) -> anyhow::Result<&'a [u8]> {
487-
Ok(dir
488-
.get_file(source)
393+
fn get_contents<'a>(dir: &Dir<'a>, source: &'a Path) -> anyhow::Result<&'a str> {
394+
dir.get_file(source)
489395
.ok_or_else(|| anyhow!("Could not find entry {}", source.display()))?
490-
.contents())
491-
}
492-
493-
fn get_resolved_contents<'a, T: TemplateGeneratorTargetFs>(
494-
target_kind: &T,
495-
ctx: &GeneratorContext<'a>,
496-
dir: &Dir<'a>,
497-
source: &'a Path,
498-
target: &'a Path,
499-
) -> anyhow::Result<Option<Cow<'a, [u8]>>> {
500-
match check_target(target_kind, target, ctx.resolve_mode)? {
501-
None => Ok(Some(Cow::Borrowed(get_contents(dir, source)?))),
502-
Some(TargetExistsResolveDecision::Skip) => Ok(None),
503-
Some(TargetExistsResolveDecision::Merge(merge)) => {
504-
Ok(Some(Cow::Owned(merge(get_contents(dir, source)?)?)))
505-
}
506-
}
396+
.contents_utf8()
397+
.ok_or_else(|| anyhow!("File contents are not valid UTF-8: {}", source.display()))
507398
}

cli/golem-cli/src/app/template/metadata.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414

1515
use crate::app::template::repo::TEMPLATES_DIR;
1616

17-
use anyhow::{anyhow, Context};
18-
use clap::builder::TypedValueParser;
17+
use anyhow::anyhow;
1918
use serde_derive::{Deserialize, Serialize};
2019
use std::path::Path;
2120

21+
// TODO: FCL: drop or support exclude
22+
2223
#[derive(Debug, Clone, Serialize, Deserialize)]
2324
#[serde(tag = "type", rename_all = "kebab-case", deny_unknown_fields)]
2425
pub enum AppTemplateMetadata {
@@ -40,6 +41,12 @@ pub enum AppTemplateMetadata {
4041
exclude: Option<Vec<String>>,
4142
dev_only: Option<bool>,
4243
},
44+
#[serde(rename_all = "camelCase")]
45+
Agent {
46+
description: String,
47+
exclude: Option<Vec<String>>,
48+
dev_only: Option<bool>,
49+
},
4350
}
4451

4552
impl AppTemplateMetadata {
@@ -68,4 +75,8 @@ impl AppTemplateMetadata {
6875
pub fn is_component(&self) -> bool {
6976
matches!(self, AppTemplateMetadata::Component { .. })
7077
}
78+
79+
pub fn is_agent(&self) -> bool {
80+
matches!(self, AppTemplateMetadata::Agent { .. })
81+
}
7182
}

cli/golem-cli/src/app/template/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub use apply_plan::{TemplateApplyPlan, TemplateApplyPlanEntry};
2424
pub use metadata::AppTemplateMetadata;
2525
pub use repo::AppTemplateRepo;
2626
pub use template::{
27-
AppTemplate, AppTemplateCommon, AppTemplateCommonOnDemand, AppTemplateComponent,
27+
AppTemplate, AppTemplateAgent, AppTemplateCommon, AppTemplateCommonOnDemand,
28+
AppTemplateComponent,
2829
AppTemplateName, AppTemplatesForLanguage,
2930
};

0 commit comments

Comments
 (0)