Newer
Older
dub_jkp / source / dub / data / settings.d
/*******************************************************************************

    Contains struct definition for settings.json files

    User settings are file that allow to configure dub default behavior.

*******************************************************************************/

module dub.data.settings;

import dub.internal.configy.Attributes;
import dub.internal.vibecompat.inet.path;

/// Determines which of the default package suppliers are queried for packages.
public enum SkipPackageSuppliers {
    none,       /// Uses all configured package suppliers.
    standard,   /// Does not use the default package suppliers (`defaultPackageSuppliers`).
    configured, /// Does not use default suppliers or suppliers configured in DUB's configuration file
    all,        /// Uses only manually specified package suppliers.
    default_,   /// The value wasn't specified. It is provided in order to know when it is safe to ignore it
}

/**
 * User-provided settings (configuration)
 *
 * All fields in this struct should be optional.
 * Fields that are *not* optional should be mandatory from the POV
 * of the application, not the POV of file parsing.
 * For example, git's `core.author` and `core.email` are required to commit,
 * but the error happens on the commit, not when the gitconfig is parsed.
 *
 * We have multiple configuration locations, and two kinds of fields:
 * additive and non-additive. Additive fields are fields which are the union
 * of all configuration files (e.g. `registryURLs`). Non-additive fields
 * will ignore values set in lower priorities configuration, although parsing
 * must still succeed. Additive fields are marked as `@Optional`,
 * non-additive are marked as `SetInfo`.
 */
package(dub) struct Settings {
    @Optional string[] registryUrls;
    @Optional NativePath[] customCachePaths;

    private struct SkipRegistry {
	    SkipPackageSuppliers skipRegistry;
	    static SkipRegistry fromString (string value) {
		    import std.conv : to;
		    auto result = value.to!SkipPackageSuppliers;
		    if (result == SkipPackageSuppliers.default_) {
			    throw new Exception(
				"skipRegistry value `default_` is only meant for interal use."
				~ " Instead, use one of `none`, `standard`, `configured`, or `all`."
						);
		    }
		    return SkipRegistry(result);
	    }
	    alias skipRegistry this;
    }
    SetInfo!(SkipRegistry) skipRegistry;
    SetInfo!(string) defaultCompiler;
    SetInfo!(string) defaultArchitecture;
    SetInfo!(bool) defaultLowMemory;

    SetInfo!(string[string]) defaultEnvironments;
    SetInfo!(string[string]) defaultBuildEnvironments;
    SetInfo!(string[string]) defaultRunEnvironments;
    SetInfo!(string[string]) defaultPreGenerateEnvironments;
    SetInfo!(string[string]) defaultPostGenerateEnvironments;
    SetInfo!(string[string]) defaultPreBuildEnvironments;
    SetInfo!(string[string]) defaultPostBuildEnvironments;
    SetInfo!(string[string]) defaultPreRunEnvironments;
    SetInfo!(string[string]) defaultPostRunEnvironments;
    SetInfo!(string) dubHome;

    /// Merge a lower priority config (`this`) with a `higher` priority config
    public Settings merge(Settings higher)
        return @safe pure nothrow
    {
        import std.traits : hasUDA;
        Settings result;

        static foreach (idx, _; Settings.tupleof) {
            static if (hasUDA!(Settings.tupleof[idx], Optional))
                result.tupleof[idx] = higher.tupleof[idx] ~ this.tupleof[idx];
            else static if (IsSetInfo!(typeof(this.tupleof[idx]))) {
                if (higher.tupleof[idx].set)
                    result.tupleof[idx] = higher.tupleof[idx];
                else
                    result.tupleof[idx] = this.tupleof[idx];
            } else
                static assert(false,
                              "Expect `@Optional` or `SetInfo` on: `" ~
                              __traits(identifier, this.tupleof[idx]) ~
                              "` of type : `" ~
                              typeof(this.tupleof[idx]).stringof ~ "`");
        }

        return result;
    }

    /// Workaround multiple `E` declaration in `static foreach` when inline
    private template IsSetInfo(T) { enum bool IsSetInfo = is(T : SetInfo!E, E); }
}

