diff --git a/cli/cli.go b/cli/cli.go index 75b7e83..c3dcc7f 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -21,11 +21,14 @@ package cli import ( _type "embed-code/embed-code-go/type" "flag" + "fmt" + "log/slog" "os" "strings" "embed-code/embed-code-go/configuration" "embed-code/embed-code-go/embedding" + "embed-code/embed-code-go/logging" "gopkg.in/yaml.v3" ) @@ -152,6 +155,10 @@ func ReadArgs() Config { // Returns filled Config. func FillArgsFromConfigFile(args Config) (Config, error) { configFields := readConfigFields(args.ConfigPath) + slog.Info(fmt.Sprintf( + "Loaded config file `%s`. Found %d embedding setup(s).", + logging.FileReference(args.ConfigPath), len(configFields.Embeddings), + )) args.BaseDocsPath = configFields.BaseDocsPath args.BaseCodePaths = configFields.BaseCodePaths @@ -181,6 +188,11 @@ func BuildEmbedCodeConfiguration(userArgs Config) []configuration.Configuration if len(userArgs.Embeddings) > 0 { for _, embedding := range userArgs.Embeddings { + slog.Info(fmt.Sprintf( + "Processing the `%s` embedding setup. Documentation folder: `%s`. %s.", + embedding.Name, logging.FileReference(embedding.DocsPath), + sourceFoldersLabel(embedding.CodePaths), + )) embedCodeConfigs = append(embedCodeConfigs, configFromEmbedding(embedding)) } return embedCodeConfigs @@ -189,13 +201,17 @@ func BuildEmbedCodeConfiguration(userArgs Config) []configuration.Configuration embedCodeConfig := configWithOptionalParams(userArgs) embedCodeConfig.CodeRoots = userArgs.BaseCodePaths embedCodeConfig.DocumentationRoot = userArgs.BaseDocsPath + slog.Info(fmt.Sprintf( + "Preparing command line settings. Documentation folder: `%s`. %s.", + logging.FileReference(userArgs.BaseDocsPath), sourceFoldersLabel(userArgs.BaseCodePaths), + )) embedCodeConfigs = append(embedCodeConfigs, embedCodeConfig) return embedCodeConfigs } -// Creates a new Configuration from one complete embedding config. +// configFromEmbedding creates a new Configuration from one complete embedding config. func configFromEmbedding(embedding EmbeddingConfig) configuration.Configuration { embedCodeConfig := configuration.NewConfiguration() embedCodeConfig.Name = embedding.Name @@ -215,13 +231,16 @@ func configFromEmbedding(embedding EmbeddingConfig) configuration.Configuration return embedCodeConfig } -// Creates a new Configuration with the filled optional properties from the user args. +// configWithOptionalParams creates a new Configuration with optional properties from user args. func configWithOptionalParams(userArgs Config) configuration.Configuration { embedCodeConfig := configuration.NewConfiguration() if len(userArgs.DocIncludes) > 0 { embedCodeConfig.DocIncludes = userArgs.DocIncludes } + if len(userArgs.DocExcludes) > 0 { + embedCodeConfig.DocExcludes = userArgs.DocExcludes + } if isNotEmpty(userArgs.Separator) { embedCodeConfig.Separator = userArgs.Separator } @@ -229,7 +248,29 @@ func configWithOptionalParams(userArgs Config) configuration.Configuration { return embedCodeConfig } -// Returns a list of strings from given comma-separated string listArgument. +// sourceFoldersLabel formats source folders for human-readable log messages. +func sourceFoldersLabel(paths _type.NamedPathList) string { + if len(paths) == 0 { + return "No source code folders configured" + } + + var labels []string + for _, path := range paths { + label := fmt.Sprintf("`%s`", logging.FileReference(path.Path)) + if strings.TrimSpace(path.Name) != "" { + label = fmt.Sprintf("`%s` as `%s`", logging.FileReference(path.Path), path.Name) + } + labels = append(labels, label) + } + + if len(labels) == 1 { + return "Source code folder: " + labels[0] + } + + return "Source code folders: " + strings.Join(labels, ", ") +} + +// parseListArgument returns a list of strings from given comma-separated string listArgument. func parseListArgument(listArgument string) []string { splitArgs := strings.Split(listArgument, ",") parsedArgs := make([]string, 0) @@ -242,7 +283,7 @@ func parseListArgument(listArgument string) []string { return parsedArgs } -// Reads the file from provided configFilePath and returns a ConfigFields struct. +// readConfigFields reads the provided config file and returns parsed fields. // // configFilePath — a path to a yaml configuration file. // diff --git a/cli/cli_test.go b/cli/cli_test.go index 4a9de17..2ec99bc 100644 --- a/cli/cli_test.go +++ b/cli/cli_test.go @@ -233,6 +233,16 @@ var _ = Describe("CLI validation", func() { Expect(embedConfigs[2].Separator).To(Equal("---")) }) + It("should copy command line doc excludes to the runtime config", func() { + config := baseCliConfig() + config.DocExcludes = []string{"old-docs/**/*.md", "drafts/**/*"} + + embedConfigs := cli.BuildEmbedCodeConfiguration(config) + + Expect(embedConfigs).To(HaveLen(1)) + Expect(embedConfigs[0].DocExcludes).To(Equal([]string(config.DocExcludes))) + }) + }) }) diff --git a/embedding/commentfilter/filter.go b/embedding/commentfilter/filter.go index 0356917..0387be3 100644 --- a/embedding/commentfilter/filter.go +++ b/embedding/commentfilter/filter.go @@ -19,6 +19,7 @@ package commentfilter import ( + "embed-code/embed-code-go/logging" "fmt" "log/slog" "path/filepath" @@ -112,7 +113,7 @@ func warnUnsupportedFileType( "`comments=\"%s\"` was requested in `%s` for `%s`, "+ "but comment filtering is not supported for this file extension.", mode, - fileURL(embeddingDocPath, embeddingLine), + logging.FileReferenceWithLine(embeddingDocPath, embeddingLine), filePath, ), ) @@ -139,7 +140,7 @@ func warnUnsupportedCommentsMode( "`comments=\"%s\"` was requested in `%s` for `%s`, but this mode does not have "+ "a distinct meaning for this file type. Supported modes are: %s.", mode, - fileURL(embeddingDocPath, embeddingLine), + logging.FileReferenceWithLine(embeddingDocPath, embeddingLine), filePath, strings.Join(wrappedModes, ", "), ), @@ -148,21 +149,6 @@ func warnUnsupportedCommentsMode( return true } -// fileURL returns an absolute file URL for a local path and line. -func fileURL(path string, line int) string { - absolutePath, err := filepath.Abs(path) - if err != nil { - return "file://" + path - } - - url := "file://" + absolutePath - if line > 0 { - url = fmt.Sprintf("%s:%d", url, line) - } - - return url -} - // containsMode reports whether the list includes the given mode. func containsMode(modes []Mode, mode Mode) bool { for _, supportedMode := range modes { diff --git a/embedding/embedding_test.go b/embedding/embedding_test.go index 3ae82c4..f2403b6 100644 --- a/embedding/embedding_test.go +++ b/embedding/embedding_test.go @@ -330,7 +330,12 @@ var _ = Describe("Embedding", func() { docPath := fmt.Sprintf("%s/excluded-doc.md", config.DocumentationRoot) processor := embedding.NewProcessor(docPath, config) - Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) + context, err := processor.Embed() + + Expect(err).ShouldNot(HaveOccurred()) + Expect(context).ShouldNot(BeNil()) + Expect(context.EmbeddingsCount()).Should(Equal(0)) + Expect(context.IsContainsEmbedding()).Should(BeFalse()) Expect(processor.IsUpToDate()).Should(BeTrue()) }) }) diff --git a/embedding/parsing/context.go b/embedding/parsing/context.go index 4629383..01fa227 100644 --- a/embedding/parsing/context.go +++ b/embedding/parsing/context.go @@ -97,6 +97,15 @@ func NewContext(markdownFile string) Context { } } +// NewEmptyContext creates a Context for a documentation file that was not parsed. +func NewEmptyContext(markdownFile string) Context { + return Context{ + MarkdownFilePath: markdownFile, + Result: make([]string, 0), + lineIndex: 0, + } +} + // CurrentLine returns the line of source code at the current ParsingContext.lineIndex. func (c *Context) CurrentLine() string { return c.source[c.lineIndex] diff --git a/embedding/parsing/instruction.go b/embedding/parsing/instruction.go index 06a384d..ba5b7bd 100644 --- a/embedding/parsing/instruction.go +++ b/embedding/parsing/instruction.go @@ -20,6 +20,8 @@ package parsing import ( "fmt" + "log/slog" + "strings" "embed-code/embed-code-go/configuration" "embed-code/embed-code-go/embedding/commentfilter" @@ -158,16 +160,22 @@ func (e Instruction) Content() ([]string, error) { if err != nil { return nil, err } + codeFileReference, referenceErr := fragmentation.ResolveCodeFileReference( + e.CodeFile, + e.Configuration, + ) if e.StartPattern != nil || e.EndPattern != nil || e.LinePattern != nil { - codeFileReference, err := fragmentation.ResolveCodeFileReference(e.CodeFile, e.Configuration) - if err != nil { - return nil, err + if referenceErr != nil { + return nil, referenceErr } fileContent, err = e.matchingLines(fileContent, codeFileReference) if err != nil { return nil, err } } + if referenceErr == nil { + slog.Info(e.contentLogMessage(codeFileReference)) + } return commentfilter.Filter( fileContent, @@ -178,6 +186,41 @@ func (e Instruction) Content() ([]string, error) { ), nil } +// contentLogMessage describes the source content selected by this instruction. +func (e Instruction) contentLogMessage(codeFileReference string) string { + switch { + case e.Fragment != "": + return fmt.Sprintf("Extracted fragment `%s` from `%s`.", + e.Fragment, codeFileReference) + case e.LinePattern != nil: + return fmt.Sprintf("Extracted line-pattern embedding from `%s` using %s.", + codeFileReference, patternLabel("line", e.LinePattern)) + case e.StartPattern != nil || e.EndPattern != nil: + return fmt.Sprintf("Extracted start/end-pattern embedding from `%s` using %s.", + codeFileReference, rangePatternLabel(e.StartPattern, e.EndPattern)) + default: + return fmt.Sprintf("Extracted source file `%s`.", codeFileReference) + } +} + +// rangePatternLabel formats the start and end patterns set on an instruction. +func rangePatternLabel(start *Pattern, end *Pattern) string { + var labels []string + if start != nil { + labels = append(labels, patternLabel("start", start)) + } + if end != nil { + labels = append(labels, patternLabel("end", end)) + } + + return strings.Join(labels, " and ") +} + +// patternLabel formats a pattern for human-readable logs. +func patternLabel(kind string, pattern *Pattern) string { + return fmt.Sprintf("%s pattern `%s`", kind, pattern.sourceGlob) +} + // Returns string representation of Instruction. func (e Instruction) String() string { return fmt.Sprintf( diff --git a/embedding/parsing/instruction_test.go b/embedding/parsing/instruction_test.go index f89c1a8..fcba144 100644 --- a/embedding/parsing/instruction_test.go +++ b/embedding/parsing/instruction_test.go @@ -28,6 +28,7 @@ import ( "embed-code/embed-code-go/configuration" "embed-code/embed-code-go/embedding/parsing" + "embed-code/embed-code-go/logging" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -476,8 +477,8 @@ var _ = Describe("Instruction", func() { Expect(err).Should(MatchError( fmt.Sprintf( - "no line in code file `file://%s` matches the start pattern `foo bar`", - absTestCodeFile("org/example/Hello.java"), + "no line in code file `%s` matches the start pattern `foo bar`", + logging.FileReference(absTestCodeFile("org/example/Hello.java")), ), )) }) @@ -494,8 +495,8 @@ var _ = Describe("Instruction", func() { Expect(err).Should(MatchError( fmt.Sprintf( - "no line in code file `file://%s` matches the end pattern `foo bar`", - absTestCodeFile("org/example/Hello.java"), + "no line in code file `%s` matches the end pattern `foo bar`", + logging.FileReference(absTestCodeFile("org/example/Hello.java")), ), )) }) diff --git a/embedding/processor.go b/embedding/processor.go index 764a57a..a5db680 100644 --- a/embedding/processor.go +++ b/embedding/processor.go @@ -30,6 +30,7 @@ import ( "embed-code/embed-code-go/configuration" "embed-code/embed-code-go/embedding/parsing" "embed-code/embed-code-go/files" + "embed-code/embed-code-go/logging" "github.com/bmatcuk/doublestar/v4" ) @@ -62,34 +63,45 @@ type EmbedAllResult struct { // NewProcessor creates and returns new Processor with given docFile and config. func NewProcessor(docFile string, config configuration.Configuration) Processor { - return Processor{ - DocFilePath: docFile, - Config: config, - TransitionsMap: parsing.Transitions, - requiredDocPaths: requiredDocs(config), - } + return newProcessor(docFile, config, parsing.Transitions, requiredDocs(config)) } // NewProcessorWithTransitions Creates and returns new Processor with given docFile, config // and transitions. func NewProcessorWithTransitions(docFile string, config configuration.Configuration, transitions parsing.TransitionMap) Processor { + return newProcessor(docFile, config, transitions, requiredDocs(config)) +} + +// newProcessor creates a Processor with a precomputed documentation file list. +func newProcessor( + docFile string, + config configuration.Configuration, + transitions parsing.TransitionMap, + requiredDocPaths []string, +) Processor { return Processor{ DocFilePath: docFile, Config: config, TransitionsMap: transitions, - requiredDocPaths: requiredDocs(config), + requiredDocPaths: requiredDocPaths, } } -// Embed Constructs embedding and modifies the doc file if embedding is needed. +// Embed constructs embedding and modifies the doc file if embedding is needed. // +// Returns an empty context without parsing the file when it is excluded by configuration. // If any problems faced, an error is returned. func (p Processor) Embed() (*parsing.Context, error) { if !slices.Contains(p.requiredDocPaths, p.DocFilePath) { - return nil, nil + slog.Info(fmt.Sprintf("Skipping `%s`; it is excluded by the configuration.", + logging.FileReference(p.DocFilePath))) + context := parsing.NewEmptyContext(p.DocFilePath) + + return &context, nil } + slog.Info(fmt.Sprintf("Started processing doc file `%s`.", logging.FileReference(p.DocFilePath))) context, err := p.fillEmbeddingContext() if err != nil { return nil, err @@ -100,6 +112,10 @@ func (p Processor) Embed() (*parsing.Context, error) { if err != nil { return &context, err } + slog.Info(fmt.Sprintf("Updated `%s` after processing %d embedding(s).", + logging.FileReference(p.DocFilePath), context.EmbeddingsCount())) + } else { + slog.Info(fmt.Sprintf("Documentation is up-to-date in `%s`.", logging.FileReference(p.DocFilePath))) } return &context, nil @@ -135,14 +151,25 @@ func (p Processor) IsUpToDate() bool { // isUpToDate reports whether the target markdown is up-to-date and returns processing errors. func (p Processor) isUpToDate() (bool, error) { if !slices.Contains(p.requiredDocPaths, p.DocFilePath) { + slog.Info(fmt.Sprintf("Skipping `%s`; it is excluded by the configuration.", + logging.FileReference(p.DocFilePath))) return true, nil } + slog.Info(fmt.Sprintf("Checking `%s`.", logging.FileReference(p.DocFilePath))) context, err := p.fillEmbeddingContext() if err != nil { return false, err } - return !context.IsContentChanged(), nil + upToDate := !context.IsContentChanged() + status := "up to date" + if !upToDate { + status = "needs an update" + } + slog.Info(fmt.Sprintf("Checked `%s`: %d embedding(s), %s.", + logging.FileReference(p.DocFilePath), context.EmbeddingsCount(), status)) + + return upToDate, nil } // EmbedAll processes embedding for multiple documentation files based on provided config. @@ -157,7 +184,7 @@ func EmbedAll(config configuration.Configuration) EmbedAllResult { var updatedTargetFiles []string var embeddingErrors []error for _, doc := range requiredDocPaths { - processor := NewProcessor(doc, config) + processor := newProcessor(doc, config, parsing.Transitions, requiredDocPaths) context, err := processor.Embed() if err != nil { embeddingErrors = append(embeddingErrors, err) @@ -174,15 +201,16 @@ func EmbedAll(config configuration.Configuration) EmbedAllResult { if totalEmbeddings > 0 { slog.Info( fmt.Sprintf( - "Found `%d` target documentation files with `%d` embeddings under `%s`%s.", - len(requiredDocPaths), totalEmbeddings, config.DocumentationRoot, + "Processed %d documentation file(s) with %d embedding(s) in `%s`%s.", + len(requiredDocPaths), totalEmbeddings, + logging.FileReference(config.DocumentationRoot), configNameLabel(config), ), ) } else { slog.Warn( - fmt.Sprintf("No embedding instructions were found under `%s`%s.", - config.DocumentationRoot, configNameLabel(config)), + fmt.Sprintf("No embedding instructions were found in documentation folder `%s`%s.", + logging.FileReference(config.DocumentationRoot), configNameLabel(config)), ) } return EmbedAllResult{ @@ -192,11 +220,12 @@ func EmbedAll(config configuration.Configuration) EmbedAllResult { } } +// configNameLabel formats a configuration name for summary log messages. func configNameLabel(config configuration.Configuration) string { if config.Name == "" { return "" } - return fmt.Sprintf(" for embedding `%s`", config.Name) + return fmt.Sprintf(" for `%s` embedding setup", config.Name) } // CheckUpToDate returns documentation files that are not up-to-date with code files. @@ -313,7 +342,7 @@ func findChangedFiles(config configuration.Configuration) ([]string, []error) { var changedFiles []string var checkErrors []error for _, doc := range requiredDocPaths { - upToDate, err := NewProcessor(doc, config).isUpToDate() + upToDate, err := newProcessor(doc, config, parsing.Transitions, requiredDocPaths).isUpToDate() if err != nil { checkErrors = append(checkErrors, err) continue @@ -326,6 +355,7 @@ func findChangedFiles(config configuration.Configuration) ([]string, []error) { return changedFiles, checkErrors } +// requiredDocs returns documentation files matched by includes minus excludes. func requiredDocs(config configuration.Configuration) []string { documentationRoot := config.DocumentationRoot includedPatterns := config.DocIncludes @@ -341,12 +371,34 @@ func requiredDocs(config configuration.Configuration) []string { panic(err) } if len(excludedDocs) == 0 { + slog.Info(fmt.Sprintf( + "Found %d documentation file(s) from `%s` matching include pattern(s) %s.", + len(includedDocs), logging.FileReference(documentationRoot), + patternsLabel(includedPatterns), + )) return includedDocs } - return removeElements(includedDocs, excludedDocs) + result := removeElements(includedDocs, excludedDocs) + slog.Info(fmt.Sprintf( + "Found %d documentation file(s) from `%s` matching include pattern(s) %s and exclude pattern(s) %s.", + len(result), logging.FileReference(documentationRoot), patternsLabel(includedPatterns), + patternsLabel(excludedPatterns), + )) + + return result +} + +// patternsLabel formats glob patterns for human-readable log messages. +func patternsLabel(patterns []string) string { + if len(patterns) == 0 { + return "nothing" + } + + return "`" + strings.Join(patterns, "`, `") + "`" } +// getFilesByPatterns expands documentation glob patterns relative to the given root. func getFilesByPatterns(root string, patterns []string) ([]string, error) { var result []string for _, pattern := range patterns { diff --git a/fragmentation/resolver.go b/fragmentation/resolver.go index bcf4271..4203137 100644 --- a/fragmentation/resolver.go +++ b/fragmentation/resolver.go @@ -20,10 +20,12 @@ package fragmentation import ( "fmt" + "log/slog" "path/filepath" "strings" config "embed-code/embed-code-go/configuration" + "embed-code/embed-code-go/logging" _type "embed-code/embed-code-go/type" ) @@ -55,6 +57,10 @@ func ResolveContent(codePath string, fragmentName string, config config.Configur return nil, err } if !found { + slog.Info(fmt.Sprintf( + "Could not find source file `%s` in the configured source code folders.", + codePath, + )) return nil, unresolvedSourceError(codePath, fragmentName, config) } @@ -65,7 +71,8 @@ func ResolveContent(codePath string, fragmentName string, config config.Configur fragment, found := content.fragments[fragmentName] if !found { - codeFileReference := "file://" + source.absolutePath + codeFileReference := logging.FileReference(source.absolutePath) + slog.Info(missingFragmentLogMessage(fragmentName, source.absolutePath)) return nil, fmt.Errorf("fragment `%s` from code file `%s` not found", fragmentName, codeFileReference) } @@ -73,6 +80,16 @@ func ResolveContent(codePath string, fragmentName string, config config.Configur return fragmentLines(fragment, content.lines, config.Separator), nil } +// missingFragmentLogMessage describes a missing fragment without exposing internal names. +func missingFragmentLogMessage(fragmentName string, sourcePath string) string { + sourceReference := logging.FileReference(sourcePath) + if fragmentName == DefaultFragmentName { + return fmt.Sprintf("Could not load source file `%s`.", sourceReference) + } + + return fmt.Sprintf("Could not find fragment `%s` in `%s`.", fragmentName, sourceReference) +} + // ResolveCodeFileReference returns a user-facing reference to the source file. func ResolveCodeFileReference(codePath string, config config.Configuration) (string, error) { source, found, err := resolveSource(codePath, config) @@ -80,7 +97,7 @@ func ResolveCodeFileReference(codePath string, config config.Configuration) (str return "", err } if found { - return "file://" + source.absolutePath, nil + return logging.FileReference(source.absolutePath), nil } return codeFileReference(codePath, config) @@ -202,10 +219,10 @@ func codeFileReference(codePath string, config config.Configuration) (string, er return "", err } if named { - return fmt.Sprintf("%s (%s)", codePath, source.absolutePath), nil + return fmt.Sprintf("%s (%s)", codePath, logging.FileReference(source.absolutePath)), nil } if len(config.CodeRoots) == 1 { - return source.absolutePath, nil + return logging.FileReference(source.absolutePath), nil } } diff --git a/logging/logger.go b/logging/logger.go index a05e9a1..59f4226 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -22,8 +22,11 @@ import ( "fmt" "golang.org/x/net/context" "log/slog" + "net/url" "os" + "path/filepath" "runtime/debug" + "strconv" "strings" ) @@ -86,6 +89,63 @@ func (h *Handler) WithGroup(name string) slog.Handler { return &newHandler } +// FileReference returns a clickable file URL when the path can be made absolute. +func FileReference(path string) string { + absPath, err := filepath.Abs(path) + if err != nil { + return path + } + + return fileURLFromAbsolutePath(absPath) +} + +// FileReferenceWithLine returns a clickable file URL with an optional line suffix. +func FileReferenceWithLine(path string, line int) string { + reference := FileReference(path) + if line <= 0 { + return reference + } + + return reference + ":" + strconv.Itoa(line) +} + +// fileURLFromAbsolutePath formats an absolute local path as an OS-neutral file URL. +func fileURLFromAbsolutePath(path string) string { + normalizedPath := filepath.ToSlash(strings.ReplaceAll(path, "\\", "/")) + if isWindowsDrivePath(normalizedPath) { + return (&url.URL{ + Scheme: "file", + Path: "/" + normalizedPath, + }).String() + } + if strings.HasPrefix(normalizedPath, "//") { + withoutSlashes := strings.TrimPrefix(normalizedPath, "//") + host, pathAfterHost, _ := strings.Cut(withoutSlashes, "/") + + return (&url.URL{ + Scheme: "file", + Host: host, + Path: "/" + pathAfterHost, + }).String() + } + + return (&url.URL{ + Scheme: "file", + Path: normalizedPath, + }).String() +} + +// isWindowsDrivePath reports whether a slash-normalized path starts with a drive letter. +func isWindowsDrivePath(path string) bool { + if len(path) < 2 || path[1] != ':' { + return false + } + + driveLetter := path[0] + return (driveLetter >= 'A' && driveLetter <= 'Z') || + (driveLetter >= 'a' && driveLetter <= 'z') +} + // HandlePanic is a handler for the panic. // // To use, defer this function in any method that calls panic diff --git a/logging/logger_test.go b/logging/logger_test.go new file mode 100644 index 0000000..3cd0b97 --- /dev/null +++ b/logging/logger_test.go @@ -0,0 +1,61 @@ +// Copyright 2026, TeamDev. All rights reserved. +// +// Redistribution and use in source and/or binary forms, with or without +// modification, must retain the above copyright notice and the following +// disclaimer. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package logging + +import "testing" + +// TestFileURLFromAbsolutePath verifies OS-specific path shapes are valid file URLs. +func TestFileURLFromAbsolutePath(t *testing.T) { + tests := []struct { + name string + path string + want string + }{ + { + name: "unix path", + path: "/Users/me/project/file.go", + want: "file:///Users/me/project/file.go", + }, + { + name: "windows drive path", + path: `C:\Users\me\project\file.go`, + want: "file:///C:/Users/me/project/file.go", + }, + { + name: "windows drive path with spaces", + path: `C:\Users\me\my project\file.go`, + want: "file:///C:/Users/me/my%20project/file.go", + }, + { + name: "windows unc path", + path: `\\server\share\project\file.go`, + want: "file://server/share/project/file.go", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := fileURLFromAbsolutePath(test.path) + if got != test.want { + t.Fatalf("fileURLFromAbsolutePath(%q) = %q, want %q", + test.path, got, test.want) + } + }) + } +} diff --git a/main.go b/main.go index c09df6e..95d4ae1 100644 --- a/main.go +++ b/main.go @@ -24,11 +24,10 @@ import ( "embed-code/embed-code-go/logging" "fmt" "log/slog" - "path/filepath" ) // Version of the embed-code application. -const Version = "1.2.1" +const Version = "1.2.2" // The entry point for embed-code. // @@ -82,6 +81,11 @@ func main() { userArgs := cli.ReadArgs() configureLogging(userArgs) defer logging.HandlePanic(userArgs.Stacktrace) + source := "command line arguments" + if cli.IsUsingConfigFile(userArgs) { + source = fmt.Sprintf("configuration file `%s`", logging.FileReference(userArgs.ConfigPath)) + } + slog.Info(fmt.Sprintf("Started embed-code in `%s` mode using %s.", userArgs.Mode, source)) if cli.IsUsingConfigFile(userArgs) { err := cli.ValidateConfigFile(userArgs) @@ -124,6 +128,7 @@ func configureLogging(config cli.Config) { slog.SetDefault(logger) } +// logError writes a user-facing error through the configured logger. func logError(message string, err error) { slog.Error(fmt.Sprintf("%s: %v", message, err)) } @@ -168,10 +173,6 @@ func printFiles(singularHeading string, pluralHeading string, files []string) { fmt.Println(pluralHeading) } for _, file := range files { - absPath, err := filepath.Abs(file) - if err != nil { - panic(err) - } - fmt.Printf("- file://%s.\n", absPath) + fmt.Printf("- %s.\n", logging.FileReference(file)) } }