1 /**
2  * Contains type definition for `dub.selections.json`
3  */
4 module dub.recipe.selection;
5 
6 import dub.dependency;
7 import dub.internal.vibecompat.core.file : NativePath;
8 
9 import dub.internal.configy.Attributes;
10 
11 import std.exception;
12 
13 public struct Selected
14 {
15     /// The current version of the file format
16     public uint fileVersion;
17 
18     /// The selected package and their matching versions
19     public SelectedDependency[string] versions;
20 }
21 
22 
23 /// Wrapper around `SelectedDependency` to do deserialization but still provide
24 /// a `Dependency` object to client code.
25 private struct SelectedDependency
26 {
27     public Dependency actual;
28     alias actual this;
29 
30     /// Constructor, used in `fromYAML`
31     public this (inout(Dependency) dep) inout @safe pure nothrow @nogc
32     {
33         this.actual = dep;
34     }
35 
36     /// Allow external code to assign to this object as if it was a `Dependency`
37     public ref SelectedDependency opAssign (Dependency dep) return pure nothrow @nogc
38     {
39         this.actual = dep;
40         return this;
41     }
42 
43     /// Read a `Dependency` from the config file - Required to support both short and long form
44     static SelectedDependency fromYAML (scope ConfigParser!SelectedDependency p)
45     {
46         import dub.internal.dyaml.node;
47 
48         if (p.node.nodeID == NodeID.scalar)
49             return SelectedDependency(Dependency(Version(p.node.as!string)));
50 
51         auto d = p.parseAs!YAMLFormat;
52         if (d.path.length)
53             return SelectedDependency(Dependency(NativePath(d.path)));
54         else
55         {
56             assert(d.version_.length);
57             if (d.repository.length)
58                 return SelectedDependency(Dependency(Repository(d.repository, d.version_)));
59             return SelectedDependency(Dependency(Version(d.version_)));
60         }
61     }
62 
63 	/// In-file representation of a dependency as permitted in `dub.selections.json`
64 	private struct YAMLFormat
65 	{
66 		@Optional @Name("version") string version_;
67 		@Optional string path;
68 		@Optional string repository;
69 
70 		public void validate () const scope @safe pure
71 		{
72 			enforce(this.version_.length || this.path.length || this.repository.length,
73 				"Need to provide a version string, or an object with one of the following fields: `version`, `path`, or `repository`");
74 			enforce(!this.path.length || !this.repository.length,
75 				"Cannot provide a `path` dependency if a repository dependency is used");
76 			enforce(!this.path.length || !this.version_.length,
77 				"Cannot provide a `path` dependency if a `version` dependency is used");
78 			enforce(!this.repository.length || this.version_.length,
79 				"Cannot provide a `repository` dependency without a `version`");
80 		}
81 	}
82 }
83 
84 // Ensure we can read all type of dependencies
85 unittest
86 {
87     import dub.internal.configy.Read : parseConfigString;
88     import dub.internal.vibecompat.core.file : NativePath;
89 
90     immutable string content = `{
91     "fileVersion": 1,
92     "versions": {
93         "simple": "1.5.6",
94         "branch": "~master",
95         "branch2": "~main",
96         "path": { "path": "../some/where" },
97         "repository": { "repository": "git+https://github.com/dlang/dub", "version": "123456123456123456" }
98     }
99 }`;
100 
101     auto s = parseConfigString!Selected(content, "/dev/null");
102     assert(s.fileVersion == 1);
103     assert(s.versions.length == 5);
104     assert(s.versions["simple"]     == Dependency(Version("1.5.6")));
105     assert(s.versions["branch"]     == Dependency(Version("~master")));
106     assert(s.versions["branch2"]    == Dependency(Version("~main")));
107     assert(s.versions["path"]       == Dependency(NativePath("../some/where")));
108     assert(s.versions["repository"] == Dependency(Repository("git+https://github.com/dlang/dub", "123456123456123456")));
109 }