unittest {
    import dub.internal.configy.Read;

    const str1 = `{
  "registryUrls": [ "http://foo.bar\/optional\/escape" ],
  "customCachePaths": [ "foo/bar", "foo/foo" ],

  "skipRegistry": "all",
  "defaultCompiler": "dmd",
  "defaultArchitecture": "fooarch",
  "defaultLowMemory": false,

  "defaultEnvironments": {
    "VAR2": "settings.VAR2",
    "VAR3": "settings.VAR3",
    "VAR4": "settings.VAR4"
  }
}`;

    const str2 = `{
  "registryUrls": [ "http://bar.foo" ],
  "customCachePaths": [ "bar/foo", "bar/bar" ],

  "skipRegistry": "none",
  "defaultCompiler": "ldc",
  "defaultArchitecture": "bararch",
  "defaultLowMemory": true,

  "defaultEnvironments": {
    "VAR": "Hi",
  }
}`;

     auto c1 = parseConfigString!Settings(str1, "/dev/null");
     assert(c1.registryUrls == [ "http://foo.bar/optional/escape" ]);
     assert(c1.customCachePaths == [ NativePath("foo/bar"), NativePath("foo/foo") ]);
     assert(c1.skipRegistry == SkipPackageSuppliers.all);
     assert(c1.defaultCompiler == "dmd");
     assert(c1.defaultArchitecture == "fooarch");
     assert(c1.defaultLowMemory == false);
     assert(c1.defaultEnvironments.length == 3);
     assert(c1.defaultEnvironments["VAR2"] == "settings.VAR2");
     assert(c1.defaultEnvironments["VAR3"] == "settings.VAR3");
     assert(c1.defaultEnvironments["VAR4"] == "settings.VAR4");

     auto c2 = parseConfigString!Settings(str2, "/dev/null");
     assert(c2.registryUrls == [ "http://bar.foo" ]);
     assert(c2.customCachePaths == [ NativePath("bar/foo"), NativePath("bar/bar") ]);
     assert(c2.skipRegistry == SkipPackageSuppliers.none);
     assert(c2.defaultCompiler == "ldc");
     assert(c2.defaultArchitecture == "bararch");
     assert(c2.defaultLowMemory == true);
     assert(c2.defaultEnvironments.length == 1);
     assert(c2.defaultEnvironments["VAR"] == "Hi");

     auto m1 = c2.merge(c1);
     // c1 takes priority, so its registryUrls is first
     assert(m1.registryUrls == [ "http://foo.bar/optional/escape", "http://bar.foo" ]);
     // Same with CCP
     assert(m1.customCachePaths == [
         NativePath("foo/bar"), NativePath("foo/foo"),
         NativePath("bar/foo"), NativePath("bar/bar"),
     ]);

     // c1 fields only
     assert(m1.skipRegistry == c1.skipRegistry);
     assert(m1.defaultCompiler == c1.defaultCompiler);
     assert(m1.defaultArchitecture == c1.defaultArchitecture);
     assert(m1.defaultLowMemory == c1.defaultLowMemory);
     assert(m1.defaultEnvironments == c1.defaultEnvironments);

     auto m2 = c1.merge(c2);
     assert(m2.registryUrls == [ "http://bar.foo", "http://foo.bar/optional/escape" ]);
     assert(m2.customCachePaths == [
         NativePath("bar/foo"), NativePath("bar/bar"),
         NativePath("foo/bar"), NativePath("foo/foo"),
     ]);
     assert(m2.skipRegistry == c2.skipRegistry);
     assert(m2.defaultCompiler == c2.defaultCompiler);
     assert(m2.defaultArchitecture == c2.defaultArchitecture);
     assert(m2.defaultLowMemory == c2.defaultLowMemory);
     assert(m2.defaultEnvironments == c2.defaultEnvironments);

     auto m3 = Settings.init.merge(c1);
     assert(m3 == c1);
}

unittest {
    // Test that SkipPackageRegistry.default_ is not allowed

    import dub.internal.configy.Read;
    import std.exception : assertThrown;

    const str1 = `{
  "skipRegistry": "all"
`;
    assertThrown!Exception(parseConfigString!Settings(str1, "/dev/null"));
}