Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

Part V: Generated Models and Builders

The source generator turns ~80 Ruby templates into ~400 C# files. This part shows what those files look like: typed config classes, fluent builders, version annotations, and how it all feels in the IDE.

A Generated Config Class

Each Ruby prefix becomes a C# class. Here is what the generator emits for the nginx prefix (simplified for clarity):

// <auto-generated/>
#nullable enable

namespace FrenchExDev.Net.GitLab.DockerCompose;

[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public partial class NginxConfig
{
    /// <summary>nginx['enable']</summary>
    public bool? Enable { get; set; }

    /// <summary>nginx['client_max_body_size']</summary>
    public string? ClientMaxBodySize { get; set; }

    /// <summary>nginx['redirect_http_to_https']</summary>
    public bool? RedirectHttpToHttps { get; set; }

    /// <summary>Most root users won't need this setting.</summary>
    public int? ListenPort { get; set; }

    /// <summary>Most root users won't need this setting.</summary>
    public bool? ListenHttps { get; set; }

    /// <summary>nginx['proxy_set_headers']</summary>
    public Dictionary<string, string?>? ProxySetHeaders { get; set; }

    /// <summary>nginx['real_ip_trusted_addresses']</summary>
    public List<string>? RealIpTrustedAddresses { get; set; }

    /// <summary>nginx['custom_nginx_config']</summary>
    [SinceVersion("15.2.5")]
    public string? CustomNginxConfig { get; set; }

    // ... ~30 more properties
}

Key characteristics:

  • All properties are nullable. A null property means "use the GitLab default." Only non-null properties are rendered to Ruby.
  • Doc comments come from ##! lines in the original template. When the parser found ##! Most root users won't need this setting. before nginx['listen_port'], it attached that comment to the property.
  • Version attributes mark when a setting appeared or disappeared. CustomNginxConfig was introduced in v15.2.5, so it carries [SinceVersion("15.2.5")]. Settings present in all tracked versions have no version attributes.
  • Type mapping is automatic. Ruby true/false becomes bool?. Ruby 123 becomes int?. Ruby ['a', 'b'] becomes List<string>?. Ruby { 'k' => 'v' } becomes Dictionary<string, string?>?.

Nested Config Classes

When Ruby settings use nested bracket keys or hash values, the generator emits sub-classes:

# gitlab_rails['object_store']['enabled'] = true
# gitlab_rails['object_store']['proxy_download'] = true
# gitlab_rails['object_store']['connection'] = {
#   'provider' => 'AWS',
#   'region' => 'us-east-1',
#   'aws_access_key_id' => 'ACCESS_KEY',
#   'aws_secret_access_key' => 'SECRET_KEY',
# }
# gitlab_rails['object_store']['objects']['artifacts']['bucket'] = 'gitlab-artifacts'

This produces a class hierarchy:

public partial class GitLabRailsConfig
{
    // ... 200+ other properties

    /// <summary>gitlab_rails['object_store']</summary>
    public GitLabRailsConfigObjectStore? ObjectStore { get; set; }
}

public partial class GitLabRailsConfigObjectStore
{
    public bool? Enabled { get; set; }
    public bool? ProxyDownload { get; set; }
    public GitLabRailsConfigObjectStoreConnection? Connection { get; set; }
    public GitLabRailsConfigObjectStoreObjects? Objects { get; set; }
}

public partial class GitLabRailsConfigObjectStoreConnection
{
    public string? Provider { get; set; }
    public string? Region { get; set; }
    public string? AwsAccessKeyId { get; set; }
    public string? AwsSecretAccessKey { get; set; }
}

public partial class GitLabRailsConfigObjectStoreObjects
{
    public GitLabRailsConfigObjectStoreObjectsArtifacts? Artifacts { get; set; }
    // ... other bucket types: lfs, uploads, packages, etc.
}

public partial class GitLabRailsConfigObjectStoreObjectsArtifacts
{
    public string? Bucket { get; set; }
}

The nesting depth is determined by the bracket nesting in the Ruby template. Each intermediate node becomes its own class with its own builder.

The Root Config

The root GitLabOmnibusConfig aggregates all 55 prefix classes plus standalone URL properties:

// <auto-generated/>
#nullable enable

namespace FrenchExDev.Net.GitLab.DockerCompose;

[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public partial class GitLabOmnibusConfig
{
    // Standalone URLs
    /// <summary>external_url 'http://gitlab.example.com'</summary>
    public string? ExternalUrl { get; set; }

    /// <summary>registry_external_url 'https://registry.example.com'</summary>
    public string? RegistryExternalUrl { get; set; }

    /// <summary>pages_external_url 'http://pages.example.com'</summary>
    public string? PagesExternalUrl { get; set; }

    /// <summary>mattermost_external_url 'http://mattermost.example.com'</summary>
    public string? MattermostExternalUrl { get; set; }

    /// <summary>gitlab_kas_external_url 'wss://kas.example.com'</summary>
    [SinceVersion("17.0.6")]
    public string? GitlabKasExternalUrl { get; set; }

    // Prefix sections (55 sub-config objects)
    public GitLabRailsConfig? GitlabRails { get; set; }
    public NginxConfig? Nginx { get; set; }
    public PostgresqlConfig? Postgresql { get; set; }
    public RedisConfig? Redis { get; set; }
    public RegistryConfig? Registry { get; set; }
    public PrometheusConfig? Prometheus { get; set; }
    public GitalyConfig? Gitaly { get; set; }
    public PumaConfig? Puma { get; set; }
    public SidekiqConfig? Sidekiq { get; set; }
    public LetsencryptConfig? Letsencrypt { get; set; }
    public GitLabKasConfig? GitlabKas { get; set; }
    public MattermostConfig? Mattermost { get; set; }
    public GitLabPagesConfig? GitlabPages { get; set; }
    public GitLabShellConfig? GitlabShell { get; set; }
    public GitLabWorkhorseConfig? GitlabWorkhorse { get; set; }
    // ... 40 more prefix sections

    public string? Roles { get; set; }
}

The Builder API

Every config class gets a companion builder class. The builders are emitted by the shared BuilderEmitter from FrenchExDev.Net.Builder.SourceGenerator.Lib -- the same infrastructure described in Builder Pattern.

Root Builder

var config = new GitLabOmnibusConfigBuilder()
    .WithExternalUrl("https://gitlab.example.com")
    .WithRegistryExternalUrl("https://registry.example.com")
    .WithNginx(n => n
        .WithListenPort(80)
        .WithListenHttps(false)
        .WithProxySetHeaders(new Dictionary<string, string?>
        {
            ["X-Forwarded-Proto"] = "https",
            ["X-Forwarded-Ssl"] = "on",
        }))
    .WithGitlabRails(r => r
        .WithSmtpEnable(true)
        .WithSmtpAddress("smtp.example.com")
        .WithSmtpPort(587)
        .WithDbHost("postgresql")
        .WithDbPassword("${GITLAB_DB_PASSWORD}")
        .WithObjectStore(os => os
            .WithEnabled(true)
            .WithConnection(c => c
                .WithProvider("AWS")
                .WithRegion("us-east-1")
                .WithAwsAccessKeyId("${AWS_ACCESS_KEY}")
                .WithAwsSecretAccessKey("${AWS_SECRET_KEY}"))
            .WithObjects(o => o
                .WithArtifacts(a => a
                    .WithBucket("gitlab-artifacts")))))
    .WithRedis(r => r
        .WithEnable(false))
    .WithLetsencrypt(l => l
        .WithEnable(false))
    .Build();

Each With* method on a sub-object builder takes an Action<SubBuilder> lambda, enabling the nested fluent syntax. The Build() method returns a fully populated GitLabOmnibusConfig object.

Version-Aware Builder Methods

Builder methods carry the same version attributes as the config properties:

// Generated builder method for GitlabKasExternalUrl
/// <summary>gitlab_kas_external_url 'wss://kas.example.com'</summary>
[SinceVersion("17.0.6")]
public GitLabOmnibusConfigBuilder WithGitlabKasExternalUrl(string? value)
{
    _gitlabKasExternalUrl = value;
    return this;
}

When you hover over .WithGitlabKasExternalUrl() in the IDE, the tooltip shows:

  • The XML doc comment: gitlab_kas_external_url 'wss://kas.example.com'
  • The [SinceVersion("17.0.6")] attribute

This tells you: "this setting exists from GitLab v17.0.6 onwards."

Version Metadata

The generator emits a GitLabOmnibusVersions class with the complete version list:

// <auto-generated/>
public static class GitLabOmnibusVersions
{
    private static readonly string[] _versions = new[]
    {
        "9.2.10",
        "15.0.5",
        "15.1.7",
        "15.2.5",
        // ... ~80 versions
        "18.9.7",
        "18.10.1",
    };

    public static IReadOnlyList<string> Available => _versions;
    public static string Latest => "18.10.1";
    public static string Oldest => "9.2.10";
}

And the version attribute definitions:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class |
                AttributeTargets.Method, AllowMultiple = false)]
internal sealed class SinceVersionAttribute : Attribute
{
    public string Version { get; }
    public SinceVersionAttribute(string version) => Version = version;
}

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class |
                AttributeTargets.Method, AllowMultiple = false)]
internal sealed class UntilVersionAttribute : Attribute
{
    public string Version { get; }
    public UntilVersionAttribute(string version) => Version = version;
}

Rendering Metadata

The generator also emits GitLabRbMetadata -- a registry of all properties with their Ruby prefix, key path, and value kind. This metadata is used by the renderer at runtime to handle special cases:

public sealed record GitLabRbPropertyMeta(
    string Prefix,
    string[] KeyPath,
    GitLabRbValueKind ValueKind);

public static class GitLabRbMetadata
{
    public static readonly IReadOnlyList<GitLabRbPropertyMeta> Properties =
        new GitLabRbPropertyMeta[]
        {
            new("nginx", new[] { "enable" }, GitLabRbValueKind.Boolean),
            new("nginx", new[] { "listen_port" }, GitLabRbValueKind.Integer),
            new("nginx", new[] { "proxy_set_headers" }, GitLabRbValueKind.StringDict),
            new("gitlab_rails", new[] { "smtp_enable" }, GitLabRbValueKind.Boolean),
            new("gitlab_rails", new[] { "object_store", "enabled" }, GitLabRbValueKind.Boolean),
            new("gitlab_rails", new[] { "object_store", "connection" }, GitLabRbValueKind.SubObject),
            // ... hundreds more
        };

    public static readonly IReadOnlyList<string> StandaloneUrls = new[]
    {
        "external_url",
        "registry_external_url",
        "pages_external_url",
        "mattermost_external_url",
        "gitlab_kas_external_url",
        "runtime_dir",
    };
}

Scale

To put the numbers in perspective:

Metric Value
Input files ~80 versioned .rb templates
Input size ~7 MB total
Ruby prefixes 55
Total settings ~500 across all prefixes
Generated config classes ~55 (top-level) + ~150 (nested)
Generated builder classes ~200
Generated metadata files 3 (versions, attributes, metadata)
Total generated files ~400
Generated code size ~50,000 lines

All of this is generated at build time from a single [Generator] attribute on GitLabOmnibusGenerator. The developer sees none of the generation machinery -- only the clean, typed API surface.

The next part shows what happens when you call Build() on the builder: how GitLabRbRenderer converts the populated C# object graph back into valid Ruby syntax.

⬇ Download