diff --git a/.gitignore b/.gitignore index 7981eaa..c48505d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ /test/ignore-hidden-2/ignore-hidden-2 /test/expected-import-path-output /test/expected-string-import-path-output +/test/expected-describe-data-1-list-output +/test/expected-describe-data-2-dmd-output +/test/describe-project/dummy.dat +/test/describe-project/dummy-dep1.dat diff --git a/build-files.txt b/build-files.txt index 9a1c99d..58e2808 100644 --- a/build-files.txt +++ b/build-files.txt @@ -2,6 +2,7 @@ source/dub/commandline.d source/dub/dependency.d source/dub/dependencyresolver.d +source/dub/description.d source/dub/dub.d source/dub/init.d source/dub/packagemanager.d @@ -19,6 +20,7 @@ source/dub/generators/cmake.d source/dub/generators/generator.d source/dub/generators/sublimetext.d +source/dub/generators/targetdescription.d source/dub/generators/visuald.d source/dub/internal/libInputVisitor.d source/dub/internal/sdlang/ast.d @@ -33,6 +35,7 @@ source/dub/internal/vibecompat/core/file.d source/dub/internal/vibecompat/core/log.d source/dub/internal/vibecompat/data/json.d +source/dub/internal/vibecompat/data/serialization.d source/dub/internal/vibecompat/data/utils.d source/dub/internal/vibecompat/inet/path.d source/dub/internal/vibecompat/inet/url.d diff --git a/source/dub/commandline.d b/source/dub/commandline.d index e8c5623..04c2e34 100644 --- a/source/dub/commandline.d +++ b/source/dub/commandline.d @@ -13,6 +13,7 @@ import dub.generators.generator; import dub.internal.vibecompat.core.file; import dub.internal.vibecompat.core.log; +import dub.internal.vibecompat.data.json; import dub.internal.vibecompat.inet.url; import dub.package_; import dub.packagemanager; @@ -748,6 +749,9 @@ private { bool m_importPaths = false; bool m_stringImportPaths = false; + bool m_dataList = false; + bool m_dataNullDelim = false; + string[] m_data; } this() @@ -760,14 +764,28 @@ "their dependencies in a format similar to a JSON package " "description file. This is useful mostly for IDEs.", "", - "When --import-paths is supplied, the import paths for a project ", - "will be printed line-by-line instead. The paths for D source " - "files across all dependent projects will be included.", + "All usual options that are also used for build/run/generate apply.", "", - "--string-import-paths can can supplied to print the string " - "import paths for a project.", + "When --data=VALUE is supplied, specific build settings for a project ", + "will be printed instead (by default, formatted for the current compiler).", "", - "All usual options that are also used for build/run/generate apply." + "The --data=VALUE option can be specified multiple times to retrieve " + "several pieces of information at once. A comma-separated list is " + "also acceptable (ex: --data=dflags,libs). The data will be output in " + "the same order requested on the command line.", + "", + "The accepted values for --data=VALUE are:", + "", + "main-source-file, dflags, lflags, libs, linker-files, " + "source-files, versions, debug-versions, import-paths, " + "string-import-paths, import-files, options", + "", + "The following are also accepted by --data if --data-list is used:", + "", + "target-type, target-path, target-name, working-directory, " + "copy-files, string-import-files, pre-generate-commands," + "post-generate-commands, pre-build-commands, post-build-commands, " + "requirements", ]; } @@ -776,11 +794,28 @@ super.prepare(args); args.getopt("import-paths", &m_importPaths, [ - "List the import paths for project." + "Shortcut for --data=import-paths --data-list" ]); args.getopt("string-import-paths", &m_stringImportPaths, [ - "List the string import paths for project." + "Shortcut for --data=string-import-paths --data-list" + ]); + + args.getopt("data", &m_data, [ + "Just list the values of a particular build setting, either for this "~ + "package alone or recursively including all dependencies. Accepts a "~ + "comma-separated list. See above for more details and accepted "~ + "possibilities for VALUE." + ]); + + args.getopt("data-list", &m_dataList, [ + "Output --data information in list format (line-by-line), instead "~ + "of formatting for a compiler command line.", + ]); + + args.getopt("data-0", &m_dataNullDelim, [ + "Output --data information using null-delimiters, rather than "~ + "spaces or newlines. Result is usable with, ex., xargs -0.", ]); } @@ -791,6 +826,11 @@ "--import-paths and --string-import-paths may not be used together." ); + enforceUsage( + !(m_data && (m_importPaths || m_stringImportPaths)), + "--data may not be used together with --import-paths or --string-import-paths." + ); + // disable all log output and use "writeln" to output the JSON description auto ll = getLogLevel(); setLogLevel(LogLevel.none); @@ -806,11 +846,15 @@ auto config = m_buildConfig.length ? m_buildConfig : m_defaultConfig; if (m_importPaths) { - dub.listImportPaths(m_buildPlatform, config); + dub.listImportPaths(m_buildPlatform, config, m_buildType, m_dataNullDelim); } else if (m_stringImportPaths) { - dub.listStringImportPaths(m_buildPlatform, config); + dub.listStringImportPaths(m_buildPlatform, config, m_buildType, m_dataNullDelim); + } else if (m_data) { + dub.listProjectData(m_buildPlatform, config, m_buildType, m_data, + m_dataList? null : m_compiler, m_dataNullDelim); } else { - dub.describeProject(m_buildPlatform, config); + auto desc = dub.project.describe(m_buildPlatform, config, m_buildType); + writeln(desc.serializeToPrettyJson()); } return 0; diff --git a/source/dub/compilers/buildsettings.d b/source/dub/compilers/buildsettings.d index e752e80..556c839 100644 --- a/source/dub/compilers/buildsettings.d +++ b/source/dub/compilers/buildsettings.d @@ -12,11 +12,15 @@ import std.array : array; import std.algorithm : filter; import std.path : globMatch; +static if (__VERSION__ >= 2067) + import std.typecons : BitFlags; /// BuildPlatform specific settings, like needed libraries or additional /// include paths. struct BuildSettings { + import dub.internal.vibecompat.data.serialization; + TargetType targetType; string targetPath; string targetName; @@ -25,6 +29,7 @@ string[] dflags; string[] lflags; string[] libs; + string[] linkerFiles; string[] sourceFiles; string[] copyFiles; string[] versions; @@ -37,8 +42,8 @@ string[] postGenerateCommands; string[] preBuildCommands; string[] postBuildCommands; - BuildRequirements requirements; - BuildOptions options; + @byName BuildRequirements requirements; + @byName BuildOptions options; BuildSettings dup() const { @@ -60,6 +65,7 @@ addDFlags(bs.dflags); addLFlags(bs.lflags); addLibs(bs.libs); + addLinkerFiles(bs.linkerFiles); addSourceFiles(bs.sourceFiles); addCopyFiles(bs.copyFiles); addVersions(bs.versions); @@ -78,6 +84,7 @@ void removeDFlags(in string[] value...) { remove(dflags, value); } void addLFlags(in string[] value...) { lflags ~= value; } void addLibs(in string[] value...) { add(libs, value); } + void addLinkerFiles(in string[] value...) { add(linkerFiles, value); } void addSourceFiles(in string[] value...) { add(sourceFiles, value); } void prependSourceFiles(in string[] value...) { prepend(sourceFiles, value); } void removeSourceFiles(in string[] value...) { removePaths(sourceFiles, value); } @@ -94,9 +101,12 @@ void addPostGenerateCommands(in string[] value...) { add(postGenerateCommands, value, false); } void addPreBuildCommands(in string[] value...) { add(preBuildCommands, value, false); } void addPostBuildCommands(in string[] value...) { add(postBuildCommands, value, false); } - void addRequirements(in BuildRequirements[] value...) { foreach (v; value) this.requirements |= v; } - void addOptions(in BuildOptions[] value...) { foreach (v; value) this.options |= v; } - void removeOptions(in BuildOptions[] value...) { foreach (v; value) this.options &= ~v; } + void addRequirements(in BuildRequirement[] value...) { foreach (v; value) this.requirements |= v; } + void addRequirements(in BuildRequirements value) { this.requirements |= value; } + void addOptions(in BuildOption[] value...) { foreach (v; value) this.options |= v; } + void addOptions(in BuildOptions value) { this.options |= value; } + void removeOptions(in BuildOption[] value...) { foreach (v; value) this.options &= ~v; } + void removeOptions(in BuildOptions value) { this.options &= ~value; } // Adds vals to arr without adding duplicates. private void add(ref string[] arr, in string[] vals, bool no_duplicates = true) @@ -203,7 +213,7 @@ object } -enum BuildRequirements { +enum BuildRequirement { none = 0, /// No special requirements allowWarnings = 1<<0, /// Warnings do not abort compilation silenceWarnings = 1<<1, /// Don't show warnings @@ -217,7 +227,46 @@ noDefaultFlags = 1<<9, /// Do not issue any of the default build flags (e.g. -debug, -w, -property etc.) - use only for development purposes } -enum BuildOptions { + struct BuildRequirements { + import dub.internal.vibecompat.data.serialization : ignore; + + static if (__VERSION__ >= 2067) { + @ignore BitFlags!BuildRequirement values; + this(BuildRequirement req) { values = req; } + deprecated("Use BuildRequirement.* instead.") { + enum none = BuildRequirement.none; + enum allowWarnings = BuildRequirement.allowWarnings; + enum silenceWarnings = BuildRequirement.silenceWarnings; + enum disallowDeprecations = BuildRequirement.disallowDeprecations; + enum silenceDeprecations = BuildRequirement.silenceDeprecations; + enum disallowInlining = BuildRequirement.disallowInlining; + enum disallowOptimization = BuildRequirement.disallowOptimization; + enum requireBoundsCheck = BuildRequirement.requireBoundsCheck; + enum requireContracts = BuildRequirement.requireContracts; + enum relaxProperties = BuildRequirement.relaxProperties; + enum noDefaultFlags = BuildRequirement.noDefaultFlags; + } + } else { + @ignore BuildRequirement values; + this(BuildRequirement req) { values = req; } + BuildRequirement[] toRepresentation() + const { + BuildRequirement[] ret; + for (int f = 1; f <= BuildRequirement.max; f *= 2) + if (values & f) ret ~= cast(BuildRequirement)f; + return ret; + } + static BuildRequirements fromRepresentation(BuildRequirement[] v) + { + BuildRequirements ret; + foreach (f; v) ret.values |= f; + return ret; + } + } + alias values this; + } + +enum BuildOption { none = 0, /// Use compiler defaults debugMode = 1<<0, /// Compile in debug mode (enables contracts, -debug) releaseMode = 1<<1, /// Compile in release mode (disables assertions and bounds checks, -release) @@ -241,3 +290,54 @@ deprecationErrors = 1<<19, /// Stop compilation upon usage of deprecated features (-de) property = 1<<20, /// DEPRECATED: Enforce property syntax (-property) } + + struct BuildOptions { + import dub.internal.vibecompat.data.serialization : ignore; + + static if (__VERSION__ >= 2067) { + @ignore BitFlags!BuildOption values; + this(BuildOption opt) { values = opt; } + deprecated("Use BuildOption.* instead.") { + enum none = BuildOption.none; + enum debugMode = BuildOption.debugMode; + enum releaseMode = BuildOption.releaseMode; + enum coverage = BuildOption.coverage; + enum debugInfo = BuildOption.debugInfo; + enum debugInfoC = BuildOption.debugInfoC; + enum alwaysStackFrame = BuildOption.alwaysStackFrame; + enum stackStomping = BuildOption.stackStomping; + enum inline = BuildOption.inline; + enum noBoundsCheck = BuildOption.noBoundsCheck; + enum optimize = BuildOption.optimize; + enum profile = BuildOption.profile; + enum unittests = BuildOption.unittests; + enum verbose = BuildOption.verbose; + enum ignoreUnknownPragmas = BuildOption.ignoreUnknownPragmas; + enum syntaxOnly = BuildOption.syntaxOnly; + enum warnings = BuildOption.warnings; + enum warningsAsErrors = BuildOption.warningsAsErrors; + enum ignoreDeprecations = BuildOption.ignoreDeprecations; + enum deprecationWarnings = BuildOption.deprecationWarnings; + enum deprecationErrors = BuildOption.deprecationErrors; + enum property = BuildOption.property; + } + } else { + @ignore BuildOption values; + this(BuildOption opt) { values = opt; } + BuildOption[] toRepresentation() + const { + BuildOption[] ret; + for (int f = 1; f <= BuildOption.max; f *= 2) + if (values & f) ret ~= cast(BuildOption)f; + return ret; + } + static BuildOptions fromRepresentation(BuildOption[] v) + { + BuildOptions ret; + foreach (f; v) ret.values |= f; + return ret; + } + } + + alias values this; + } diff --git a/source/dub/compilers/compiler.d b/source/dub/compilers/compiler.d index 1f47a3d..32ffae3 100644 --- a/source/dub/compilers/compiler.d +++ b/source/dub/compilers/compiler.d @@ -106,22 +106,22 @@ ]; struct SpecialOption { - BuildOptions[] flags; + BuildOption[] flags; string alternative; } static immutable SpecialOption[] s_specialOptions = [ - {[BuildOptions.debugMode], "Call DUB with --build=debug"}, - {[BuildOptions.releaseMode], "Call DUB with --build=release"}, - {[BuildOptions.coverage], "Call DUB with --build=cov or --build=unittest-cov"}, - {[BuildOptions.debugInfo], "Call DUB with --build=debug"}, - {[BuildOptions.inline], "Call DUB with --build=release"}, - {[BuildOptions.noBoundsCheck], "Call DUB with --build=release-nobounds"}, - {[BuildOptions.optimize], "Call DUB with --build=release"}, - {[BuildOptions.profile], "Call DUB with --build=profile"}, - {[BuildOptions.unittests], "Call DUB with --build=unittest"}, - {[BuildOptions.warnings, BuildOptions.warningsAsErrors], "Use \"buildRequirements\" to control the warning level"}, - {[BuildOptions.ignoreDeprecations, BuildOptions.deprecationWarnings, BuildOptions.deprecationErrors], "Use \"buildRequirements\" to control the deprecation warning level"}, - {[BuildOptions.property], "This flag is deprecated and has no effect"} + {[BuildOption.debugMode], "Call DUB with --build=debug"}, + {[BuildOption.releaseMode], "Call DUB with --build=release"}, + {[BuildOption.coverage], "Call DUB with --build=cov or --build=unittest-cov"}, + {[BuildOption.debugInfo], "Call DUB with --build=debug"}, + {[BuildOption.inline], "Call DUB with --build=release"}, + {[BuildOption.noBoundsCheck], "Call DUB with --build=release-nobounds"}, + {[BuildOption.optimize], "Call DUB with --build=release"}, + {[BuildOption.profile], "Call DUB with --build=profile"}, + {[BuildOption.unittests], "Call DUB with --build=unittest"}, + {[BuildOption.warnings, BuildOption.warningsAsErrors], "Use \"buildRequirements\" to control the warning level"}, + {[BuildOption.ignoreDeprecations, BuildOption.deprecationWarnings, BuildOption.deprecationErrors], "Use \"buildRequirements\" to control the deprecation warning level"}, + {[BuildOption.property], "This flag is deprecated and has no effect"} ]; bool got_preamble = false; @@ -169,16 +169,16 @@ */ void enforceBuildRequirements(ref BuildSettings settings) { - settings.addOptions(BuildOptions.warningsAsErrors); - if (settings.requirements & BuildRequirements.allowWarnings) { settings.options &= ~BuildOptions.warningsAsErrors; settings.options |= BuildOptions.warnings; } - if (settings.requirements & BuildRequirements.silenceWarnings) settings.options &= ~(BuildOptions.warningsAsErrors|BuildOptions.warnings); - if (settings.requirements & BuildRequirements.disallowDeprecations) { settings.options &= ~(BuildOptions.ignoreDeprecations|BuildOptions.deprecationWarnings); settings.options |= BuildOptions.deprecationErrors; } - if (settings.requirements & BuildRequirements.silenceDeprecations) { settings.options &= ~(BuildOptions.deprecationErrors|BuildOptions.deprecationWarnings); settings.options |= BuildOptions.ignoreDeprecations; } - if (settings.requirements & BuildRequirements.disallowInlining) settings.options &= ~BuildOptions.inline; - if (settings.requirements & BuildRequirements.disallowOptimization) settings.options &= ~BuildOptions.optimize; - if (settings.requirements & BuildRequirements.requireBoundsCheck) settings.options &= ~BuildOptions.noBoundsCheck; - if (settings.requirements & BuildRequirements.requireContracts) settings.options &= ~BuildOptions.releaseMode; - if (settings.requirements & BuildRequirements.relaxProperties) settings.options &= ~BuildOptions.property; + settings.addOptions(BuildOption.warningsAsErrors); + if (settings.requirements & BuildRequirement.allowWarnings) { settings.options &= ~BuildOption.warningsAsErrors; settings.options |= BuildOption.warnings; } + if (settings.requirements & BuildRequirement.silenceWarnings) settings.options &= ~(BuildOption.warningsAsErrors|BuildOption.warnings); + if (settings.requirements & BuildRequirement.disallowDeprecations) { settings.options &= ~(BuildOption.ignoreDeprecations|BuildOption.deprecationWarnings); settings.options |= BuildOption.deprecationErrors; } + if (settings.requirements & BuildRequirement.silenceDeprecations) { settings.options &= ~(BuildOption.deprecationErrors|BuildOption.deprecationWarnings); settings.options |= BuildOption.ignoreDeprecations; } + if (settings.requirements & BuildRequirement.disallowInlining) settings.options &= ~BuildOption.inline; + if (settings.requirements & BuildRequirement.disallowOptimization) settings.options &= ~BuildOption.optimize; + if (settings.requirements & BuildRequirement.requireBoundsCheck) settings.options &= ~BuildOption.noBoundsCheck; + if (settings.requirements & BuildRequirement.requireContracts) settings.options &= ~BuildOption.releaseMode; + if (settings.requirements & BuildRequirement.relaxProperties) settings.options &= ~BuildOption.property; } diff --git a/source/dub/compilers/dmd.d b/source/dub/compilers/dmd.d index 1286c7f..e2fb472 100644 --- a/source/dub/compilers/dmd.d +++ b/source/dub/compilers/dmd.d @@ -25,27 +25,27 @@ class DmdCompiler : Compiler { private static immutable s_options = [ - tuple(BuildOptions.debugMode, ["-debug"]), - tuple(BuildOptions.releaseMode, ["-release"]), - tuple(BuildOptions.coverage, ["-cov"]), - tuple(BuildOptions.debugInfo, ["-g"]), - tuple(BuildOptions.debugInfoC, ["-gc"]), - tuple(BuildOptions.alwaysStackFrame, ["-gs"]), - tuple(BuildOptions.stackStomping, ["-gx"]), - tuple(BuildOptions.inline, ["-inline"]), - tuple(BuildOptions.noBoundsCheck, ["-noboundscheck"]), - tuple(BuildOptions.optimize, ["-O"]), - tuple(BuildOptions.profile, ["-profile"]), - tuple(BuildOptions.unittests, ["-unittest"]), - tuple(BuildOptions.verbose, ["-v"]), - tuple(BuildOptions.ignoreUnknownPragmas, ["-ignore"]), - tuple(BuildOptions.syntaxOnly, ["-o-"]), - tuple(BuildOptions.warnings, ["-wi"]), - tuple(BuildOptions.warningsAsErrors, ["-w"]), - tuple(BuildOptions.ignoreDeprecations, ["-d"]), - tuple(BuildOptions.deprecationWarnings, ["-dw"]), - tuple(BuildOptions.deprecationErrors, ["-de"]), - tuple(BuildOptions.property, ["-property"]), + tuple(BuildOption.debugMode, ["-debug"]), + tuple(BuildOption.releaseMode, ["-release"]), + tuple(BuildOption.coverage, ["-cov"]), + tuple(BuildOption.debugInfo, ["-g"]), + tuple(BuildOption.debugInfoC, ["-gc"]), + tuple(BuildOption.alwaysStackFrame, ["-gs"]), + tuple(BuildOption.stackStomping, ["-gx"]), + tuple(BuildOption.inline, ["-inline"]), + tuple(BuildOption.noBoundsCheck, ["-noboundscheck"]), + tuple(BuildOption.optimize, ["-O"]), + tuple(BuildOption.profile, ["-profile"]), + tuple(BuildOption.unittests, ["-unittest"]), + tuple(BuildOption.verbose, ["-v"]), + tuple(BuildOption.ignoreUnknownPragmas, ["-ignore"]), + tuple(BuildOption.syntaxOnly, ["-o-"]), + tuple(BuildOption.warnings, ["-wi"]), + tuple(BuildOption.warningsAsErrors, ["-w"]), + tuple(BuildOption.ignoreDeprecations, ["-d"]), + tuple(BuildOption.deprecationWarnings, ["-dw"]), + tuple(BuildOption.deprecationErrors, ["-de"]), + tuple(BuildOption.property, ["-property"]), ]; @property string name() const { return "dmd"; } diff --git a/source/dub/compilers/gdc.d b/source/dub/compilers/gdc.d index a82339c..04dbbc7 100644 --- a/source/dub/compilers/gdc.d +++ b/source/dub/compilers/gdc.d @@ -25,27 +25,27 @@ class GdcCompiler : Compiler { private static immutable s_options = [ - tuple(BuildOptions.debugMode, ["-fdebug"]), - tuple(BuildOptions.releaseMode, ["-frelease"]), - tuple(BuildOptions.coverage, ["-fprofile-arcs", "-ftest-coverage"]), - tuple(BuildOptions.debugInfo, ["-g"]), - tuple(BuildOptions.debugInfoC, ["-g", "-fdebug-c"]), - //tuple(BuildOptions.alwaysStackFrame, ["-X"]), - //tuple(BuildOptions.stackStomping, ["-X"]), - tuple(BuildOptions.inline, ["-finline-functions"]), - tuple(BuildOptions.noBoundsCheck, ["-fno-bounds-check"]), - tuple(BuildOptions.optimize, ["-O3"]), - tuple(BuildOptions.profile, ["-pg"]), - tuple(BuildOptions.unittests, ["-funittest"]), - tuple(BuildOptions.verbose, ["-fd-verbose"]), - tuple(BuildOptions.ignoreUnknownPragmas, ["-fignore-unknown-pragmas"]), - tuple(BuildOptions.syntaxOnly, ["-fsyntax-only"]), - tuple(BuildOptions.warnings, ["-Wall"]), - tuple(BuildOptions.warningsAsErrors, ["-Werror", "-Wall"]), - tuple(BuildOptions.ignoreDeprecations, ["-Wno-deprecated"]), - tuple(BuildOptions.deprecationWarnings, ["-Wdeprecated"]), - tuple(BuildOptions.deprecationErrors, ["-Werror", "-Wdeprecated"]), - tuple(BuildOptions.property, ["-fproperty"]), + tuple(BuildOption.debugMode, ["-fdebug"]), + tuple(BuildOption.releaseMode, ["-frelease"]), + tuple(BuildOption.coverage, ["-fprofile-arcs", "-ftest-coverage"]), + tuple(BuildOption.debugInfo, ["-g"]), + tuple(BuildOption.debugInfoC, ["-g", "-fdebug-c"]), + //tuple(BuildOption.alwaysStackFrame, ["-X"]), + //tuple(BuildOption.stackStomping, ["-X"]), + tuple(BuildOption.inline, ["-finline-functions"]), + tuple(BuildOption.noBoundsCheck, ["-fno-bounds-check"]), + tuple(BuildOption.optimize, ["-O3"]), + tuple(BuildOption.profile, ["-pg"]), + tuple(BuildOption.unittests, ["-funittest"]), + tuple(BuildOption.verbose, ["-fd-verbose"]), + tuple(BuildOption.ignoreUnknownPragmas, ["-fignore-unknown-pragmas"]), + tuple(BuildOption.syntaxOnly, ["-fsyntax-only"]), + tuple(BuildOption.warnings, ["-Wall"]), + tuple(BuildOption.warningsAsErrors, ["-Werror", "-Wall"]), + tuple(BuildOption.ignoreDeprecations, ["-Wno-deprecated"]), + tuple(BuildOption.deprecationWarnings, ["-Wdeprecated"]), + tuple(BuildOption.deprecationErrors, ["-Werror", "-Wdeprecated"]), + tuple(BuildOption.property, ["-fproperty"]), ]; @property string name() const { return "gdc"; } diff --git a/source/dub/compilers/ldc.d b/source/dub/compilers/ldc.d index 3dcfa54..35ae0f0 100644 --- a/source/dub/compilers/ldc.d +++ b/source/dub/compilers/ldc.d @@ -25,27 +25,27 @@ class LdcCompiler : Compiler { private static immutable s_options = [ - tuple(BuildOptions.debugMode, ["-d-debug"]), - tuple(BuildOptions.releaseMode, ["-release"]), - //tuple(BuildOptions.coverage, ["-?"]), - tuple(BuildOptions.debugInfo, ["-g"]), - tuple(BuildOptions.debugInfoC, ["-gc"]), - //tuple(BuildOptions.alwaysStackFrame, ["-?"]), - //tuple(BuildOptions.stackStomping, ["-?"]), - tuple(BuildOptions.inline, ["-enable-inlining"]), - tuple(BuildOptions.noBoundsCheck, ["-disable-boundscheck"]), - tuple(BuildOptions.optimize, ["-O"]), - //tuple(BuildOptions.profile, ["-?"]), - tuple(BuildOptions.unittests, ["-unittest"]), - tuple(BuildOptions.verbose, ["-v"]), - tuple(BuildOptions.ignoreUnknownPragmas, ["-ignore"]), - tuple(BuildOptions.syntaxOnly, ["-o-"]), - tuple(BuildOptions.warnings, ["-wi"]), - tuple(BuildOptions.warningsAsErrors, ["-w"]), - tuple(BuildOptions.ignoreDeprecations, ["-d"]), - tuple(BuildOptions.deprecationWarnings, ["-dw"]), - tuple(BuildOptions.deprecationErrors, ["-de"]), - tuple(BuildOptions.property, ["-property"]), + tuple(BuildOption.debugMode, ["-d-debug"]), + tuple(BuildOption.releaseMode, ["-release"]), + //tuple(BuildOption.coverage, ["-?"]), + tuple(BuildOption.debugInfo, ["-g"]), + tuple(BuildOption.debugInfoC, ["-gc"]), + //tuple(BuildOption.alwaysStackFrame, ["-?"]), + //tuple(BuildOption.stackStomping, ["-?"]), + tuple(BuildOption.inline, ["-enable-inlining"]), + tuple(BuildOption.noBoundsCheck, ["-disable-boundscheck"]), + tuple(BuildOption.optimize, ["-O"]), + //tuple(BuildOption.profile, ["-?"]), + tuple(BuildOption.unittests, ["-unittest"]), + tuple(BuildOption.verbose, ["-v"]), + tuple(BuildOption.ignoreUnknownPragmas, ["-ignore"]), + tuple(BuildOption.syntaxOnly, ["-o-"]), + tuple(BuildOption.warnings, ["-wi"]), + tuple(BuildOption.warningsAsErrors, ["-w"]), + tuple(BuildOption.ignoreDeprecations, ["-d"]), + tuple(BuildOption.deprecationWarnings, ["-dw"]), + tuple(BuildOption.deprecationErrors, ["-de"]), + tuple(BuildOption.property, ["-property"]), ]; @property string name() const { return "ldc"; } diff --git a/source/dub/dependency.d b/source/dub/dependency.d index a1cbc7c..7b88b57 100644 --- a/source/dub/dependency.d +++ b/source/dub/dependency.d @@ -500,6 +500,8 @@ m_version = vers; } + static Version fromString(string vers) { return Version(vers); } + bool opEquals(const Version oth) const { if (isUnknown || oth.isUnknown) { throw new Exception("Can't compare unknown versions! (this: %s, other: %s)".format(this, oth)); diff --git a/source/dub/description.d b/source/dub/description.d new file mode 100644 index 0000000..e958501 --- /dev/null +++ b/source/dub/description.d @@ -0,0 +1,106 @@ +/** + Types for project descriptions (dub describe). + + Copyright: © 2015 rejectedsoftware e.K. + License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. + Authors: Sönke Ludwig +*/ +module dub.description; + +import dub.compilers.buildsettings; +import dub.dependency; +import dub.internal.vibecompat.data.serialization; + + +/** + Describes a complete project for use in IDEs or build tools. + + The build settings will be specific to the compiler, platform + and configuration that has been selected. +*/ +struct ProjectDescription { + string rootPackage; + alias mainPackage = rootPackage; /// Compatibility alias + string configuration; + string buildType; + string compiler; + string[] architecture; + string[] platform; + PackageDescription[] packages; /// All packages in the dependency tree + TargetDescription[] targets; /// Build targets + @ignore size_t[string] targetLookup; /// Target index by name + + /// Targets by name + ref inout(TargetDescription) lookupTarget(string name) inout + { + return targets[ targetLookup[name] ]; + } +} + + +/** + Build settings and meta data of a single package. +*/ +struct PackageDescription { + string path; + string name; + Version version_; + string description; + string homepage; + string[] authors; + string copyright; + string license; + string[] dependencies; + + @byName TargetType targetType; + string targetPath; + string targetName; + string targetFileName; + string workingDirectory; + string mainSourceFile; + string[] dflags; + string[] lflags; + string[] libs; + string[] copyFiles; + string[] versions; + string[] debugVersions; + string[] importPaths; + string[] stringImportPaths; + string[] preGenerateCommands; + string[] postGenerateCommands; + string[] preBuildCommands; + string[] postBuildCommands; + @byName BuildRequirement[] buildRequirements; + @byName BuildOption[] options; + SourceFileDescription[] files; +} + +struct TargetDescription { + string rootPackage; + string[] packages; + string rootConfiguration; + BuildSettings buildSettings; + string[] dependencies; + string[] linkDependencies; +} + +/** + Description for a single source file. +*/ +struct SourceFileDescription { + @byName SourceFileRole role; + alias type = role; /// Compatibility alias + string path; +} + +/** + Determines +*/ +enum SourceFileRole { + unusedStringImport, + unusedImport, + unusedSource, + stringImport, + import_, + source +} diff --git a/source/dub/dub.d b/source/dub/dub.d index fba29ee..f3b5848 100644 --- a/source/dub/dub.d +++ b/source/dub/dub.d @@ -414,38 +414,52 @@ } /// Outputs a JSON description of the project, including its dependencies. - void describeProject(BuildPlatform platform, string config) + deprecated void describeProject(BuildPlatform platform, string config) { - auto dst = Json.emptyObject; - dst.configuration = config; - dst.compiler = platform.compiler; - dst.architecture = platform.architecture.serializeToJson(); - dst.platform = platform.platform.serializeToJson(); - - m_project.describe(dst, platform, config); - import std.stdio; - write(dst.toPrettyString()); + auto desc = m_project.describe(platform, config); + writeln(desc.serializeToPrettyJson()); } - void listImportPaths(BuildPlatform platform, string config) + void listImportPaths(BuildPlatform platform, string config, string buildType, bool nullDelim) { import std.stdio; - foreach(path; m_project.listImportPaths(platform, config)) { + foreach(path; m_project.listImportPaths(platform, config, buildType, nullDelim)) { writeln(path); } } - void listStringImportPaths(BuildPlatform platform, string config) + void listStringImportPaths(BuildPlatform platform, string config, string buildType, bool nullDelim) { import std.stdio; - foreach(path; m_project.listStringImportPaths(platform, config)) { + foreach(path; m_project.listStringImportPaths(platform, config, buildType, nullDelim)) { writeln(path); } } + void listProjectData(BuildPlatform platform, string config, string buildType, + string[] requestedData, Compiler formattingCompiler, bool nullDelim) + { + import std.stdio; + import std.ascii : newline; + + // Split comma-separated lists + string[] requestedDataSplit = + requestedData + .map!(a => a.splitter(",").map!strip) + .joiner() + .array(); + + auto data = m_project.listBuildSettings(platform, config, buildType, + requestedDataSplit, formattingCompiler, nullDelim); + + write( data.joiner(nullDelim? "\0" : newline) ); + if(!nullDelim) + writeln(); + } + /// Cleans intermediate/cache files of the given package void cleanPackage(Path path) { diff --git a/source/dub/generators/build.d b/source/dub/generators/build.d index ee949fe..e453047 100644 --- a/source/dub/generators/build.d +++ b/source/dub/generators/build.d @@ -82,7 +82,7 @@ { // run the generated executable auto buildsettings = targets[m_project.rootPackage.name].buildSettings; - if (settings.run && !(buildsettings.options & BuildOptions.syntaxOnly)) { + if (settings.run && !(buildsettings.options & BuildOption.syntaxOnly)) { auto exe_file_path = Path(buildsettings.targetPath) ~ getTargetFileName(buildsettings, settings.platform); runTarget(exe_file_path, buildsettings, settings.runArgs, settings); } @@ -91,7 +91,7 @@ private void buildTarget(GeneratorSettings settings, BuildSettings buildsettings, in Package pack, string config, in Package[] packages, in Path[] additional_dep_files) { auto cwd = Path(getcwd()); - bool generate_binary = !(buildsettings.options & BuildOptions.syntaxOnly); + bool generate_binary = !(buildsettings.options & BuildOption.syntaxOnly); auto build_id = computeBuildID(config, buildsettings, settings); @@ -134,7 +134,7 @@ } // determine basic build properties - auto generate_binary = !(buildsettings.options & BuildOptions.syntaxOnly); + auto generate_binary = !(buildsettings.options & BuildOption.syntaxOnly); logInfo("Building %s %s configuration \"%s\", build type %s.", pack.name, pack.vers, config, settings.buildType); @@ -223,7 +223,7 @@ { auto cwd = Path(getcwd()); - auto generate_binary = !(buildsettings.options & BuildOptions.syntaxOnly); + auto generate_binary = !(buildsettings.options & BuildOption.syntaxOnly); auto is_static_library = buildsettings.targetType == TargetType.staticLibrary || buildsettings.targetType == TargetType.library; // make file paths relative to shrink the command line @@ -373,7 +373,7 @@ void buildWithCompiler(GeneratorSettings settings, BuildSettings buildsettings) { - auto generate_binary = !(buildsettings.options & BuildOptions.syntaxOnly); + auto generate_binary = !(buildsettings.options & BuildOption.syntaxOnly); auto is_static_library = buildsettings.targetType == TargetType.staticLibrary || buildsettings.targetType == TargetType.library; Path target_file; diff --git a/source/dub/generators/generator.d b/source/dub/generators/generator.d index 5b8db64..ab16f72 100644 --- a/source/dub/generators/generator.d +++ b/source/dub/generators/generator.d @@ -108,7 +108,7 @@ foreach (pack; m_project.getTopologicalPackageList(true, null, configs)) { BuildSettings buildsettings; buildsettings.processVars(m_project, pack, pack.getBuildSettings(settings.platform, configs[pack.name]), true); - bool generate_binary = !(buildsettings.options & BuildOptions.syntaxOnly); + bool generate_binary = !(buildsettings.options & BuildOption.syntaxOnly); finalizeGeneration(pack.name, buildsettings, pack.path, Path(bs.targetPath), generate_binary); } @@ -219,7 +219,7 @@ m_project.addBuildTypeSettings(buildsettings, settings.platform, settings.buildType); settings.compiler.extractBuildOptions(buildsettings); - enforce (generates_binary || pack !is m_project.rootPackage || (buildsettings.options & BuildOptions.syntaxOnly), + enforce (generates_binary || pack !is m_project.rootPackage || (buildsettings.options & BuildOption.syntaxOnly), format("Main package must have a binary target type, not %s. Cannot build.", tt)); targets[pack.name].buildSettings = buildsettings.dup; diff --git a/source/dub/generators/targetdescription.d b/source/dub/generators/targetdescription.d new file mode 100644 index 0000000..da46b21 --- /dev/null +++ b/source/dub/generators/targetdescription.d @@ -0,0 +1,62 @@ +/** + Pseudo generator to output build descriptions. + + Copyright: © 2015 rejectedsoftware e.K. + License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. + Authors: Sönke Ludwig +*/ +module dub.generators.targetdescription; + +import dub.compilers.buildsettings; +import dub.compilers.compiler; +import dub.description; +import dub.generators.generator; +import dub.internal.vibecompat.inet.path; +import dub.project; + +class TargetDescriptionGenerator : ProjectGenerator { + TargetDescription[] targetDescriptions; + size_t[string] targetDescriptionLookup; + + this(Project project) + { + super(project); + } + + protected override void generateTargets(GeneratorSettings settings, in TargetInfo[string] targets) + { + auto configs = m_project.getPackageConfigs(settings.platform, settings.config); + targetDescriptions.length = targets.length; + size_t i = 0; + size_t rootIndex; + foreach (t; targets) { + if (t.pack.name == m_project.rootPackage.name) + rootIndex = i; + + TargetDescription d; + d.rootPackage = t.pack.name; + d.packages = t.packages.map!(p => p.name).array; + d.rootConfiguration = t.config; + d.buildSettings = t.buildSettings.dup; + d.dependencies = t.dependencies.dup; + d.linkDependencies = t.linkDependencies.dup; + + targetDescriptionLookup[d.rootPackage] = i; + targetDescriptions[i++] = d; + } + + // Add static library dependencies + auto bs = targetDescriptions[rootIndex].buildSettings; + foreach (ref desc; targetDescriptions) { + foreach (linkDepName; desc.linkDependencies) { + auto linkDepTarget = targetDescriptions[ targetDescriptionLookup[linkDepName] ]; + auto dbs = linkDepTarget.buildSettings; + if (bs.targetType != TargetType.staticLibrary) { + auto linkerFile = (Path(dbs.targetPath) ~ getTargetFileName(dbs, settings.platform)).toNativeString(); + bs.addLinkerFiles(linkerFile); + } + } + } + targetDescriptions[rootIndex].buildSettings = bs; + } +} diff --git a/source/dub/generators/visuald.d b/source/dub/generators/visuald.d index 49f35aa..ca64372 100644 --- a/source/dub/generators/visuald.d +++ b/source/dub/generators/visuald.d @@ -257,10 +257,10 @@ ret.formattedWrite(" \n", to!string(type), arch); // debug and optimize setting - ret.formattedWrite(" %s\n", buildsettings.options & BuildOptions.debugInfo ? "1" : "0"); - ret.formattedWrite(" %s\n", buildsettings.options & BuildOptions.optimize ? "1" : "0"); - ret.formattedWrite(" %s\n", buildsettings.options & BuildOptions.inline ? "1" : "0"); - ret.formattedWrite(" %s\n", buildsettings.options & BuildOptions.releaseMode ? "1" : "0"); + ret.formattedWrite(" %s\n", buildsettings.options & BuildOption.debugInfo ? "1" : "0"); + ret.formattedWrite(" %s\n", buildsettings.options & BuildOption.optimize ? "1" : "0"); + ret.formattedWrite(" %s\n", buildsettings.options & BuildOption.inline ? "1" : "0"); + ret.formattedWrite(" %s\n", buildsettings.options & BuildOption.releaseMode ? "1" : "0"); // Lib or exe? enum @@ -308,7 +308,7 @@ if (output_type != StaticLib) ret.formattedWrite(" %s %s\n", linkLibs, addLinkFiles); // Unittests - ret.formattedWrite(" %s\n", buildsettings.options & BuildOptions.unittests ? "1" : "0"); + ret.formattedWrite(" %s\n", buildsettings.options & BuildOption.unittests ? "1" : "0"); // compute directory for intermediate files (need dummy/ because of how -op determines the resulting path) size_t ndummy = 0; @@ -337,7 +337,7 @@ ret.put(" 0\n"); ret.put(" 0\n"); ret.put(" 0\n"); - ret.formattedWrite(" %s\n", buildsettings.options & BuildOptions.verbose ? "1" : "0"); + ret.formattedWrite(" %s\n", buildsettings.options & BuildOption.verbose ? "1" : "0"); ret.put(" 0\n"); ret.put(" 0\n"); ret.formattedWrite(" %s\n", arch == "x64" ? 1 : 0); @@ -353,18 +353,18 @@ ret.put(" 0\n"); ret.put(" 0\n"); ret.put(" 0\n"); - ret.formattedWrite(" %s\n", buildsettings.options & BuildOptions.noBoundsCheck ? "1" : "0"); + ret.formattedWrite(" %s\n", buildsettings.options & BuildOption.noBoundsCheck ? "1" : "0"); ret.put(" 0\n"); ret.put(" 1\n"); - ret.formattedWrite(" %s\n", buildsettings.options & BuildOptions.warningsAsErrors ? "1" : "0"); - ret.formattedWrite(" %s\n", buildsettings.options & BuildOptions.warnings ? "1" : "0"); - ret.formattedWrite(" %s\n", buildsettings.options & BuildOptions.property ? "1" : "0"); - ret.formattedWrite(" %s\n", buildsettings.options & BuildOptions.alwaysStackFrame ? "1" : "0"); + ret.formattedWrite(" %s\n", buildsettings.options & BuildOption.warningsAsErrors ? "1" : "0"); + ret.formattedWrite(" %s\n", buildsettings.options & BuildOption.warnings ? "1" : "0"); + ret.formattedWrite(" %s\n", buildsettings.options & BuildOption.property ? "1" : "0"); + ret.formattedWrite(" %s\n", buildsettings.options & BuildOption.alwaysStackFrame ? "1" : "0"); ret.put(" 0\n"); - ret.formattedWrite(" %s\n", buildsettings.options & BuildOptions.coverage ? "1" : "0"); + ret.formattedWrite(" %s\n", buildsettings.options & BuildOption.coverage ? "1" : "0"); ret.put(" 0\n"); ret.put(" 2\n"); - ret.formattedWrite(" %s\n", buildsettings.options & BuildOptions.ignoreUnknownPragmas ? "1" : "0"); + ret.formattedWrite(" %s\n", buildsettings.options & BuildOption.ignoreUnknownPragmas ? "1" : "0"); ret.formattedWrite(" %s\n", settings.compiler.name == "ldc" ? 2 : settings.compiler.name == "gdc" ? 1 : 0); ret.formattedWrite(" 0\n"); ret.formattedWrite(" %s\n", bin_path.toNativeString()); diff --git a/source/dub/internal/utils.d b/source/dub/internal/utils.d index d0cd482..9692d9a 100644 --- a/source/dub/internal/utils.d +++ b/source/dub/internal/utils.d @@ -21,6 +21,7 @@ import std.file; import std.process; import std.string; +import std.traits : isIntegral; import std.typecons; import std.zip; version(DubUseCurl) import std.net.curl; @@ -269,3 +270,33 @@ return strings.partition3!((a, b) => a.length + threshold < b.length)(input)[1] .schwartzSort!(p => levenshteinDistance(input.toUpper, p.toUpper)); } + +/** + If T is a bitfield-style enum, this function returns a string range + listing the names of all members included in the given value. + + Example: + --------- + enum Bits { + none = 0, + a = 1<<0, + b = 1<<1, + c = 1<<2, + a_c = a | c, + } + + assert( bitFieldNames(Bits.none).equals(["none"]) ); + assert( bitFieldNames(Bits.a).equals(["a"]) ); + assert( bitFieldNames(Bits.a_c).equals(["a", "c", "a_c"]) ); + --------- + */ +auto bitFieldNames(T)(T value) if(is(T==enum) && isIntegral!T) +{ + import std.algorithm : filter, map; + import std.conv : to; + import std.traits : EnumMembers; + + return [ EnumMembers!(T) ] + .filter!(member => member==0? value==0 : (value & member) == member) + .map!(member => to!string(member)); +} diff --git a/source/dub/internal/vibecompat/data/json.d b/source/dub/internal/vibecompat/data/json.d index 22e12f1..268f9d6 100644 --- a/source/dub/internal/vibecompat/data/json.d +++ b/source/dub/internal/vibecompat/data/json.d @@ -1,3 +1,14 @@ +/** + JSON serialization and value handling. + + This module provides the Json struct for reading, writing and manipulating + JSON values. De(serialization) of arbitrary D types is also supported and + is recommended for handling JSON in performance sensitive applications. + + Copyright: © 2012-2015 RejectedSoftware e.K. + License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. + Authors: Sönke Ludwig +*/ module dub.internal.vibecompat.data.json; version (Have_vibe_d) public import vibe.data.json; @@ -5,6 +16,10 @@ import dub.internal.vibecompat.data.utils; +public import dub.internal.vibecompat.data.serialization; + +public import std.json : JSONException; +import std.algorithm : equal, min; import std.array; import std.conv; import std.datetime; @@ -15,6 +30,7 @@ import std.traits; version = JsonLineNumbers; +version = VibeJsonFieldNames; /******************************************************************************/ @@ -28,52 +44,62 @@ behave mostly like values in ECMA script in the way that you can transparently perform operations on them. However, strict typechecking is done, so that operations between differently typed JSON values will throw - an exception. Additionally, an explicit cast or using get!() or to!() is + a JSONException. Additionally, an explicit cast or using get!() or to!() is required to convert a JSON value to the corresponding static D type. */ struct Json { private { - union { - bool m_bool; - long m_int; - double m_float; - string m_string; - Json[] m_array; - Json[string] m_object; - }; + // putting all fields in a union results in many false pointers leading to + // memory leaks and, worse, std.algorithm.swap triggering an assertion + // because of internal pointers. This crude workaround seems to fix + // the issues. + void*[2] m_data; + ref inout(T) getDataAs(T)() inout { static assert(T.sizeof <= m_data.sizeof); return *cast(inout(T)*)m_data.ptr; } + @property ref inout(long) m_int() inout { return getDataAs!long(); } + @property ref inout(double) m_float() inout { return getDataAs!double(); } + @property ref inout(bool) m_bool() inout { return getDataAs!bool(); } + @property ref inout(string) m_string() inout { return getDataAs!string(); } + @property ref inout(Json[string]) m_object() inout { return getDataAs!(Json[string])(); } + @property ref inout(Json[]) m_array() inout { return getDataAs!(Json[])(); } + Type m_type = Type.undefined; - uint m_magic = 0x1337f00d; // workaround for Appender bug - string m_name; + + version (VibeJsonFieldNames) { + uint m_magic = 0x1337f00d; // works around Appender bug (DMD BUG 10690/10859/11357) + string m_name; + string m_fileName; + } } /** Represents the run time type of a JSON value. */ enum Type { - /// A non-existent value in a JSON object - undefined, - /// Null value - null_, - /// Boolean value - bool_, - /// 64-bit integer value - int_, - /// 64-bit floating point value - float_, - /// UTF-8 string - string, - /// Array of JSON values - array, - /// JSON object aka. dictionary from string to Json - object + undefined, /// A non-existent value in a JSON object + null_, /// Null value + bool_, /// Boolean value + int_, /// 64-bit integer value + float_, /// 64-bit floating point value + string, /// UTF-8 string + array, /// Array of JSON values + object, /// JSON object aka. dictionary from string to Json + + Undefined = undefined, /// Compatibility alias - will be deprecated soon + Null = null_, /// Compatibility alias - will be deprecated soon + Bool = bool_, /// Compatibility alias - will be deprecated soon + Int = int_, /// Compatibility alias - will be deprecated soon + Float = float_, /// Compatibility alias - will be deprecated soon + String = string, /// Compatibility alias - will be deprecated soon + Array = array, /// Compatibility alias - will be deprecated soon + Object = object /// Compatibility alias - will be deprecated soon } - /// New JSON value of Type.undefined + /// New JSON value of Type.Undefined static @property Json undefined() { return Json(); } - /// New JSON value of Type.object + /// New JSON value of Type.Object static @property Json emptyObject() { return Json(cast(Json[string])null); } - /// New JSON value of Type.array + /// New JSON value of Type.Array static @property Json emptyArray() { return Json(cast(Json[])null); } version(JsonLineNumbers) int line; @@ -85,7 +111,17 @@ /// ditto this(bool v) { m_type = Type.bool_; m_bool = v; } /// ditto - this(int v) { m_type = Type.int_; m_int = v; } + this(byte v) { this(cast(long)v); } + /// ditto + this(ubyte v) { this(cast(long)v); } + /// ditto + this(short v) { this(cast(long)v); } + /// ditto + this(ushort v) { this(cast(long)v); } + /// ditto + this(int v) { this(cast(long)v); } + /// ditto + this(uint v) { this(cast(long)v); } /// ditto this(long v) { m_type = Type.int_; m_int = v; } /// ditto @@ -100,7 +136,8 @@ /** Allows assignment of D values to a JSON value. */ - ref Json opAssign(Json v){ + ref Json opAssign(Json v) + { m_type = v.m_type; final switch(m_type){ case Type.undefined: m_string = null; break; @@ -109,14 +146,8 @@ case Type.int_: m_int = v.m_int; break; case Type.float_: m_float = v.m_float; break; case Type.string: m_string = v.m_string; break; - case Type.array: - m_array = v.m_array; - if (m_magic == 0x1337f00d) { foreach (ref av; m_array) av.m_name = m_name; } else m_name = null; - break; - case Type.object: - m_object = v.m_object; - if (m_magic == 0x1337f00d) { foreach (k, ref av; m_object) av.m_name = m_name ~ "." ~ k; } else m_name = null; - break; + case Type.array: opAssign(v.m_array); break; + case Type.object: opAssign(v.m_object); break; } return this; } @@ -137,7 +168,7 @@ { m_type = Type.array; m_array = v; - if (m_magic == 0x1337f00d) foreach (ref av; m_array) av.m_name = m_name; + version (VibeJsonFieldNames) { if (m_magic == 0x1337f00d) { foreach (idx, ref av; m_array) av.m_name = format("%s[%s]", m_name, idx); } else m_name = null; } return v; } /// ditto @@ -145,44 +176,113 @@ { m_type = Type.object; m_object = v; - if (m_magic == 0x1337f00d) foreach (k, ref av; m_object) av.m_name = m_name ~ "." ~ k; + version (VibeJsonFieldNames) { if (m_magic == 0x1337f00d) { foreach (key, ref av; m_object) av.m_name = format("%s.%s", m_name, key); } else m_name = null; } return v; } /** + Allows removal of values from Type.Object Json objects. + */ + void remove(string item) { checkType!(Json[string])(); m_object.remove(item); } + + /** The current type id of this JSON object. */ @property Type type() const { return m_type; } /** + Clones a JSON value recursively. + */ + Json clone() + const { + final switch (m_type) { + case Type.undefined: return Json.undefined; + case Type.null_: return Json(null); + case Type.bool_: return Json(m_bool); + case Type.int_: return Json(m_int); + case Type.float_: return Json(m_float); + case Type.string: return Json(m_string); + case Type.array: + auto ret = Json.emptyArray; + foreach (v; this) ret ~= v.clone(); + return ret; + case Type.object: + auto ret = Json.emptyObject; + foreach (string name, v; this) ret[name] = v.clone(); + return ret; + } + } + + /** + Check whether the JSON object contains the given key and if yes, + return a pointer to the corresponding object, otherwise return `null`. + */ + inout(Json*) opBinaryRight(string op : "in")(string key) inout { + checkType!(Json[string])(); + return key in m_object; + } + + /** Allows direct indexing of array typed JSON values. */ ref inout(Json) opIndex(size_t idx) inout { checkType!(Json[])(); return m_array[idx]; } + /// + unittest { + Json value = Json.emptyArray; + value ~= 1; + value ~= true; + value ~= "foo"; + assert(value[0] == 1); + assert(value[1] == true); + assert(value[2] == "foo"); + } + + /** Allows direct indexing of object typed JSON values using a string as the key. */ - const(Json) opIndex(string key) const { + const(Json) opIndex(string key) + const { checkType!(Json[string])(); if( auto pv = key in m_object ) return *pv; Json ret = Json.undefined; ret.m_string = key; + version (VibeJsonFieldNames) ret.m_name = format("%s.%s", m_name, key); return ret; } /// ditto - ref Json opIndex(string key){ + ref Json opIndex(string key) + { checkType!(Json[string])(); if( auto pv = key in m_object ) return *pv; - m_object[key] = Json(); + if (m_object is null) { + m_object = ["": Json.init]; + m_object.remove(""); + } + m_object[key] = Json.init; + assert(m_object !is null); + assert(key in m_object, "Failed to insert key '"~key~"' into AA!?"); m_object[key].m_type = Type.undefined; // DMDBUG: AAs are teh $H1T!!!11 assert(m_object[key].type == Type.undefined); - m_object[key].m_name = m_name ~ "." ~ key; m_object[key].m_string = key; + version (VibeJsonFieldNames) m_object[key].m_name = format("%s.%s", m_name, key); return m_object[key]; } + /// + unittest { + Json value = Json.emptyObject; + value["a"] = 1; + value["b"] = true; + value["c"] = "foo"; + assert(value["a"] == 1); + assert(value["b"] == true); + assert(value["c"] == "foo"); + } + /** Returns a slice of a JSON array. */ @@ -191,16 +291,11 @@ inout(Json[]) opSlice(size_t from, size_t to) inout { checkType!(Json[])(); return m_array[from .. to]; } /** - Removes an entry from an object. - */ - void remove(string item) { checkType!(Json[string])(); m_object.remove(item); } - - /** Returns the number of entries of string, array or object typed JSON values. */ @property size_t length() const { - checkType!(string, Json[], Json[string]); + checkType!(string, Json[], Json[string])("property length"); switch(m_type){ case Type.string: return m_string.length; case Type.array: return m_array.length; @@ -214,7 +309,7 @@ */ int opApply(int delegate(ref Json obj) del) { - checkType!(Json[], Json[string]); + checkType!(Json[], Json[string])("opApply"); if( m_type == Type.array ){ foreach( ref v; m_array ) if( auto ret = del(v) ) @@ -231,7 +326,7 @@ /// ditto int opApply(int delegate(ref const Json obj) del) const { - checkType!(Json[], Json[string]); + checkType!(Json[], Json[string])("opApply"); if( m_type == Type.array ){ foreach( ref v; m_array ) if( auto ret = del(v) ) @@ -248,7 +343,7 @@ /// ditto int opApply(int delegate(ref size_t idx, ref Json obj) del) { - checkType!(Json[]); + checkType!(Json[])("opApply"); foreach( idx, ref v; m_array ) if( auto ret = del(idx, v) ) return ret; @@ -257,7 +352,7 @@ /// ditto int opApply(int delegate(ref size_t idx, ref const Json obj) del) const { - checkType!(Json[]); + checkType!(Json[])("opApply"); foreach( idx, ref v; m_array ) if( auto ret = del(idx, v) ) return ret; @@ -266,7 +361,7 @@ /// ditto int opApply(int delegate(ref string idx, ref Json obj) del) { - checkType!(Json[string]); + checkType!(Json[string])("opApply"); foreach( idx, ref v; m_object ) if( v.type != Type.undefined ) if( auto ret = del(idx, v) ) @@ -276,7 +371,7 @@ /// ditto int opApply(int delegate(ref string idx, ref const Json obj) del) const { - checkType!(Json[string]); + checkType!(Json[string])("opApply"); foreach( idx, ref v; m_object ) if( v.type != Type.undefined ) if( auto ret = del(idx, v) ) @@ -286,23 +381,46 @@ /** Converts the JSON value to the corresponding D type - types must match exactly. + + Available_Types: + $(UL + $(LI `bool` (`Type.bool_`)) + $(LI `double` (`Type.float_`)) + $(LI `float` (Converted from `double`)) + $(LI `long` (`Type.int_`)) + $(LI `ulong`, `int`, `uint`, `short`, `ushort`, `byte`, `ubyte` (Converted from `long`)) + $(LI `string` (`Type.string`)) + $(LI `Json[]` (`Type.array`)) + $(LI `Json[string]` (`Type.object`)) + ) + + See_Also: `opt`, `to`, `deserializeJson` */ inout(T) opCast(T)() inout { return get!T; } /// ditto @property inout(T) get(T)() inout { checkType!T(); - static if( is(T == bool) ) return m_bool; - else static if( is(T == double) ) return m_float; - else static if( is(T == float) ) return cast(T)m_float; - else static if( is(T == long) ) return m_int; - else static if( is(T : long) ){ enforce(m_int <= T.max && m_int >= T.min); return cast(T)m_int; } - else static if( is(T == string) ) return m_string; - else static if( is(T == Json[]) ) return m_array; - else static if( is(T == Json[string]) ) return m_object; - else static assert("JSON can only be casted to (bool, long, double, string, Json[] or Json[string]. Not "~T.stringof~"."); + static if (is(T == bool)) return m_bool; + else static if (is(T == double)) return m_float; + else static if (is(T == float)) return cast(T)m_float; + else static if (is(T == long)) return m_int; + else static if (is(T == ulong)) return cast(ulong)m_int; + else static if (is(T : long)){ enforceJson(m_int <= T.max && m_int >= T.min, "Integer conversion out of bounds error", m_fileName, line); return cast(T)m_int; } + else static if (is(T == string)) return m_string; + else static if (is(T == Json[])) return m_array; + else static if (is(T == Json[string])) return m_object; + else static assert("JSON can only be cast to (bool, long, double, string, Json[] or Json[string]. Not "~T.stringof~"."); } - /// ditto + + /** + Returns the native type for this JSON if it matches the current runtime type. + + If the runtime type does not match the given native type, the 'def' parameter is returned + instead. + + See_Also: `get` + */ @property const(T) opt(T)(const(T) def = T.init) const { if( typeId!T != m_type ) return def; @@ -316,7 +434,13 @@ } /** - Converts the JSON value to the corresponding D type - types are converted as neccessary. + Converts the JSON value to the corresponding D type - types are converted as necessary. + + Automatically performs conversions between strings and numbers. See + `get` for the list of available types. For converting/deserializing + JSON to complex data types see `deserializeJson`. + + See_Also: `get`, `deserializeJson` */ @property inout(T) to(T)() inout { @@ -391,7 +515,7 @@ default: return Json(["value": this]); case Type.object: return m_object; } - } else static assert("JSON can only be casted to (bool, long, double, string, Json[] or Json[string]. Not "~T.stringof~"."); + } else static assert("JSON can only be cast to (bool, long, double, string, Json[] or Json[string]. Not "~T.stringof~"."); } /** @@ -415,7 +539,7 @@ checkType!bool(); return Json(~m_bool); } else static if( op == "+" || op == "-" || op == "++" || op == "--" ){ - checkType!(long, double); + checkType!(long, double)("unary "~op); if( m_type == Type.int_ ) mixin("return Json("~op~"m_int);"); else if( m_type == Type.float_ ) mixin("return Json("~op~"m_float);"); else assert(false); @@ -425,7 +549,7 @@ /** Performs binary operations between JSON values. - The two JSON values must be of the same run time type or an exception + The two JSON values must be of the same run time type or a JSONException will be thrown. Only the operations listed are allowed for each of the types. @@ -436,46 +560,47 @@ $(DT Float) $(DD +, -, *, /, %) $(DT String) $(DD ~) $(DT Array) $(DD ~) - $(DT Object) $(DD none) + $(DT Object) $(DD in) ) */ Json opBinary(string op)(ref const(Json) other) const { - enforce(m_type == other.m_type, "Binary operation '"~op~"' between "~.to!string(m_type)~" and "~.to!string(other.m_type)~" JSON objects."); + enforceJson(m_type == other.m_type, "Binary operation '"~op~"' between "~.to!string(m_type)~" and "~.to!string(other.m_type)~" JSON objects."); static if( op == "&&" ){ - checkType!(bool)("'&&'"); other.checkType!(bool)("'&&'"); + checkType!(bool)(op); return Json(m_bool && other.m_bool); } else static if( op == "||" ){ - checkType!(bool)("'||'"); other.checkType!(bool)("'||'"); + checkType!(bool)(op); return Json(m_bool || other.m_bool); } else static if( op == "+" ){ - checkType!(double, long)("'+'"); other.checkType!(double, long)("'+'"); - if( m_type == Type.int_ ) return Json(m_int + other.m_int); + checkType!(long, double)(op); + if( m_type == Type.Int ) return Json(m_int + other.m_int); else if( m_type == Type.float_ ) return Json(m_float + other.m_float); else assert(false); } else static if( op == "-" ){ - checkType!(double, long)("'-'"); other.checkType!(double, long)("'-'"); - if( m_type == Type.int_ ) return Json(m_int - other.m_int); + checkType!(long, double)(op); + if( m_type == Type.Int ) return Json(m_int - other.m_int); else if( m_type == Type.float_ ) return Json(m_float - other.m_float); else assert(false); } else static if( op == "*" ){ - checkType!(double, long)("'*'"); other.checkType!(double, long)("'*'"); - if( m_type == Type.int_ ) return Json(m_int * other.m_int); + checkType!(long, double)(op); + if( m_type == Type.Int ) return Json(m_int * other.m_int); else if( m_type == Type.float_ ) return Json(m_float * other.m_float); else assert(false); } else static if( op == "/" ){ - checkType!(double, long)("'/'"); other.checkType!(double, long)("'/'"); - if( m_type == Type.int_ ) return Json(m_int / other.m_int); + checkType!(long, double)(op); + if( m_type == Type.Int ) return Json(m_int / other.m_int); else if( m_type == Type.float_ ) return Json(m_float / other.m_float); else assert(false); } else static if( op == "%" ){ - checkType!(double, long)("'%'"); other.checkType!(double, long)("'%'"); - if( m_type == Type.int_ ) return Json(m_int % other.m_int); + checkType!(long, double)(op); + if( m_type == Type.Int ) return Json(m_int % other.m_int); else if( m_type == Type.float_ ) return Json(m_float % other.m_float); else assert(false); } else static if( op == "~" ){ - checkType!(string)("'~'"); other.checkType!(string)("'~'"); + checkType!(string, Json[])(op); if( m_type == Type.string ) return Json(m_string ~ other.m_string); + else if (m_type == Type.array) return Json(m_array ~ other.m_array); else assert(false); } else static assert("Unsupported operator '"~op~"' for type JSON."); } @@ -484,7 +609,7 @@ if( op == "~" ) { static if( op == "~" ){ - checkType!(string, Json[])("'~'"); other.checkType!(string, Json[])("'~'"); + checkType!(string, Json[])(op); if( m_type == Type.string ) return Json(m_string ~ other.m_string); else if( m_type == Type.array ) return Json(m_array ~ other.m_array); else assert(false); @@ -492,35 +617,43 @@ } /// ditto void opOpAssign(string op)(Json other) - if( op == "+" || op == "-" || op == "*" ||op == "/" || op == "%" ) + if (op == "+" || op == "-" || op == "*" || op == "/" || op == "%" || op =="~") { - enforce(m_type == other.m_type, "Binary operation '"~op~"' between "~.to!string(m_type)~" and "~.to!string(other.m_type)~" JSON objects."); + enforceJson(m_type == other.m_type || op == "~" && m_type == Type.array, + "Binary operation '"~op~"=' between "~.to!string(m_type)~" and "~.to!string(other.m_type)~" JSON objects."); static if( op == "+" ){ if( m_type == Type.int_ ) m_int += other.m_int; else if( m_type == Type.float_ ) m_float += other.m_float; - else enforce(false, "'+' only allowed for scalar types, not "~.to!string(m_type)~"."); + else enforceJson(false, "'+=' only allowed for scalar types, not "~.to!string(m_type)~"."); } else static if( op == "-" ){ if( m_type == Type.int_ ) m_int -= other.m_int; else if( m_type == Type.float_ ) m_float -= other.m_float; - else enforce(false, "'-' only allowed for scalar types, not "~.to!string(m_type)~"."); + else enforceJson(false, "'-=' only allowed for scalar types, not "~.to!string(m_type)~"."); } else static if( op == "*" ){ if( m_type == Type.int_ ) m_int *= other.m_int; else if( m_type == Type.float_ ) m_float *= other.m_float; - else enforce(false, "'*' only allowed for scalar types, not "~.to!string(m_type)~"."); + else enforceJson(false, "'*=' only allowed for scalar types, not "~.to!string(m_type)~"."); } else static if( op == "/" ){ if( m_type == Type.int_ ) m_int /= other.m_int; else if( m_type == Type.float_ ) m_float /= other.m_float; - else enforce(false, "'/' only allowed for scalar types, not "~.to!string(m_type)~"."); + else enforceJson(false, "'/=' only allowed for scalar types, not "~.to!string(m_type)~"."); } else static if( op == "%" ){ if( m_type == Type.int_ ) m_int %= other.m_int; else if( m_type == Type.float_ ) m_float %= other.m_float; - else enforce(false, "'%' only allowed for scalar types, not "~.to!string(m_type)~"."); - } /*else static if( op == "~" ){ - if( m_type == Type.string ) m_string ~= other.m_string; - else if( m_type == Type.array ) m_array ~= other.m_array; - else enforce(false, "'%' only allowed for scalar types, not "~.to!string(m_type)~"."); - }*/ else static assert("Unsupported operator '"~op~"' for type JSON."); - assert(false); + else enforceJson(false, "'%=' only allowed for scalar types, not "~.to!string(m_type)~"."); + } else static if( op == "~" ){ + if (m_type == Type.string) m_string ~= other.m_string; + else if (m_type == Type.array) { + if (other.m_type == Type.array) m_array ~= other.m_array; + else appendArrayElement(other); + } else enforceJson(false, "'~=' only allowed for string and array types, not "~.to!string(m_type)~"."); + } else static assert("Unsupported operator '"~op~"=' for type JSON."); + } + /// ditto + void opOpAssign(string op, T)(T other) + if (!is(T == Json) && is(typeof(Json(other)))) + { + opOpAssign!op(Json(other)); } /// ditto Json opBinary(string op)(bool other) const { checkType!bool(); mixin("return Json(m_bool "~op~" other);"); } @@ -531,11 +664,7 @@ /// ditto Json opBinary(string op)(string other) const { checkType!string(); mixin("return Json(m_string "~op~" other);"); } /// ditto - Json opBinary(string op)(Json other) - if (op == "~") { - if (m_type == Type.array) return Json(m_array ~ other); - else return Json(this ~ other); - } + Json opBinary(string op)(Json[] other) { checkType!(Json[])(); mixin("return Json(m_array "~op~" other);"); } /// ditto Json opBinaryRight(string op)(bool other) const { checkType!bool(); mixin("return Json(other "~op~" m_bool);"); } /// ditto @@ -552,8 +681,20 @@ if( pv.type == Type.undefined ) return null; return pv; } + /// ditto + Json opBinaryRight(string op)(Json[] other) { checkType!(Json[])(); mixin("return Json(other "~op~" m_array);"); } /** + * The append operator will append arrays. This method always appends it's argument as an array element, so nested arrays can be created. + */ + void appendArrayElement(Json element) + { + enforceJson(m_type == Type.array, "'appendArrayElement' only allowed for array types, not "~.to!string(m_type)~"."); + m_array ~= element; + } + + /** Scheduled for deprecation, please use `opIndex` instead. + Allows to access existing fields of a JSON object using dot syntax. */ @property const(Json) opDispatch(string prop)() const { return opIndex(prop); } @@ -588,6 +729,8 @@ /// ditto bool opEquals(bool v) const { return m_type == Type.bool_ && m_bool == v; } /// ditto + bool opEquals(int v) const { return m_type == Type.int_ && m_int == v; } + /// ditto bool opEquals(long v) const { return m_type == Type.int_ && m_int == v; } /// ditto bool opEquals(double v) const { return m_type == Type.float_ && m_float == v; } @@ -616,12 +759,12 @@ case Type.string: return m_string < other.m_string ? -1 : m_string == other.m_string ? 0 : 1; case Type.array: return m_array < other.m_array ? -1 : m_array == other.m_array ? 0 : 1; case Type.object: - enforce(false, "JSON objects cannot be compared."); + enforceJson(false, "JSON objects cannot be compared."); assert(false); } } - + alias opDollar = length; /** Returns the type id corresponding to the given D type. @@ -667,7 +810,7 @@ --- Params: - level = Specifies the base amount of indentation for the output. Indentation is always + level = Specifies the base amount of indentation for the output. Indentation is always done using tab characters. See_Also: writePrettyJsonString, toString @@ -679,32 +822,34 @@ return ret.data; } - private void checkType(T...)(string op = null) + private void checkType(TYPES...)(string op = null) const { - bool found = false; - foreach (t; T) if (m_type == typeId!t) found = true; - if (found) return; - if (T.length == 1) { - throw new Exception(format("Got %s - expected %s.", this.displayName, typeId!(T[0]).to!string)); - } else { - string types; - foreach (t; T) { - if (types.length) types ~= ", "; - types ~= typeId!t.to!string; - } - throw new Exception(format("Got %s - expected one of %s.", this.displayName, types)); - } - } + bool matched = false; + foreach (T; TYPES) if (m_type == typeId!T) matched = true; + if (matched) return; - private @property string displayName() - const { - if (m_name.length) return m_name ~ " of type " ~ m_type.to!string(); - else return "JSON of type " ~ m_type.to!string(); + string name; + version (VibeJsonFieldNames) { + if (m_name.length) name = m_name ~ " of type " ~ m_type.to!string; + else name = "JSON of type " ~ m_type.to!string; + } else name = "JSON of type " ~ m_type.to!string; + + string expected; + static if (TYPES.length == 1) expected = typeId!(TYPES[0]).to!string; + else { + foreach (T; TYPES) { + if (expected.length > 0) expected ~= ", "; + expected ~= typeId!T.to!string; + } + } + + enforceJson(op.length > 0, format("Got %s, expected %s.", name, expected), m_fileName, line); + enforceJson(false, format("Got %s, expected %s for %s.", name, expected, op), m_fileName, line); } /*invariant() { - assert(m_type >= Type.undefined && m_type <= Type.object); + assert(m_type >= Type.Undefined && m_type <= Type.Object); }*/ } @@ -719,60 +864,59 @@ The range is shrunk during parsing, leaving any remaining text that is not part of the JSON contents. - Throws an Exception if any parsing error occured. + Throws a JSONException if any parsing error occured. */ -Json parseJson(R)(ref R range, string filename, int* line) +Json parseJson(R)(ref R range, int* line = null, string filename = null) if( is(R == string) ) { - import std.algorithm : min; - - assert(line !is null); Json ret; enforceJson(!range.empty, "JSON string is empty.", filename, 0); skipWhitespace(range, line); - version(JsonLineNumbers){ + version(JsonLineNumbers) { import dub.internal.vibecompat.core.log; int curline = line ? *line : 0; } switch( range.front ){ case 'f': - enforceJson(range[1 .. $].startsWith("alse"), "Expected 'false', got '"~range[0 .. min($, 5)]~"'.", filename, *line); + enforceJson(range[1 .. $].startsWith("alse"), "Expected 'false', got '"~range[0 .. min(5, $)]~"'.", filename, line); range.popFrontN(5); ret = false; break; case 'n': - enforceJson(range[1 .. $].startsWith("ull"), "Expected 'null', got '"~range[0 .. min($, 4)]~"'.", filename, *line); + enforceJson(range[1 .. $].startsWith("ull"), "Expected 'null', got '"~range[0 .. min(4, $)]~"'.", filename, line); range.popFrontN(4); ret = null; break; case 't': - enforceJson(range[1 .. $].startsWith("rue"), "Expected 'true', got '"~range[0 .. min($, 4)]~"'.", filename, *line); + enforceJson(range[1 .. $].startsWith("rue"), "Expected 'true', got '"~range[0 .. min(4, $)]~"'.", filename, line); range.popFrontN(4); ret = true; break; - case '0': .. case '9'+1: + case '0': .. case '9': case '-': bool is_float; - auto num = skipNumber(range, is_float, filename, *line); + auto num = skipNumber(range, is_float); if( is_float ) ret = to!double(num); else ret = to!long(num); break; case '\"': - ret = skipJsonString(range, filename, line); + ret = skipJsonString(range); break; case '[': Json[] arr; range.popFront(); - while(true) { + while (true) { skipWhitespace(range, line); - enforceJson(!range.empty, "Missing ']' before EOF.", filename, *line); + enforceJson(!range.empty, "Missing ']' before EOF.", filename, line); if(range.front == ']') break; - arr ~= parseJson(range, filename, line); + arr ~= parseJson(range, line, filename); skipWhitespace(range, line); - enforceJson(!range.empty && (range.front == ',' || range.front == ']'), "Expected ']' or ','.", filename, *line); + enforceJson(!range.empty, "Missing ']' before EOF.", filename, line); + enforceJson(range.front == ',' || range.front == ']', + format("Expected ']' or ',' - got '%s'.", range.front), filename, line); if( range.front == ']' ) break; else range.popFront(); } @@ -782,44 +926,49 @@ case '{': Json[string] obj; range.popFront(); - while(true) { + while (true) { skipWhitespace(range, line); - enforceJson(!range.empty, "Missing '}' before EOF.", filename, *line); + enforceJson(!range.empty, "Missing '}' before EOF.", filename, line); if(range.front == '}') break; - string key = skipJsonString(range, filename, line); + string key = skipJsonString(range); skipWhitespace(range, line); - enforceJson(range.startsWith(":"), "Expected ':' for key '" ~ key ~ "'", filename, *line); + enforceJson(range.startsWith(":"), "Expected ':' for key '" ~ key ~ "'", filename, line); range.popFront(); skipWhitespace(range, line); - Json itm = parseJson(range, filename, line); + Json itm = parseJson(range, line, filename); obj[key] = itm; skipWhitespace(range, line); - enforceJson(!range.empty && (range.front == ',' || range.front == '}'), "Expected '}' or ',' - got '"~range[0]~"'.", filename, *line); - if( range.front == '}' ) break; + enforceJson(!range.empty, "Missing '}' before EOF.", filename, line); + enforceJson(range.front == ',' || range.front == '}', + format("Expected '}' or ',' - got '%s'.", range.front), filename, line); + if (range.front == '}') break; else range.popFront(); } range.popFront(); ret = obj; break; default: - enforceJson(false, "Expected valid json token, got '"~range[0 .. min($, 12)]~"'.", filename, *line); + enforceJson(false, format("Expected valid JSON token, got '%s'.", range[0 .. min(12, $)]), filename, line); + assert(false); } assert(ret.type != Json.Type.undefined); version(JsonLineNumbers) ret.line = curline; + ret.m_fileName = filename; return ret; } /** Parses the given JSON string and returns the corresponding Json object. - Throws an Exception if any parsing error occurs. + Throws a JSONException if any parsing error occurs. */ Json parseJsonString(string str, string filename = null) { + auto strcopy = str; int line = 0; - auto ret = parseJson(str, filename, &line); - enforceJson(str.strip().length == 0, "Expected end of string after JSON value, not '"~str.strip()~"'.", filename, line); + auto ret = parseJson(strcopy, &line, filename); + enforceJson(strcopy.strip().length == 0, "Expected end of string after JSON value.", filename, line); return ret; } @@ -833,8 +982,20 @@ assert(parseJsonString("[1, 2, 3]") == Json([Json(1), Json(2), Json(3)])); assert(parseJsonString("{\"a\": 1}") == Json(["a": Json(1)])); assert(parseJsonString(`"\\\/\b\f\n\r\t\u1234"`).get!string == "\\/\b\f\n\r\t\u1234"); + auto json = parseJsonString(`{"hey": "This is @à test éhééhhéhéé !%/??*&?\ud83d\udcec"}`); + assert(json.toPrettyString() == parseJsonString(json.toPrettyString()).toPrettyString()); } +unittest { + try parseJsonString(`{"a": 1`); + catch (Exception e) assert(e.msg.endsWith("Missing '}' before EOF.")); + try parseJsonString(`{"a": 1 x`); + catch (Exception e) assert(e.msg.endsWith("Expected '}' or ',' - got 'x'.")); + try parseJsonString(`[1`); + catch (Exception e) assert(e.msg.endsWith("Missing ']' before EOF.")); + try parseJsonString(`[1 x`); + catch (Exception e) assert(e.msg.endsWith("Expected ']' or ',' - got 'x'.")); +} /** Serializes the given value to JSON. @@ -842,16 +1003,16 @@ The following types of values are supported: $(DL - $(DT Json) $(DD Used as-is) - $(DT null) $(DD Converted to Json.Type.null_) - $(DT bool) $(DD Converted to Json.Type.bool_) - $(DT float, double) $(DD Converted to Json.Type.Double) - $(DT short, ushort, int, uint, long, ulong) $(DD Converted to Json.Type.int_) - $(DT string) $(DD Converted to Json.Type.string) - $(DT T[]) $(DD Converted to Json.Type.array) - $(DT T[string]) $(DD Converted to Json.Type.object) - $(DT struct) $(DD Converted to Json.Type.object) - $(DT class) $(DD Converted to Json.Type.object or Json.Type.null_) + $(DT `Json`) $(DD Used as-is) + $(DT `null`) $(DD Converted to `Json.Type.null_`) + $(DT `bool`) $(DD Converted to `Json.Type.bool_`) + $(DT `float`, `double`) $(DD Converted to `Json.Type.float_`) + $(DT `short`, `ushort`, `int`, `uint`, `long`, `ulong`) $(DD Converted to `Json.Type.int_`) + $(DT `string`) $(DD Converted to `Json.Type.string`) + $(DT `T[]`) $(DD Converted to `Json.Type.array`) + $(DT `T[string]`) $(DD Converted to `Json.Type.object`) + $(DT `struct`) $(DD Converted to `Json.Type.object`) + $(DT `class`) $(DD Converted to `Json.Type.object` or `Json.Type.null_`) ) All entries of an array or an associative array, as well as all R/W properties and @@ -872,55 +1033,150 @@ --- The methods will have to be defined in pairs. The first pair that is implemented by - the type will be used for serialization (i.e. toJson overrides toString). + the type will be used for serialization (i.e. `toJson` overrides `toString`). + + See_Also: `deserializeJson`, `vibe.data.serialization` */ Json serializeToJson(T)(T value) { + version (VibeOldSerialization) { + return serializeToJsonOld(value); + } else { + return serialize!JsonSerializer(value); + } +} +/// ditto +void serializeToJson(R, T)(R destination, T value) + if (isOutputRange!(R, char) || isOutputRange!(R, ubyte)) +{ + serialize!(JsonStringSerializer!R)(value, destination); +} +/// ditto +string serializeToJsonString(T)(T value) +{ + auto ret = appender!string; + serializeToJson(ret, value); + return ret.data; +} + +/// +unittest { + struct Foo { + int number; + string str; + } + + Foo f; + f.number = 12; + f.str = "hello"; + + string json = serializeToJsonString(f); + assert(json == `{"number":12,"str":"hello"}`); + + Json jsonval = serializeToJson(f); + assert(jsonval.type == Json.Type.object); + assert(jsonval["number"] == Json(12)); + assert(jsonval["str"] == Json("hello")); +} + + +/** + Serializes the given value to a pretty printed JSON string. + + See_also: `serializeToJson`, `vibe.data.serialization` +*/ +void serializeToPrettyJson(R, T)(R destination, T value) + if (isOutputRange!(R, char) || isOutputRange!(R, ubyte)) +{ + serialize!(JsonStringSerializer!(R, true))(value, destination); +} +/// ditto +string serializeToPrettyJson(T)(T value) +{ + auto ret = appender!string; + serializeToPrettyJson(ret, value); + return ret.data; +} + +/// +unittest { + struct Foo { + int number; + string str; + } + + Foo f; + f.number = 12; + f.str = "hello"; + + string json = serializeToPrettyJson(f); + assert(json == +`{ + "number": 12, + "str": "hello" +}`); +} + + +/// private +Json serializeToJsonOld(T)(T value) +{ + import vibe.internal.meta.traits; + alias TU = Unqual!T; - static if( is(TU == Json) ) return value; - else static if( is(TU == typeof(null)) ) return Json(null); - else static if( is(TU == bool) ) return Json(value); - else static if( is(TU == float) ) return Json(cast(double)value); - else static if( is(TU == double) ) return Json(value); - else static if( is(TU == DateTime) ) return Json(value.toISOExtString()); - else static if( is(TU == SysTime) ) return Json(value.toISOExtString()); - else static if( is(TU : long) ) return Json(cast(long)value); - else static if( is(TU == string) ) return Json(value); - else static if( isArray!T ){ + static if (is(TU == Json)) return value; + else static if (is(TU == typeof(null))) return Json(null); + else static if (is(TU == bool)) return Json(value); + else static if (is(TU == float)) return Json(cast(double)value); + else static if (is(TU == double)) return Json(value); + else static if (is(TU == DateTime)) return Json(value.toISOExtString()); + else static if (is(TU == SysTime)) return Json(value.toISOExtString()); + else static if (is(TU == Date)) return Json(value.toISOExtString()); + else static if (is(TU : long)) return Json(cast(long)value); + else static if (is(TU : string)) return Json(value); + else static if (isArray!T) { auto ret = new Json[value.length]; - foreach( i; 0 .. value.length ) + foreach (i; 0 .. value.length) ret[i] = serializeToJson(value[i]); return Json(ret); - } else static if( isAssociativeArray!TU ){ + } else static if (isAssociativeArray!TU) { Json[string] ret; - foreach( string key, value; value ) - ret[key] = serializeToJson(value); + alias TK = KeyType!T; + foreach (key, value; value) { + static if(is(TK == string)) { + ret[key] = serializeToJson(value); + } else static if (is(TK == enum)) { + ret[to!string(key)] = serializeToJson(value); + } else static if (isStringSerializable!(TK)) { + ret[key.toString()] = serializeToJson(value); + } else static assert("AA key type %s not supported for JSON serialization."); + } return Json(ret); - } else static if( isJsonSerializable!TU ){ + } else static if (isJsonSerializable!TU) { return value.toJson(); - } else static if( isStringSerializable!TU ){ + } else static if (isStringSerializable!TU) { return Json(value.toString()); - } else static if( is(TU == struct) ){ + } else static if (is(TU == struct)) { Json[string] ret; - foreach( m; __traits(allMembers, T) ){ - static if( isRWField!(TU, m) ){ + foreach (m; __traits(allMembers, T)) { + static if (isRWField!(TU, m)) { auto mv = __traits(getMember, value, m); ret[underscoreStrip(m)] = serializeToJson(mv); } } return Json(ret); - } else static if( is(TU == class) ){ - if( value is null ) return Json(null); + } else static if(is(TU == class)) { + if (value is null) return Json(null); Json[string] ret; - foreach( m; __traits(allMembers, T) ){ - static if( isRWField!(TU, m) ){ + foreach (m; __traits(allMembers, T)) { + static if (isRWField!(TU, m)) { auto mv = __traits(getMember, value, m); ret[underscoreStrip(m)] = serializeToJson(mv); } } return Json(ret); - } else static if( isPointer!TU ){ - if( value is null ) return Json(null); + } else static if (isPointer!TU) { + if (value is null) return Json(null); return serializeToJson(*value); } else { static assert(false, "Unsupported type '"~T.stringof~"' for JSON serialization."); @@ -931,7 +1187,9 @@ /** Deserializes a JSON value into the destination variable. - The same types as for serializeToJson() are supported and handled inversely. + The same types as for `serializeToJson()` are supported and handled inversely. + + See_Also: `serializeToJson`, `serializeToJsonString`, `vibe.data.serialization` */ void deserializeJson(T)(ref T dst, Json src) { @@ -940,52 +1198,82 @@ /// ditto T deserializeJson(T)(Json src) { - static if( is(T == Json) ) return src; - else static if( is(T == typeof(null)) ){ return null; } - else static if( is(T == bool) ) return src.get!bool; - else static if( is(T == float) ) return src.to!float; // since doubles are frequently serialized without - else static if( is(T == double) ) return src.to!double; // a decimal point, we allow conversions here - else static if( is(T == DateTime) ) return DateTime.fromISOExtString(src.get!string); - else static if( is(T == SysTime) ) return SysTime.fromISOExtString(src.get!string); - else static if( is(T : long) ) return cast(T)src.get!long; - else static if( is(T == string) ) return src.get!string; - else static if( isArray!T ){ + version (VibeOldSerialization) { + return deserializeJsonOld!T(src); + } else { + return deserialize!(JsonSerializer, T)(src); + } +} +/// ditto +T deserializeJson(T, R)(R input) + if (isInputRange!R && !is(R == Json)) +{ + return deserialize!(JsonStringSerializer!R, T)(input); +} + +/// private +T deserializeJsonOld(T)(Json src) +{ + import vibe.internal.meta.traits; + + static if( is(T == struct) || isSomeString!T || isIntegral!T || isFloatingPoint!T ) + if( src.type == Json.Type.null_ ) return T.init; + static if (is(T == Json)) return src; + else static if (is(T == typeof(null))) { return null; } + else static if (is(T == bool)) return src.get!bool; + else static if (is(T == float)) return src.to!float; // since doubles are frequently serialized without + else static if (is(T == double)) return src.to!double; // a decimal point, we allow conversions here + else static if (is(T == DateTime)) return DateTime.fromISOExtString(src.get!string); + else static if (is(T == SysTime)) return SysTime.fromISOExtString(src.get!string); + else static if (is(T == Date)) return Date.fromISOExtString(src.get!string); + else static if (is(T : long)) return cast(T)src.get!long; + else static if (is(T : string)) return cast(T)src.get!string; + else static if (isArray!T) { alias TV = typeof(T.init[0]) ; auto dst = new Unqual!TV[src.length]; - foreach( size_t i, v; src ) + foreach (size_t i, v; src) dst[i] = deserializeJson!(Unqual!TV)(v); - return dst; - } else static if( isAssociativeArray!T ){ + return cast(T)dst; + } else static if( isAssociativeArray!T ) { alias TV = typeof(T.init.values[0]) ; - Unqual!TV[string] dst; - foreach( string key, value; src ) - dst[key] = deserializeJson!(Unqual!TV)(value); + alias TK = KeyType!T; + Unqual!TV[TK] dst; + foreach (string key, value; src) { + static if (is(TK == string)) { + dst[key] = deserializeJson!(Unqual!TV)(value); + } else static if (is(TK == enum)) { + dst[to!(TK)(key)] = deserializeJson!(Unqual!TV)(value); + } else static if (isStringSerializable!TK) { + auto dsk = TK.fromString(key); + dst[dsk] = deserializeJson!(Unqual!TV)(value); + } else static assert("AA key type %s not supported for JSON serialization."); + } return dst; - } else static if( isJsonSerializable!T ){ + } else static if (isJsonSerializable!T) { return T.fromJson(src); - } else static if( isStringSerializable!T ){ + } else static if (isStringSerializable!T) { return T.fromString(src.get!string); - } else static if( is(T == struct) ){ + } else static if (is(T == struct)) { T dst; - foreach( m; __traits(allMembers, T) ){ - static if( isRWPlainField!(T, m) || isRWField!(T, m) ){ + foreach (m; __traits(allMembers, T)) { + static if (isRWPlainField!(T, m) || isRWField!(T, m)) { alias TM = typeof(__traits(getMember, dst, m)) ; __traits(getMember, dst, m) = deserializeJson!TM(src[underscoreStrip(m)]); } } return dst; - } else static if( is(T == class) ){ - if( src.type == Json.Type.null_ ) return null; + } else static if (is(T == class)) { + if (src.type == Json.Type.null_) return null; auto dst = new T; - foreach( m; __traits(allMembers, T) ){ - static if( isRWPlainField!(T, m) || isRWField!(T, m) ){ + foreach (m; __traits(allMembers, T)) { + static if (isRWPlainField!(T, m) || isRWField!(T, m)) { alias TM = typeof(__traits(getMember, dst, m)) ; __traits(getMember, dst, m) = deserializeJson!TM(src[underscoreStrip(m)]); } } return dst; - } else static if( isPointer!T ){ - if( src.type == Json.Type.null_ ) return null; + } else static if (isPointer!T) { + if (src.type == Json.Type.null_) return null; alias TD = typeof(*T.init) ; dst = new TD; *dst = deserializeJson!TD(src); @@ -995,10 +1283,24 @@ } } +/// +unittest { + struct Foo { + int number; + string str; + } + + Foo f = deserializeJson!Foo(`{"number": 12, "str": "hello"}`); + assert(f.number == 12); + assert(f.str == "hello"); +} + unittest { import std.stdio; - static struct S { float a; double b; bool c; int d; string e; byte f; ubyte g; long h; ulong i; float[] j; } - immutable S t = {1.5, -3.0, true, int.min, "Test", -128, 255, long.min, ulong.max, [1.1, 1.2, 1.3]}; + enum Foo : string { k = "test" } + enum Boo : int { l = 5 } + static struct S { float a; double b; bool c; int d; string e; byte f; ubyte g; long h; ulong i; float[] j; Foo k; Boo l; } + immutable S t = {1.5, -3.0, true, int.min, "Test", -128, 255, long.min, ulong.max, [1.1, 1.2, 1.3], Foo.k, Boo.l}; S u; deserializeJson(u, serializeToJson(t)); assert(t.a == u.a); @@ -1011,6 +1313,49 @@ assert(t.h == u.h); assert(t.i == u.i); assert(t.j == u.j); + assert(t.k == u.k); + assert(t.l == u.l); +} + +unittest +{ + assert(uint.max == serializeToJson(uint.max).deserializeJson!uint); + assert(ulong.max == serializeToJson(ulong.max).deserializeJson!ulong); +} + +unittest { + static struct A { int value; static A fromJson(Json val) { return A(val.get!int); } Json toJson() const { return Json(value); } } + static struct C { int value; static C fromString(string val) { return C(val.to!int); } string toString() const { return value.to!string; } } + static struct D { int value; } + + assert(serializeToJson(const A(123)) == Json(123)); + assert(serializeToJson(A(123)) == Json(123)); + assert(serializeToJson(const C(123)) == Json("123")); + assert(serializeToJson(C(123)) == Json("123")); + assert(serializeToJson(const D(123)) == serializeToJson(["value": 123])); + assert(serializeToJson(D(123)) == serializeToJson(["value": 123])); +} + +unittest { + auto d = Date(2001,1,1); + deserializeJson(d, serializeToJson(Date.init)); + assert(d == Date.init); + deserializeJson(d, serializeToJson(Date(2001,1,1))); + assert(d == Date(2001,1,1)); + struct S { immutable(int)[] x; } + S s; + deserializeJson(s, serializeToJson(S([1,2,3]))); + assert(s == S([1,2,3])); + struct T { + @optional S s; + @optional int i; + @optional float f_; // underscore strip feature + @optional double d; + @optional string str; + } + auto t = T(S([1,2,3])); + deserializeJson(t, parseJsonString(`{ "s" : null, "i" : null, "f" : null, "d" : null, "str" : null }`)); + assert(text(t) == text(T())); } unittest { @@ -1034,13 +1379,357 @@ assert(c.b == d.b); } +unittest { + static struct C { int value; static C fromString(string val) { return C(val.to!int); } string toString() const { return value.to!string; } } + enum Color { Red, Green, Blue } + { + static class T { + string[Color] enumIndexedMap; + string[C] stringableIndexedMap; + this() { + enumIndexedMap = [ Color.Red : "magenta", Color.Blue : "deep blue" ]; + stringableIndexedMap = [ C(42) : "forty-two" ]; + } + } + + T original = new T; + original.enumIndexedMap[Color.Green] = "olive"; + T other; + deserializeJson(other, serializeToJson(original)); + assert(serializeToJson(other) == serializeToJson(original)); + } + { + static struct S { + string[Color] enumIndexedMap; + string[C] stringableIndexedMap; + } + + S *original = new S; + original.enumIndexedMap = [ Color.Red : "magenta", Color.Blue : "deep blue" ]; + original.enumIndexedMap[Color.Green] = "olive"; + original.stringableIndexedMap = [ C(42) : "forty-two" ]; + S other; + deserializeJson(other, serializeToJson(original)); + assert(serializeToJson(other) == serializeToJson(original)); + } +} + +unittest { + import std.typecons : Nullable; + + struct S { Nullable!int a, b; } + S s; + s.a = 2; + + auto j = serializeToJson(s); + assert(j.a.type == Json.Type.int_); + assert(j.b.type == Json.Type.null_); + + auto t = deserializeJson!S(j); + assert(!t.a.isNull() && t.a == 2); + assert(t.b.isNull()); +} + +unittest { // #840 + int[2][2] nestedArray = 1; + assert(nestedArray.serializeToJson.deserializeJson!(typeof(nestedArray)) == nestedArray); +} + + +/** + Serializer for a plain Json representation. + + See_Also: vibe.data.serialization.serialize, vibe.data.serialization.deserialize, serializeToJson, deserializeJson +*/ +struct JsonSerializer { + template isJsonBasicType(T) { enum isJsonBasicType = isNumeric!T || isBoolean!T || is(T == string) || is(T == typeof(null)) || isJsonSerializable!T; } + + template isSupportedValueType(T) { enum isSupportedValueType = isJsonBasicType!T || is(T == Json); } + + private { + Json m_current; + Json[] m_compositeStack; + } + + this(Json data) { m_current = data; } + + @disable this(this); + + // + // serialization + // + Json getSerializedResult() { return m_current; } + void beginWriteDictionary(T)() { m_compositeStack ~= Json.emptyObject; } + void endWriteDictionary(T)() { m_current = m_compositeStack[$-1]; m_compositeStack.length--; } + void beginWriteDictionaryEntry(T)(string name) {} + void endWriteDictionaryEntry(T)(string name) { m_compositeStack[$-1][name] = m_current; } + + void beginWriteArray(T)(size_t) { m_compositeStack ~= Json.emptyArray; } + void endWriteArray(T)() { m_current = m_compositeStack[$-1]; m_compositeStack.length--; } + void beginWriteArrayEntry(T)(size_t) {} + void endWriteArrayEntry(T)(size_t) { m_compositeStack[$-1].appendArrayElement(m_current); } + + void writeValue(T)(T value) + { + static if (is(T == Json)) m_current = value; + else static if (isJsonSerializable!T) m_current = value.toJson(); + else m_current = Json(value); + } + + void writeValue(T)(in Json value) if (is(T == Json)) + { + m_current = value.clone; + } + + // + // deserialization + // + void readDictionary(T)(scope void delegate(string) field_handler) + { + enforceJson(m_current.type == Json.Type.object, "Expected JSON object, got "~m_current.type.to!string); + auto old = m_current; + foreach (string key, value; m_current) { + m_current = value; + field_handler(key); + } + m_current = old; + } + + void readArray(T)(scope void delegate(size_t) size_callback, scope void delegate() entry_callback) + { + enforceJson(m_current.type == Json.Type.array, "Expected JSON array, got "~m_current.type.to!string); + auto old = m_current; + size_callback(m_current.length); + foreach (ent; old) { + m_current = ent; + entry_callback(); + } + m_current = old; + } + + T readValue(T)() + { + static if (is(T == Json)) return m_current; + else static if (isJsonSerializable!T) return T.fromJson(m_current); + else static if (is(T == float) || is(T == double)) { + if (m_current.type == Json.Type.undefined) return T.nan; + return m_current.type == Json.Type.float_ ? cast(T)m_current.get!double : cast(T)m_current.get!long; + } + else { + return m_current.get!T(); + } + } + + bool tryReadNull() { return m_current.type == Json.Type.null_; } +} + + +/** + Serializer for a range based plain JSON string representation. + + See_Also: vibe.data.serialization.serialize, vibe.data.serialization.deserialize, serializeToJson, deserializeJson +*/ +struct JsonStringSerializer(R, bool pretty = false) + if (isInputRange!R || isOutputRange!(R, char)) +{ + private { + R m_range; + size_t m_level = 0; + } + + template isJsonBasicType(T) { enum isJsonBasicType = isNumeric!T || isBoolean!T || is(T == string) || is(T == typeof(null)) || isJsonSerializable!T; } + + template isSupportedValueType(T) { enum isSupportedValueType = isJsonBasicType!T || is(T == Json); } + + this(R range) + { + m_range = range; + } + + @disable this(this); + + // + // serialization + // + static if (isOutputRange!(R, char)) { + private { + bool m_firstInComposite; + } + + void getSerializedResult() {} + + void beginWriteDictionary(T)() { startComposite(); m_range.put('{'); } + void endWriteDictionary(T)() { endComposite(); m_range.put("}"); } + void beginWriteDictionaryEntry(T)(string name) + { + startCompositeEntry(); + m_range.put('"'); + m_range.jsonEscape(name); + static if (pretty) m_range.put(`": `); + else m_range.put(`":`); + } + void endWriteDictionaryEntry(T)(string name) {} + + void beginWriteArray(T)(size_t) { startComposite(); m_range.put('['); } + void endWriteArray(T)() { endComposite(); m_range.put(']'); } + void beginWriteArrayEntry(T)(size_t) { startCompositeEntry(); } + void endWriteArrayEntry(T)(size_t) {} + + void writeValue(T)(in T value) + { + static if (is(T == typeof(null))) m_range.put("null"); + else static if (is(T == bool)) m_range.put(value ? "true" : "false"); + else static if (is(T : long)) m_range.formattedWrite("%s", value); + else static if (is(T : real)) m_range.formattedWrite("%.16g", value); + else static if (is(T == string)) { + m_range.put('"'); + m_range.jsonEscape(value); + m_range.put('"'); + } + else static if (is(T == Json)) m_range.writeJsonString(value); + else static if (isJsonSerializable!T) m_range.writeJsonString!(R, pretty)(value.toJson(), m_level); + else static assert(false, "Unsupported type: " ~ T.stringof); + } + + private void startComposite() + { + static if (pretty) m_level++; + m_firstInComposite = true; + } + + private void startCompositeEntry() + { + if (!m_firstInComposite) { + m_range.put(','); + } else { + m_firstInComposite = false; + } + static if (pretty) indent(); + } + + private void endComposite() + { + static if (pretty) { + m_level--; + if (!m_firstInComposite) indent(); + } + m_firstInComposite = false; + } + + private void indent() + { + m_range.put('\n'); + foreach (i; 0 .. m_level) m_range.put('\t'); + } + } + + // + // deserialization + // + static if (isInputRange!(R)) { + private { + int m_line = 0; + } + + void readDictionary(T)(scope void delegate(string) entry_callback) + { + m_range.skipWhitespace(&m_line); + enforceJson(!m_range.empty && m_range.front == '{', "Expecting object."); + m_range.popFront(); + bool first = true; + while(true) { + m_range.skipWhitespace(&m_line); + enforceJson(!m_range.empty, "Missing '}'."); + if (m_range.front == '}') { + m_range.popFront(); + break; + } else if (!first) { + enforceJson(m_range.front == ',', "Expecting ',' or '}', not '"~m_range.front.to!string~"'."); + m_range.popFront(); + m_range.skipWhitespace(&m_line); + } else first = false; + + auto name = m_range.skipJsonString(&m_line); + + m_range.skipWhitespace(&m_line); + enforceJson(!m_range.empty && m_range.front == ':', "Expecting ':', not '"~m_range.front.to!string~"'."); + m_range.popFront(); + + entry_callback(name); + } + } + + void readArray(T)(scope void delegate(size_t) size_callback, scope void delegate() entry_callback) + { + m_range.skipWhitespace(&m_line); + enforceJson(!m_range.empty && m_range.front == '[', "Expecting array."); + m_range.popFront(); + bool first = true; + while(true) { + m_range.skipWhitespace(&m_line); + enforceJson(!m_range.empty, "Missing ']'."); + if (m_range.front == ']') { + m_range.popFront(); + break; + } else if (!first) { + enforceJson(m_range.front == ',', "Expecting ',' or ']'."); + m_range.popFront(); + } else first = false; + + entry_callback(); + } + } + + T readValue(T)() + { + m_range.skipWhitespace(&m_line); + static if (is(T == typeof(null))) { enforceJson(m_range.take(4).equal("null"), "Expecting 'null'."); return null; } + else static if (is(T == bool)) { + bool ret = m_range.front == 't'; + string expected = ret ? "true" : "false"; + foreach (ch; expected) { + enforceJson(m_range.front == ch, "Expecting 'true' or 'false'."); + m_range.popFront(); + } + return ret; + } else static if (is(T : long)) { + bool is_float; + auto num = m_range.skipNumber(is_float); + enforceJson(!is_float, "Expecting integer number."); + return to!T(num); + } else static if (is(T : real)) { + bool is_float; + auto num = m_range.skipNumber(is_float); + return to!T(num); + } + else static if (is(T == string)) return m_range.skipJsonString(&m_line); + else static if (is(T == Json)) return m_range.parseJson(&m_line); + else static if (isJsonSerializable!T) return T.fromJson(m_range.parseJson(&m_line)); + else static assert(false, "Unsupported type: " ~ T.stringof); + } + + bool tryReadNull() + { + m_range.skipWhitespace(&m_line); + if (m_range.front != 'n') return false; + foreach (ch; "null") { + enforceJson(m_range.front == ch, "Expecting 'null'."); + m_range.popFront(); + } + assert(m_range.empty || m_range.front != 'l'); + return true; + } + } +} + + /** Writes the given JSON object as a JSON string into the destination range. This function will convert the given JSON value to a string without adding any white space between tokens (no newlines, no indentation and no padding). - The output size is thus minizized, at the cost of bad human readability. + The output size is thus minimized, at the cost of bad human readability. Params: dst = References the string output range to which the result is written. @@ -1048,7 +1737,7 @@ See_Also: Json.toString, writePrettyJsonString */ -void writeJsonString(R)(ref R dst, in Json json) +void writeJsonString(R, bool pretty = false)(ref R dst, in Json json, size_t level = 0) // if( isOutputRange!R && is(ElementEncodingType!R == char) ) { final switch( json.type ){ @@ -1056,40 +1745,125 @@ case Json.Type.null_: dst.put("null"); break; case Json.Type.bool_: dst.put(cast(bool)json ? "true" : "false"); break; case Json.Type.int_: formattedWrite(dst, "%d", json.get!long); break; - case Json.Type.float_: formattedWrite(dst, "%.16g", json.get!double); break; + case Json.Type.float_: + auto d = json.get!double; + if (d != d) + dst.put("undefined"); // JSON has no NaN value so set null + else + formattedWrite(dst, "%.16g", json.get!double); + break; case Json.Type.string: - dst.put("\""); + dst.put('\"'); jsonEscape(dst, cast(string)json); - dst.put("\""); + dst.put('\"'); break; case Json.Type.array: - dst.put("["); + dst.put('['); bool first = true; - foreach( ref const Json e; json ){ - if( e.type == Json.Type.undefined ) continue; + foreach (ref const Json e; json) { if( !first ) dst.put(","); first = false; - writeJsonString(dst, e); + static if (pretty) { + dst.put('\n'); + foreach (tab; 0 .. level+1) dst.put('\t'); + } + if (e.type == Json.Type.undefined) dst.put("null"); + else writeJsonString!(R, pretty)(dst, e, level+1); } - dst.put("]"); + static if (pretty) { + if (json.length > 0) { + dst.put('\n'); + foreach (tab; 0 .. level) dst.put('\t'); + } + } + dst.put(']'); break; case Json.Type.object: - dst.put("{"); + dst.put('{'); bool first = true; foreach( string k, ref const Json e; json ){ if( e.type == Json.Type.undefined ) continue; - if( !first ) dst.put(","); + if( !first ) dst.put(','); first = false; - dst.put("\""); + static if (pretty) { + dst.put('\n'); + foreach (tab; 0 .. level+1) dst.put('\t'); + } + dst.put('\"'); jsonEscape(dst, k); - dst.put("\":"); - writeJsonString(dst, e); + dst.put(pretty ? `": ` : `":`); + writeJsonString!(R, pretty)(dst, e, level+1); } - dst.put("}"); + static if (pretty) { + if (json.length > 0) { + dst.put('\n'); + foreach (tab; 0 .. level) dst.put('\t'); + } + } + dst.put('}'); break; } } +unittest { + auto a = Json.emptyObject; + a.a = Json.emptyArray; + a.b = Json.emptyArray; + a.b ~= Json(1); + a.b ~= Json.emptyObject; + + assert(a.toString() == `{"a":[],"b":[1,{}]}`); + assert(a.toPrettyString() == +`{ + "a": [], + "b": [ + 1, + {} + ] +}`); +} + +unittest { // #735 + auto a = Json.emptyArray; + a ~= "a"; + a ~= Json(); + a ~= "b"; + a ~= null; + a ~= "c"; + assert(a.toString() == `["a",null,"b",null,"c"]`); +} + +unittest { + auto a = Json.emptyArray; + a ~= Json(1); + a ~= Json(2); + a ~= Json(3); + a ~= Json(4); + a ~= Json(5); + + auto b = Json(a[0..a.length]); + assert(a == b); + + auto c = Json(a[0..$]); + assert(a == c); + assert(b == c); + + auto d = [Json(1),Json(2),Json(3)]; + assert(d == a[0..a.length-2]); + assert(d == a[0..$-2]); +} + +unittest { + auto j = Json(double.init); + + assert(j.toString == "undefined"); // A double nan should serialize to undefined + j = 17.04f; + assert(j.toString == "17.04"); // A proper double should serialize correctly + + double d; + deserializeJson(d, Json.undefined); // Json.undefined should deserialize to nan + assert(d != d); +} /** Writes the given JSON object as a prettified JSON string into the destination range. @@ -1098,71 +1872,74 @@ Params: dst = References the string output range to which the result is written. json = Specifies the JSON value that is to be stringified. - level = Specifies the base amount of indentation for the output. Indentation is always - done using tab characters. + level = Specifies the base amount of indentation for the output. Indentation is always + done using tab characters. See_Also: Json.toPrettyString, writeJsonString */ void writePrettyJsonString(R)(ref R dst, in Json json, int level = 0) // if( isOutputRange!R && is(ElementEncodingType!R == char) ) { - final switch( json.type ){ - case Json.Type.undefined: dst.put("undefined"); break; - case Json.Type.null_: dst.put("null"); break; - case Json.Type.bool_: dst.put(cast(bool)json ? "true" : "false"); break; - case Json.Type.int_: formattedWrite(dst, "%d", json.get!long); break; - case Json.Type.float_: formattedWrite(dst, "%.16g", json.get!double); break; - case Json.Type.string: - dst.put("\""); - jsonEscape(dst, cast(string)json); - dst.put("\""); - break; - case Json.Type.array: - dst.put("["); - bool first = true; - foreach( e; json ){ - if( e.type == Json.Type.undefined ) continue; - if( !first ) dst.put(","); - first = false; - dst.put("\n"); - foreach( tab; 0 .. level+1 ) dst.put('\t'); - writePrettyJsonString(dst, e, level+1); - } - if( json.length > 0 ) { - dst.put('\n'); - foreach( tab; 0 .. level ) dst.put('\t'); - } - dst.put("]"); - break; - case Json.Type.object: - dst.put("{"); - bool first = true; - foreach( string k, e; json ){ - if( e.type == Json.Type.undefined ) continue; - if( !first ) dst.put(","); - dst.put("\n"); - first = false; - foreach( tab; 0 .. level+1 ) dst.put('\t'); - dst.put("\""); - jsonEscape(dst, k); - dst.put("\": "); - writePrettyJsonString(dst, e, level+1); - } - if( json.length > 0 ) { - dst.put('\n'); - foreach( tab; 0 .. level ) dst.put('\t'); - } - dst.put("}"); - break; - } + writeJsonString!(R, true)(dst, json, level); } -/// private -private void jsonEscape(R)(ref R dst, string s) + +/** + Helper function that escapes all Unicode characters in a JSON string. +*/ +string convertJsonToASCII(string json) { - foreach( ch; s ){ - switch(ch){ - default: dst.put(ch); break; + auto ret = appender!string; + jsonEscape!true(ret, json); + return ret.data; +} + + +/// private +private void jsonEscape(bool escape_unicode = false, R)(ref R dst, string s) +{ + for (size_t pos = 0; pos < s.length; pos++) { + immutable(char) ch = s[pos]; + + switch (ch) { + default: + static if (escape_unicode) { + if (ch > 0x20 && ch < 0x80) dst.put(ch); + else { + import std.utf : decode; + char[13] buf; + int len; + dchar codepoint = decode(s, pos); + import std.c.stdio : sprintf; + /* codepoint is in BMP */ + if(codepoint < 0x10000) + { + sprintf(&buf[0], "\\u%04X", codepoint); + len = 6; + } + /* not in BMP -> construct a UTF-16 surrogate pair */ + else + { + int first, last; + + codepoint -= 0x10000; + first = 0xD800 | ((codepoint & 0xffc00) >> 10); + last = 0xDC00 | (codepoint & 0x003ff); + + sprintf(&buf[0], "\\u%04X\\u%04X", first, last); + len = 12; + } + + pos -= 1; + foreach (i; 0 .. len) + dst.put(buf[i]); + + } + } else { + if (ch < 0x20) dst.formattedWrite("\\u%04X", ch); + else dst.put(ch); + } + break; case '\\': dst.put("\\\\"); break; case '\r': dst.put("\\r"); break; case '\n': dst.put("\\n"); break; @@ -1182,9 +1959,9 @@ case '"': return ret.data; case '\\': range.popFront(); - enforce(!range.empty, "Unterminated string escape sequence."); + enforceJson(!range.empty, "Unterminated string escape sequence."); switch(range.front){ - default: enforce("Invalid string escape sequence."); break; + default: enforceJson(false, "Invalid string escape sequence."); break; case '"': ret.put('\"'); range.popFront(); break; case '\\': ret.put('\\'); range.popFront(); break; case '/': ret.put('/'); range.popFront(); break; @@ -1194,17 +1971,39 @@ case 'r': ret.put('\r'); range.popFront(); break; case 't': ret.put('\t'); range.popFront(); break; case 'u': - range.popFront(); - dchar uch = 0; - foreach( i; 0 .. 4 ){ - uch *= 16; - enforce(!range.empty, "Unicode sequence must be '\\uXXXX'."); - auto dc = range.front; + + dchar decode_unicode_escape() { + enforceJson(range.front == 'u'); range.popFront(); - if( dc >= '0' && dc <= '9' ) uch += dc - '0'; - else if( dc >= 'a' && dc <= 'f' ) uch += dc - 'a' + 10; - else if( dc >= 'A' && dc <= 'F' ) uch += dc - 'A' + 10; - else enforce(false, "Unicode sequence must be '\\uXXXX'."); + dchar uch = 0; + foreach( i; 0 .. 4 ){ + uch *= 16; + enforceJson(!range.empty, "Unicode sequence must be '\\uXXXX'."); + auto dc = range.front; + range.popFront(); + + if( dc >= '0' && dc <= '9' ) uch += dc - '0'; + else if( dc >= 'a' && dc <= 'f' ) uch += dc - 'a' + 10; + else if( dc >= 'A' && dc <= 'F' ) uch += dc - 'A' + 10; + else enforceJson(false, "Unicode sequence must be '\\uXXXX'."); + } + return uch; + } + + auto uch = decode_unicode_escape(); + + if(0xD800 <= uch && uch <= 0xDBFF) { + /* surrogate pair */ + range.popFront(); // backslash '\' + auto uch2 = decode_unicode_escape(); + enforceJson(0xDC00 <= uch2 && uch2 <= 0xDFFF, "invalid Unicode"); + { + /* valid second surrogate */ + uch = + ((uch - 0xD800) << 10) + + (uch2 - 0xDC00) + + 0x10000; + } } ret.put(uch); break; @@ -1219,14 +2018,16 @@ return ret.data; } -private string skipNumber(ref string s, out bool is_float, string filename, int line) +/// private +private string skipNumber(R)(ref R s, out bool is_float) { + // TODO: make this work with input ranges size_t idx = 0; is_float = false; - if( s[idx] == '-' ) idx++; - if( s[idx] == '0' ) idx++; + if (s[idx] == '-') idx++; + if (s[idx] == '0') idx++; else { - enforceJson(isDigit(s[idx++]), "Digit expected at beginning of number.", filename, line); + enforceJson(isDigit(s[idx++]), "Digit expected at beginning of number."); while( idx < s.length && isDigit(s[idx]) ) idx++; } @@ -1240,7 +2041,7 @@ idx++; is_float = true; if( idx < s.length && (s[idx] == '+' || s[idx] == '-') ) idx++; - enforceJson(idx < s.length && isDigit(s[idx]), "Expected exponent." ~ s[0 .. idx], filename, line); + enforceJson( idx < s.length && isDigit(s[idx]), "Expected exponent." ~ s[0 .. idx]); idx++; while( idx < s.length && isDigit(s[idx]) ) idx++; } @@ -1250,39 +2051,40 @@ return ret; } -private string skipJsonString(ref string s, string filename, int* line = null) +/// private +private string skipJsonString(R)(ref R s, int* line = null) { - enforceJson(s.length >= 2, "Too small for a string: '" ~ s ~ "'", filename, *line); - enforceJson(s[0] == '\"', "Expected string, not '" ~ s ~ "'", filename, *line); - s = s[1 .. $]; + // TODO: count or disallow any newlines inside of the string + enforceJson(!s.empty && s.front == '"', "Expected '\"' to start string."); + s.popFront(); string ret = jsonUnescape(s); - enforce(s.length > 0 && s[0] == '\"', "Unterminated string literal.", filename, *line); - s = s[1 .. $]; + enforceJson(!s.empty && s.front == '"', "Expected '\"' to terminate string."); + s.popFront(); return ret; } -private void skipWhitespace(ref string s, int* line = null) +/// private +private void skipWhitespace(R)(ref R s, int* line = null) { - while( s.length > 0 ){ - switch( s[0] ){ + while (!s.empty) { + switch (s.front) { default: return; - case ' ', '\t': s = s[1 .. $]; break; + case ' ', '\t': s.popFront(); break; case '\n': - s = s[1 .. $]; - if( s.length > 0 && s[0] == '\r' ) s = s[1 .. $]; - if( line ) (*line)++; + s.popFront(); + if (!s.empty && s.front == '\r') s.popFront(); + if (line) (*line)++; break; case '\r': - s = s[1 .. $]; - if( s.length > 0 && s[0] == '\n' ) s = s[1 .. $]; - if( line ) (*line)++; + s.popFront(); + if (!s.empty && s.front == '\n') s.popFront(); + if (line) (*line)++; break; } } } -/// private -private bool isDigit(T)(T ch){ return ch >= '0' && ch <= '9'; } +private bool isDigit(dchar ch) { return ch >= '0' && ch <= '9'; } private string underscoreStrip(string field_name) { @@ -1290,13 +2092,23 @@ else return field_name[0 .. $-1]; } -private template isJsonSerializable(T) { enum isJsonSerializable = is(typeof(T.init.toJson()) == Json) && is(typeof(T.fromJson(Json())) == T); } -package template isStringSerializable(T) { enum isStringSerializable = is(typeof(T.init.toString()) == string) && is(typeof(T.fromString("")) == T); } +/// private +package template isJsonSerializable(T) { enum isJsonSerializable = is(typeof(T.init.toJson()) == Json) && is(typeof(T.fromJson(Json())) == T); } -private void enforceJson(string filename = __FILE__, int line = __LINE__)(bool cond, lazy string message, string err_file, int err_line) +private void enforceJson(string file = __FILE__, size_t line = __LINE__)(bool cond, lazy string message = "JSON exception") { - if (!cond) { - auto err_msg = format("%s(%s): Error: %s", err_file, err_line, message); - throw new Exception(err_msg, filename, line); - } + static if (__VERSION__ >= 2065) enforceEx!JSONException(cond, message, file, line); + else if (!cond) throw new JSONException(message); +} + +private void enforceJson(string file = __FILE__, size_t line = __LINE__)(bool cond, lazy string message, string err_file, int err_line) +{ + auto errmsg = format("%s(%s): Error: %s", err_file, err_line+1, message); + static if (__VERSION__ >= 2065) enforceEx!JSONException(cond, errmsg, file, line); + else if (!cond) throw new JSONException(errmsg); +} + +private void enforceJson(string file = __FILE__, size_t line = __LINE__)(bool cond, lazy string message, string err_file, int* err_line) +{ + enforceJson!(file, line)(cond, message, err_file, err_line ? *err_line : -1); } diff --git a/source/dub/internal/vibecompat/data/serialization.d b/source/dub/internal/vibecompat/data/serialization.d new file mode 100644 index 0000000..6903a05 --- /dev/null +++ b/source/dub/internal/vibecompat/data/serialization.d @@ -0,0 +1,1307 @@ +/** + Generic serialization framework. + + This module provides general means for implementing (de-)serialization with + a standardized behavior. + + Supported_types: + The following rules are applied in order when serializing or + deserializing a certain type: + + $(OL + $(LI An `enum` type is serialized as its raw value, except if + `@byName` is used, in which case the name of the enum value + is serialized.) + $(LI Any type that is specifically supported by the serializer + is directly serialized. For example, the BSON serializer + supports `BsonObjectID` directly.) + $(LI Arrays and tuples (`std.typecons.Tuple`) are serialized + using the array serialization functions where each element is + serialized again according to these rules.) + $(LI Associative arrays are serialized similar to arrays. The key + type of the AA must satisfy the `isStringSerializable` trait + and will always be serialized as a string.) + $(LI Any `Nullable!T` will be serialized as either `null`, or + as the contained value (subject to these rules again).) + $(LI Any `BitFlags!T` value will be serialized as `T[]`) + $(LI Types satisfying the `isPolicySerializable` trait for the + supplied `Policy` will be serialized as the value returned + by the policy `toRepresentation` function (again subject to + these rules).) + $(LI Types satisfying the `isCustomSerializable` trait will be + serialized as the value returned by their `toRepresentation` + method (again subject to these rules).) + $(LI Types satisfying the `isISOExtStringSerializable` trait will be + serialized as a string, as returned by their `toISOExtString` + method. This causes types such as `SysTime` to be serialized + as strings.) + $(LI Types satisfying the `isStringSerializable` trait will be + serialized as a string, as returned by their `toString` + method.) + $(LI Struct and class types by default will be serialized as + associative arrays, where the key is the name of the + corresponding field (can be overridden using the `@name` + attribute). If the struct/class is annotated with `@asArray`, + it will instead be serialized as a flat array of values in the + order of declaration. Null class references will be serialized + as `null`.) + $(LI Pointer types will be serialized as either `null`, or as + the value they point to.) + $(LI Built-in integers and floating point values, as well as + boolean values will be converted to strings, if the serializer + doesn't support them directly.) + ) + + Note that no aliasing detection is performed, so that pointers, class + references and arrays referencing the same memory will be serialized + as multiple copies. When in turn deserializing the data, they will also + end up as separate copies in memory. + + Serializer_implementation: + Serializers are implemented in terms of a struct with template methods that + get called by the serialization framework: + + --- + struct ExampleSerializer { + enum isSupportedValueType(T) = is(T == string) || is(T == typeof(null)); + + // serialization + auto getSerializedResult(); + void beginWriteDictionary(T)(); + void endWriteDictionary(T)(); + void beginWriteDictionaryEntry(T)(string name); + void endWriteDictionaryEntry(T)(string name); + void beginWriteArray(T)(size_t length); + void endWriteArray(T)(); + void beginWriteArrayEntry(T)(size_t index); + void endWriteArrayEntry(T)(size_t index); + void writeValue(T)(T value); + + // deserialization + void readDictionary(T)(scope void delegate(string) entry_callback); + void readArray(T)(scope void delegate(size_t) size_callback, scope void delegate() entry_callback); + T readValue(T)(); + bool tryReadNull(); + } + --- + + Copyright: © 2013-2014 rejectedsoftware e.K. + License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. + Authors: Sönke Ludwig +*/ +module dub.internal.vibecompat.data.serialization; + +version (Have_vibe_d) public import vibe.data.serialization; +else: + +import dub.internal.vibecompat.data.utils; + +import std.array : Appender, appender; +import std.conv : to; +import std.exception : enforce; +import std.traits; +import std.typetuple; + + +/** + Serializes a value with the given serializer. + + The serializer must have a value result for the first form + to work. Otherwise, use the range based form. + + See_Also: `vibe.data.json.JsonSerializer`, `vibe.data.json.JsonStringSerializer`, `vibe.data.bson.BsonSerializer` +*/ +auto serialize(Serializer, T, ARGS...)(T value, ARGS args) +{ + auto serializer = Serializer(args); + serialize(serializer, value); + return serializer.getSerializedResult(); +} +/// ditto +void serialize(Serializer, T)(ref Serializer serializer, T value) +{ + serializeImpl!(Serializer, DefaultPolicy, T)(serializer, value); +} + +/** Note that there is a convenience function `vibe.data.json.serializeToJson` + that can be used instead of manually invoking `serialize`. +*/ +unittest { + import dub.internal.vibecompat.data.json; + + struct Test { + int value; + string text; + } + + Test test; + test.value = 12; + test.text = "Hello"; + + Json serialized = serialize!JsonSerializer(test); + assert(serialized.value.get!int == 12); + assert(serialized.text.get!string == "Hello"); +} + +unittest { + import dub.internal.vibecompat.data.json; + + // Make sure that immutable(char[]) works just like string + // (i.e., immutable(char)[]). + immutable key = "answer"; + auto ints = [key: 42]; + auto serialized = serialize!JsonSerializer(ints); + assert(serialized[key].get!int == 42); +} + +/** + Serializes a value with the given serializer, representing values according to `Policy` when possible. + + The serializer must have a value result for the first form + to work. Otherwise, use the range based form. + + See_Also: `vibe.data.json.JsonSerializer`, `vibe.data.json.JsonStringSerializer`, `vibe.data.bson.BsonSerializer` +*/ +auto serializeWithPolicy(Serializer, alias Policy, T, ARGS...)(T value, ARGS args) +{ + auto serializer = Serializer(args); + serializeWithPolicy!(Serializer, Policy)(serializer, value); + return serializer.getSerializedResult(); +} +/// ditto +void serializeWithPolicy(Serializer, alias Policy, T)(ref Serializer serializer, T value) +{ + serializeImpl!(Serializer, Policy, T)(serializer, value); +} +/// +version (unittest) +{ + template SizePol(T) + { + import std.conv; + import std.array; + + string toRepresentation(T value) { + return to!string(value.x) ~ "x" ~ to!string(value.y); + } + + T fromRepresentation(string value) { + string[] fields = value.split('x'); + alias fieldT = typeof(T.x); + auto x = to!fieldT(fields[0]); + auto y = to!fieldT(fields[1]); + return T(x, y); + } + } +} + +/// +static if (__VERSION__ >= 2065) unittest { + import dub.internal.vibecompat.data.json; + + static struct SizeI { + int x; + int y; + } + SizeI sizeI = SizeI(1,2); + Json serializedI = serializeWithPolicy!(JsonSerializer, SizePol)(sizeI); + assert(serializedI.get!string == "1x2"); + + static struct SizeF { + float x; + float y; + } + SizeF sizeF = SizeF(0.1f,0.2f); + Json serializedF = serializeWithPolicy!(JsonSerializer, SizePol)(sizeF); + assert(serializedF.get!string == "0.1x0.2"); +} + + +/** + Deserializes and returns a serialized value. + + serialized_data can be either an input range or a value containing + the serialized data, depending on the type of serializer used. + + See_Also: `vibe.data.json.JsonSerializer`, `vibe.data.json.JsonStringSerializer`, `vibe.data.bson.BsonSerializer` +*/ +T deserialize(Serializer, T, ARGS...)(ARGS args) +{ + auto deserializer = Serializer(args); + return deserializeImpl!(T, DefaultPolicy, Serializer)(deserializer); +} + +/** Note that there is a convenience function `vibe.data.json.deserializeJson` + that can be used instead of manually invoking `deserialize`. +*/ +unittest { + import dub.internal.vibecompat.data.json; + + struct Test { + int value; + string text; + } + + Json serialized = Json.emptyObject; + serialized.value = 12; + serialized.text = "Hello"; + + Test test = deserialize!(JsonSerializer, Test)(serialized); + assert(test.value == 12); + assert(test.text == "Hello"); +} + +/** + Deserializes and returns a serialized value, interpreting values according to `Policy` when possible. + + serialized_data can be either an input range or a value containing + the serialized data, depending on the type of serializer used. + + See_Also: `vibe.data.json.JsonSerializer`, `vibe.data.json.JsonStringSerializer`, `vibe.data.bson.BsonSerializer` +*/ +T deserializeWithPolicy(Serializer, alias Policy, T, ARGS...)(ARGS args) +{ + auto deserializer = Serializer(args); + return deserializeImpl!(T, Policy, Serializer)(deserializer); +} + +/// +static if (__VERSION__ >= 2065) unittest { + import dub.internal.vibecompat.data.json; + + static struct SizeI { + int x; + int y; + } + + Json serializedI = "1x2"; + SizeI sizeI = deserializeWithPolicy!(JsonSerializer, SizePol, SizeI)(serializedI); + assert(sizeI.x == 1); + assert(sizeI.y == 2); + + static struct SizeF { + float x; + float y; + } + Json serializedF = "0.1x0.2"; + SizeF sizeF = deserializeWithPolicy!(JsonSerializer, SizePol, SizeF)(serializedF); + assert(sizeF.x == 0.1f); + assert(sizeF.y == 0.2f); +} + +private void serializeImpl(Serializer, alias Policy, T, ATTRIBUTES...)(ref Serializer serializer, T value) +{ + import std.typecons : Nullable, Tuple, tuple; + static if (__VERSION__ >= 2067) import std.typecons : BitFlags; + + static assert(Serializer.isSupportedValueType!string, "All serializers must support string values."); + static assert(Serializer.isSupportedValueType!(typeof(null)), "All serializers must support null values."); + + alias TU = Unqual!T; + + static if (is(TU == enum)) { + static if (hasAttributeL!(ByNameAttribute, ATTRIBUTES)) { + serializeImpl!(Serializer, Policy, string)(serializer, value.to!string()); + } else { + serializeImpl!(Serializer, Policy, OriginalType!TU)(serializer, cast(OriginalType!TU)value); + } + } else static if (Serializer.isSupportedValueType!TU) { + static if (is(TU == typeof(null))) serializer.writeValue!TU(null); + else serializer.writeValue!TU(value); + } else static if (/*isInstanceOf!(Tuple, TU)*/is(T == Tuple!TPS, TPS...)) { + static if (TU.Types.length == 1) { + serializeImpl!(Serializer, Policy, typeof(value[0]), ATTRIBUTES)(serializer, value[0]); + } else { + serializer.beginWriteArray!TU(value.length); + foreach (i, TV; T.Types) { + serializer.beginWriteArrayEntry!TV(i); + serializeImpl!(Serializer, Policy, TV, ATTRIBUTES)(serializer, value[i]); + serializer.endWriteArrayEntry!TV(i); + } + serializer.endWriteArray!TU(); + } + } else static if (isArray!TU) { + alias TV = typeof(value[0]); + serializer.beginWriteArray!TU(value.length); + foreach (i, ref el; value) { + serializer.beginWriteArrayEntry!TV(i); + serializeImpl!(Serializer, Policy, TV, ATTRIBUTES)(serializer, el); + serializer.endWriteArrayEntry!TV(i); + } + serializer.endWriteArray!TU(); + } else static if (isAssociativeArray!TU) { + alias TK = KeyType!TU; + alias TV = ValueType!TU; + static if (__traits(compiles, serializer.beginWriteDictionary!TU(0))) { + auto nfields = value.length; + serializer.beginWriteDictionary!TU(nfields); + } else { + serializer.beginWriteDictionary!TU(); + } + foreach (key, ref el; value) { + string keyname; + static if (is(TK : string)) keyname = key; + else static if (is(TK : real) || is(TK : long) || is(TK == enum)) keyname = key.to!string; + else static if (isStringSerializable!TK) keyname = key.toString(); + else static assert(false, "Associative array keys must be strings, numbers, enums, or have toString/fromString methods."); + serializer.beginWriteDictionaryEntry!TV(keyname); + serializeImpl!(Serializer, Policy, TV, ATTRIBUTES)(serializer, el); + serializer.endWriteDictionaryEntry!TV(keyname); + } + static if (__traits(compiles, serializer.endWriteDictionary!TU(0))) { + serializer.endWriteDictionary!TU(nfields); + } else { + serializer.endWriteDictionary!TU(); + } + } else static if (/*isInstanceOf!(Nullable, TU)*/is(T == Nullable!TPS, TPS...)) { + if (value.isNull()) serializeImpl!(Serializer, Policy, typeof(null))(serializer, null); + else serializeImpl!(Serializer, Policy, typeof(value.get()), ATTRIBUTES)(serializer, value.get()); + } else static if (__VERSION__ >= 2067 && is(T == BitFlags!E, E)) { + size_t cnt = 0; + foreach (v; EnumMembers!E) + if (value & v) + cnt++; + + serializer.beginWriteArray!(E[])(cnt); + cnt = 0; + foreach (v; EnumMembers!E) + if (value & v) { + serializer.beginWriteArrayEntry!E(cnt); + serializeImpl!(Serializer, Policy, E, ATTRIBUTES)(serializer, v); + serializer.endWriteArrayEntry!E(cnt); + cnt++; + } + serializer.endWriteArray!(E[])(); + } else static if (isPolicySerializable!(Policy, TU)) { + alias CustomType = typeof(Policy!TU.toRepresentation(TU.init)); + serializeImpl!(Serializer, Policy, CustomType, ATTRIBUTES)(serializer, Policy!TU.toRepresentation(value)); + } else static if (isCustomSerializable!TU) { + alias CustomType = typeof(T.init.toRepresentation()); + serializeImpl!(Serializer, Policy, CustomType, ATTRIBUTES)(serializer, value.toRepresentation()); + } else static if (isISOExtStringSerializable!TU) { + serializer.writeValue(value.toISOExtString()); + } else static if (isStringSerializable!TU) { + serializer.writeValue(value.toString()); + } else static if (is(TU == struct) || is(TU == class)) { + static if (!hasSerializableFields!TU) + pragma(msg, "Serializing composite type "~T.stringof~" which has no serializable fields"); + static if (is(TU == class)) { + if (value is null) { + serializeImpl!(Serializer, Policy, typeof(null))(serializer, null); + return; + } + } + static if (hasAttributeL!(AsArrayAttribute, ATTRIBUTES)) { + enum nfields = getExpandedFieldCount!(TU, SerializableFields!TU); + serializer.beginWriteArray!TU(nfields); + foreach (mname; SerializableFields!TU) { + alias TMS = TypeTuple!(typeof(__traits(getMember, value, mname))); + foreach (j, TM; TMS) { + alias TA = TypeTuple!(__traits(getAttributes, TypeTuple!(__traits(getMember, T, mname))[j])); + serializer.beginWriteArrayEntry!TM(j); + serializeImpl!(Serializer, Policy, TM, TA)(serializer, tuple(__traits(getMember, value, mname))[j]); + serializer.endWriteArrayEntry!TM(j); + } + } + serializer.endWriteArray!TU(); + } else { + static if (__traits(compiles, serializer.beginWriteDictionary!TU(0))) { + enum nfields = getExpandedFieldCount!(TU, SerializableFields!TU); + serializer.beginWriteDictionary!TU(nfields); + } else { + serializer.beginWriteDictionary!TU(); + } + foreach (mname; SerializableFields!TU) { + alias TM = TypeTuple!(typeof(__traits(getMember, value, mname))); + static if (TM.length == 1) { + alias TA = TypeTuple!(__traits(getAttributes, __traits(getMember, T, mname))); + enum name = getAttribute!(TU, mname, NameAttribute)(NameAttribute(underscoreStrip(mname))).name; + auto vt = __traits(getMember, value, mname); + serializer.beginWriteDictionaryEntry!(typeof(vt))(name); + serializeImpl!(Serializer, Policy, typeof(vt), TA)(serializer, vt); + serializer.endWriteDictionaryEntry!(typeof(vt))(name); + } else { + alias TA = TypeTuple!(); // FIXME: support attributes for tuples somehow + enum name = underscoreStrip(mname); + auto vt = tuple(__traits(getMember, value, mname)); + serializer.beginWriteDictionaryEntry!(typeof(vt))(name); + serializeImpl!(Serializer, Policy, typeof(vt), TA)(serializer, vt); + serializer.endWriteDictionaryEntry!(typeof(vt))(name); + } + } + static if (__traits(compiles, serializer.endWriteDictionary!TU(0))) { + serializer.endWriteDictionary!TU(nfields); + } else { + serializer.endWriteDictionary!TU(); + } + } + } else static if (isPointer!TU) { + if (value is null) { + serializer.writeValue(null); + return; + } + serializeImpl!(Serializer, Policy, PointerTarget!TU)(serializer, *value); + } else static if (is(TU == bool) || is(TU : real) || is(TU : long)) { + serializeImpl!(Serializer, Policy, string)(serializer, to!string(value)); + } else static assert(false, "Unsupported serialization type: " ~ T.stringof); +} + + +private T deserializeImpl(T, alias Policy, Serializer, ATTRIBUTES...)(ref Serializer deserializer) +{ + import std.typecons : Nullable; + static if (__VERSION__ >= 2067) import std.typecons : BitFlags; + + static assert(Serializer.isSupportedValueType!string, "All serializers must support string values."); + static assert(Serializer.isSupportedValueType!(typeof(null)), "All serializers must support null values."); + + static if (is(T == enum)) { + static if (hasAttributeL!(ByNameAttribute, ATTRIBUTES)) { + return deserializeImpl!(string, Policy, Serializer)(deserializer).to!T(); + } else { + return cast(T)deserializeImpl!(OriginalType!T, Policy, Serializer)(deserializer); + } + } else static if (Serializer.isSupportedValueType!T) { + return deserializer.readValue!T(); + } else static if (isStaticArray!T) { + alias TV = typeof(T.init[0]); + T ret; + size_t i = 0; + deserializer.readArray!T((sz) { assert(sz == 0 || sz == T.length); }, { + assert(i < T.length); + ret[i++] = deserializeImpl!(TV, Policy, Serializer, ATTRIBUTES)(deserializer); + }); + return ret; + } else static if (isDynamicArray!T) { + alias TV = typeof(T.init[0]); + //auto ret = appender!T(); + T ret; // Cannot use appender because of DMD BUG 10690/10859/11357 + deserializer.readArray!T((sz) { ret.reserve(sz); }, () { + ret ~= deserializeImpl!(TV, Policy, Serializer, ATTRIBUTES)(deserializer); + }); + return ret;//cast(T)ret.data; + } else static if (isAssociativeArray!T) { + alias TK = KeyType!T; + alias TV = ValueType!T; + T ret; + deserializer.readDictionary!T((name) { + TK key; + static if (is(TK == string)) key = name; + else static if (is(TK : real) || is(TK : long) || is(TK == enum)) key = name.to!TK; + else static if (isStringSerializable!TK) key = TK.fromString(name); + else static assert(false, "Associative array keys must be strings, numbers, enums, or have toString/fromString methods."); + ret[key] = deserializeImpl!(TV, Policy, Serializer, ATTRIBUTES)(deserializer); + }); + return ret; + } else static if (isInstanceOf!(Nullable, T)) { + if (deserializer.tryReadNull()) return T.init; + return T(deserializeImpl!(typeof(T.init.get()), Policy, Serializer, ATTRIBUTES)(deserializer)); + } else static if (__VERSION__ >= 2067 && is(T == BitFlags!E, E)) { + T ret; + deserializer.readArray!(E[])((sz) {}, { + ret |= deserializeImpl!(E, Policy, Serializer, ATTRIBUTES)(deserializer); + }); + return ret; + } else static if (isPolicySerializable!(Policy, T)) { + alias CustomType = typeof(Policy!T.toRepresentation(T.init)); + return Policy!T.fromRepresentation(deserializeImpl!(CustomType, Policy, Serializer, ATTRIBUTES)(deserializer)); + } else static if (isCustomSerializable!T) { + alias CustomType = typeof(T.init.toRepresentation()); + return T.fromRepresentation(deserializeImpl!(CustomType, Policy, Serializer, ATTRIBUTES)(deserializer)); + } else static if (isISOExtStringSerializable!T) { + return T.fromISOExtString(deserializer.readValue!string()); + } else static if (isStringSerializable!T) { + return T.fromString(deserializer.readValue!string()); + } else static if (is(T == struct) || is(T == class)) { + static if (is(T == class)) { + if (deserializer.tryReadNull()) return null; + } + + bool[__traits(allMembers, T).length] set; + string name; + T ret; + static if (is(T == class)) ret = new T; + + static if (hasAttributeL!(AsArrayAttribute, ATTRIBUTES)) { + size_t idx = 0; + deserializer.readArray!T((sz){}, { + static if (hasSerializableFields!T) { + switch (idx++) { + default: break; + foreach (i, mname; SerializableFields!T) { + alias TM = typeof(__traits(getMember, ret, mname)); + alias TA = TypeTuple!(__traits(getAttributes, __traits(getMember, ret, mname))); + case i: + static if (hasAttribute!(OptionalAttribute, __traits(getMember, T, mname))) + if (deserializer.tryReadNull()) return; + set[i] = true; + __traits(getMember, ret, mname) = deserializeImpl!(TM, Serializer, TA)(deserializer); + break; + } + } + } else { + pragma(msg, "Deserializing composite type "~T.stringof~" which has no serializable fields."); + } + }); + } else { + deserializer.readDictionary!T((name) { + static if (hasSerializableFields!T) { + switch (name) { + default: break; + foreach (i, mname; SerializableFields!T) { + alias TM = typeof(__traits(getMember, ret, mname)); + alias TA = TypeTuple!(__traits(getAttributes, __traits(getMember, ret, mname))); + enum fname = getAttribute!(T, mname, NameAttribute)(NameAttribute(underscoreStrip(mname))).name; + case fname: + static if (hasAttribute!(OptionalAttribute, __traits(getMember, T, mname))) + if (deserializer.tryReadNull()) return; + set[i] = true; + __traits(getMember, ret, mname) = deserializeImpl!(TM, Policy, Serializer, TA)(deserializer); + break; + } + } + } else { + pragma(msg, "Deserializing composite type "~T.stringof~" which has no serializable fields."); + } + }); + } + foreach (i, mname; SerializableFields!T) + static if (!hasAttribute!(OptionalAttribute, __traits(getMember, T, mname))) + enforce(set[i], "Missing non-optional field '"~mname~"' of type '"~T.stringof~"'."); + return ret; + } else static if (isPointer!T) { + if (deserializer.tryReadNull()) return null; + alias PT = PointerTarget!T; + auto ret = new PT; + *ret = deserializeImpl!(PT, Policy, Serializer)(deserializer); + return ret; + } else static if (is(T == bool) || is(T : real) || is(T : long)) { + return to!T(deserializeImpl!(string, Policy, Serializer)(deserializer)); + } else static assert(false, "Unsupported serialization type: " ~ T.stringof); +} + + +/** + Attribute for overriding the field name during (de-)serialization. +*/ +NameAttribute name(string name) +{ + return NameAttribute(name); +} +/// +unittest { + struct Test { + @name("screen-size") int screenSize; + } +} + + +/** + Attribute marking a field as optional during deserialization. +*/ +@property OptionalAttribute optional() +{ + return OptionalAttribute(); +} +/// +unittest { + struct Test { + // does not need to be present during deserialization + @optional int screenSize = 100; + } +} + + +/** + Attribute for marking non-serialized fields. +*/ +@property IgnoreAttribute ignore() +{ + return IgnoreAttribute(); +} +/// +unittest { + struct Test { + // is neither serialized not deserialized + @ignore int screenSize; + } +} + + +/** + Attribute for forcing serialization of enum fields by name instead of by value. +*/ +@property ByNameAttribute byName() +{ + return ByNameAttribute(); +} +/// +unittest { + enum Color { + red, + green, + blue + } + + struct Test { + // serialized as an int (e.g. 1 for Color.green) + Color color; + // serialized as a string (e.g. "green" for Color.green) + @byName Color namedColor; + // serialized as array of ints + Color[] colorArray; + // serialized as array of strings + @byName Color[] namedColorArray; + } +} + + +/** + Attribute for representing a struct/class as an array instead of an object. + + Usually structs and class objects are serialized as dictionaries mapping + from field name to value. Using this attribute, they will be serialized + as a flat array instead. Note that changing the layout will make any + already serialized data mismatch when this attribute is used. +*/ +@property AsArrayAttribute asArray() +{ + return AsArrayAttribute(); +} +/// +unittest { + struct Fields { + int f1; + string f2; + double f3; + } + + struct Test { + // serialized as name:value pairs ["f1": int, "f2": string, "f3": double] + Fields object; + // serialized as a sequential list of values [int, string, double] + @asArray Fields array; + } + + import dub.internal.vibecompat.data.json; + static assert(is(typeof(serializeToJson(Test())))); +} + + +/// +enum FieldExistence +{ + missing, + exists, + defer +} + +/// User defined attribute (not intended for direct use) +struct NameAttribute { string name; } +/// ditto +struct OptionalAttribute {} +/// ditto +struct IgnoreAttribute {} +/// ditto +struct ByNameAttribute {} +/// ditto +struct AsArrayAttribute {} + +/** + Checks if a given type has a custom serialization representation. + + A class or struct type is custom serializable if it defines a pair of + `toRepresentation`/`fromRepresentation` methods. Any class or + struct type that has this trait will be serialized by using the return + value of it's `toRepresentation` method instead of the original value. + + This trait has precedence over `isISOExtStringSerializable` and + `isStringSerializable`. +*/ +template isCustomSerializable(T) +{ + enum bool isCustomSerializable = is(typeof(T.init.toRepresentation())) && is(typeof(T.fromRepresentation(T.init.toRepresentation())) == T); +} +/// +unittest { + // represented as a single uint when serialized + static struct S { + ushort x, y; + + uint toRepresentation() const { return x + (y << 16); } + static S fromRepresentation(uint i) { return S(i & 0xFFFF, i >> 16); } + } + + static assert(isCustomSerializable!S); +} + + +/** + Checks if a given type has an ISO extended string serialization representation. + + A class or struct type is ISO extended string serializable if it defines a + pair of `toISOExtString`/`fromISOExtString` methods. Any class or + struct type that has this trait will be serialized by using the return + value of it's `toISOExtString` method instead of the original value. + + This is mainly useful for supporting serialization of the the date/time + types in `std.datetime`. + + This trait has precedence over `isStringSerializable`. +*/ +template isISOExtStringSerializable(T) +{ + enum bool isISOExtStringSerializable = is(typeof(T.init.toISOExtString()) == string) && is(typeof(T.fromISOExtString("")) == T); +} +/// +unittest { + import std.datetime; + + static assert(isISOExtStringSerializable!DateTime); + static assert(isISOExtStringSerializable!SysTime); + + // represented as an ISO extended string when serialized + static struct S { + // dummy example implementations + string toISOExtString() const { return ""; } + static S fromISOExtString(string s) { return S.init; } + } + + static assert(isISOExtStringSerializable!S); +} + + +/** + Checks if a given type has a string serialization representation. + + A class or struct type is string serializable if it defines a pair of + `toString`/`fromString` methods. Any class or struct type that + has this trait will be serialized by using the return value of it's + `toString` method instead of the original value. +*/ +template isStringSerializable(T) +{ + enum bool isStringSerializable = is(typeof(T.init.toString()) == string) && is(typeof(T.fromString("")) == T); +} +/// +unittest { + import std.conv; + + // represented as the boxed value when serialized + static struct Box(T) { + T value; + } + + template BoxPol(S) + { + auto toRepresentation(S s) { + return s.value; + } + + S fromRepresentation(typeof(S.init.value) v) { + return S(v); + } + } + static assert(isPolicySerializable!(BoxPol, Box!int)); +} + +private template DefaultPolicy(T) +{ +} + +/** + Checks if a given policy supports custom serialization for a given type. + + A class or struct type is custom serializable according to a policy if + the policy defines a pair of `toRepresentation`/`fromRepresentation` + functions. Any class or struct type that has this trait for the policy supplied to + `serializeWithPolicy` will be serialized by using the return value of the + policy `toRepresentation` function instead of the original value. + + This trait has precedence over `isCustomSerializable`, + `isISOExtStringSerializable` and `isStringSerializable`. + + See_Also: `vibe.data.serialization.serializeWithPolicy` +*/ +template isPolicySerializable(alias Policy, T) +{ + enum bool isPolicySerializable = is(typeof(Policy!T.toRepresentation(T.init))) && + is(typeof(Policy!T.fromRepresentation(Policy!T.toRepresentation(T.init))) == T); +} +/// +unittest { + import std.conv; + + // represented as a string when serialized + static struct S { + int value; + + // dummy example implementations + string toString() const { return value.to!string(); } + static S fromString(string s) { return S(s.to!int()); } + } + + static assert(isStringSerializable!S); +} + +/** + Chains serialization policy. + + Constructs a serialization policy that given a type `T` will apply the + first compatible policy `toRepresentation` and `fromRepresentation` + functions. Policies are evaluated left-to-right according to + `isPolicySerializable`. + + See_Also: `vibe.data.serialization.serializeWithPolicy` +*/ +template ChainedPolicy(alias Primary, Fallbacks...) +{ + static if (Fallbacks.length == 0) { + alias ChainedPolicy = Primary; + } else { + alias ChainedPolicy = ChainedPolicy!(ChainedPolicyImpl!(Primary, Fallbacks[0]), Fallbacks[1..$]); + } +} +/// +unittest { + import std.conv; + + // To be represented as the boxed value when serialized + static struct Box(T) { + T value; + } + // Also to berepresented as the boxed value when serialized, but has + // a different way to access the value. + static struct Box2(T) { + private T v; + ref T get() { + return v; + } + } + template BoxPol(S) + { + auto toRepresentation(S s) { + return s.value; + } + + S fromRepresentation(typeof(toRepresentation(S.init)) v) { + return S(v); + } + } + template Box2Pol(S) + { + auto toRepresentation(S s) { + return s.get(); + } + + S fromRepresentation(typeof(toRepresentation(S.init)) v) { + S s; + s.get() = v; + return s; + } + } + alias ChainPol = ChainedPolicy!(BoxPol, Box2Pol); + static assert(!isPolicySerializable!(BoxPol, Box2!int)); + static assert(!isPolicySerializable!(Box2Pol, Box!int)); + static assert(isPolicySerializable!(ChainPol, Box!int)); + static assert(isPolicySerializable!(ChainPol, Box2!int)); +} + +private template ChainedPolicyImpl(alias Primary, alias Fallback) +{ + template Pol(T) + { + static if (isPolicySerializable!(Primary, T)) { + alias toRepresentation = Primary!T.toRepresentation; + alias fromRepresentation = Primary!T.fromRepresentation; + } else { + alias toRepresentation = Fallback!T.toRepresentation; + alias fromRepresentation = Fallback!T.fromRepresentation; + } + } + alias ChainedPolicyImpl = Pol; +} + +private template hasAttribute(T, alias decl) { enum hasAttribute = findFirstUDA!(T, decl).found; } + +unittest { + @asArray int i1; + static assert(hasAttribute!(AsArrayAttribute, i1)); + int i2; + static assert(!hasAttribute!(AsArrayAttribute, i2)); +} + +private template hasAttributeL(T, ATTRIBUTES...) { + static if (ATTRIBUTES.length == 1) { + enum hasAttributeL = is(typeof(ATTRIBUTES[0]) == T); + } else static if (ATTRIBUTES.length > 1) { + enum hasAttributeL = hasAttributeL!(T, ATTRIBUTES[0 .. $/2]) || hasAttributeL!(T, ATTRIBUTES[$/2 .. $]); + } else { + enum hasAttributeL = false; + } +} + +unittest { + static assert(hasAttributeL!(AsArrayAttribute, byName, asArray)); + static assert(!hasAttributeL!(AsArrayAttribute, byName)); +} + +private static T getAttribute(TT, string mname, T)(T default_value) +{ + enum val = findFirstUDA!(T, __traits(getMember, TT, mname)); + static if (val.found) return val.value; + else return default_value; +} + +private string underscoreStrip(string field_name) +{ + if( field_name.length < 1 || field_name[$-1] != '_' ) return field_name; + else return field_name[0 .. $-1]; +} + + +private template hasSerializableFields(T, size_t idx = 0) +{ + enum hasSerializableFields = SerializableFields!(T).length > 0; + /*static if (idx < __traits(allMembers, T).length) { + enum mname = __traits(allMembers, T)[idx]; + static if (!isRWPlainField!(T, mname) && !isRWField!(T, mname)) enum hasSerializableFields = hasSerializableFields!(T, idx+1); + else static if (hasAttribute!(IgnoreAttribute, __traits(getMember, T, mname))) enum hasSerializableFields = hasSerializableFields!(T, idx+1); + else enum hasSerializableFields = true; + } else enum hasSerializableFields = false;*/ +} + +private template SerializableFields(COMPOSITE) +{ + alias SerializableFields = FilterSerializableFields!(COMPOSITE, __traits(allMembers, COMPOSITE)); +} + +private template FilterSerializableFields(COMPOSITE, FIELDS...) +{ + static if (FIELDS.length > 1) { + alias FilterSerializableFields = TypeTuple!( + FilterSerializableFields!(COMPOSITE, FIELDS[0 .. $/2]), + FilterSerializableFields!(COMPOSITE, FIELDS[$/2 .. $])); + } else static if (FIELDS.length == 1) { + alias T = COMPOSITE; + enum mname = FIELDS[0]; + static if (isRWPlainField!(T, mname) || isRWField!(T, mname)) { + alias Tup = TypeTuple!(__traits(getMember, COMPOSITE, FIELDS[0])); + static if (Tup.length != 1) { + alias FilterSerializableFields = TypeTuple!(mname); + } else { + static if (!hasAttribute!(IgnoreAttribute, __traits(getMember, T, mname))) + alias FilterSerializableFields = TypeTuple!(mname); + else alias FilterSerializableFields = TypeTuple!(); + } + } else alias FilterSerializableFields = TypeTuple!(); + } else alias FilterSerializableFields = TypeTuple!(); +} + +private size_t getExpandedFieldCount(T, FIELDS...)() +{ + size_t ret = 0; + foreach (F; FIELDS) ret += TypeTuple!(__traits(getMember, T, F)).length; + return ret; +} + +/******************************************************************************/ +/* General serialization unit testing */ +/******************************************************************************/ + +version (unittest) { + private struct TestSerializer { + import std.array, std.conv, std.string; + + string result; + + enum isSupportedValueType(T) = is(T == string) || is(T == typeof(null)) || is(T == float) || is (T == int); + + string getSerializedResult() { return result; } + void beginWriteDictionary(T)() { result ~= "D("~T.mangleof~"){"; } + void endWriteDictionary(T)() { result ~= "}D("~T.mangleof~")"; } + void beginWriteDictionaryEntry(T)(string name) { result ~= "DE("~T.mangleof~","~name~")("; } + void endWriteDictionaryEntry(T)(string name) { result ~= ")DE("~T.mangleof~","~name~")"; } + void beginWriteArray(T)(size_t length) { result ~= "A("~T.mangleof~")["~length.to!string~"]["; } + void endWriteArray(T)() { result ~= "]A("~T.mangleof~")"; } + void beginWriteArrayEntry(T)(size_t i) { result ~= "AE("~T.mangleof~","~i.to!string~")("; } + void endWriteArrayEntry(T)(size_t i) { result ~= ")AE("~T.mangleof~","~i.to!string~")"; } + void writeValue(T)(T value) { + if (is(T == typeof(null))) result ~= "null"; + else { + assert(isSupportedValueType!T); + result ~= "V("~T.mangleof~")("~value.to!string~")"; + } + } + + // deserialization + void readDictionary(T)(scope void delegate(string) entry_callback) + { + skip("D("~T.mangleof~"){"); + while (result.startsWith("DE(")) { + result = result[3 .. $]; + auto idx = result.indexOf(','); + auto idx2 = result.indexOf(")("); + assert(idx > 0 && idx2 > idx); + auto t = result[0 .. idx]; + auto n = result[idx+1 .. idx2]; + result = result[idx2+2 .. $]; + entry_callback(n); + skip(")DE("~t~","~n~")"); + } + skip("}D("~T.mangleof~")"); + } + + void readArray(T)(scope void delegate(size_t) size_callback, scope void delegate() entry_callback) + { + skip("A("~T.mangleof~")["); + auto bidx = result.indexOf("]["); + assert(bidx > 0); + auto cnt = result[0 .. bidx].to!size_t; + result = result[bidx+2 .. $]; + + size_t i = 0; + while (result.startsWith("AE(")) { + result = result[3 .. $]; + auto idx = result.indexOf(','); + auto idx2 = result.indexOf(")("); + assert(idx > 0 && idx2 > idx); + auto t = result[0 .. idx]; + auto n = result[idx+1 .. idx2]; + result = result[idx2+2 .. $]; + assert(n == i.to!string); + entry_callback(); + skip(")AE("~t~","~n~")"); + i++; + } + skip("]A("~T.mangleof~")"); + + assert(i == cnt); + } + + T readValue(T)() + { + skip("V("~T.mangleof~")("); + auto idx = result.indexOf(')'); + assert(idx >= 0); + auto ret = result[0 .. idx].to!T; + result = result[idx+1 .. $]; + return ret; + } + + void skip(string prefix) + { + assert(result.startsWith(prefix), result); + result = result[prefix.length .. $]; + } + + bool tryReadNull() + { + if (result.startsWith("null")) { + result = result[4 .. $]; + return true; + } else return false; + } + } +} + +unittest { // basic serialization behavior + import std.typecons : Nullable; + + static void test(T)(T value, string expected) { + assert(serialize!TestSerializer(value) == expected, serialize!TestSerializer(value)); + static if (isPointer!T) { + if (value) assert(*deserialize!(TestSerializer, T)(expected) == *value); + else assert(deserialize!(TestSerializer, T)(expected) is null); + } else static if (is(T == Nullable!U, U)) { + if (value.isNull()) assert(deserialize!(TestSerializer, T)(expected).isNull); + else assert(deserialize!(TestSerializer, T)(expected) == value); + } else assert(deserialize!(TestSerializer, T)(expected) == value); + } + + test("hello", "V(Aya)(hello)"); + test(12, "V(i)(12)"); + test(12.0, "V(Aya)(12)"); + test(12.0f, "V(f)(12)"); + assert(serialize!TestSerializer(null) == "null"); + test(["hello", "world"], "A(AAya)[2][AE(Aya,0)(V(Aya)(hello))AE(Aya,0)AE(Aya,1)(V(Aya)(world))AE(Aya,1)]A(AAya)"); + test(["hello": "world"], "D(HAyaAya){DE(Aya,hello)(V(Aya)(world))DE(Aya,hello)}D(HAyaAya)"); + test(cast(int*)null, "null"); + int i = 42; + test(&i, "V(i)(42)"); + Nullable!int j; + test(j, "null"); + j = 42; + test(j, "V(i)(42)"); +} + +unittest { // basic user defined types + static struct S { string f; } + enum Sm = S.mangleof; + auto s = S("hello"); + enum s_ser = "D("~Sm~"){DE(Aya,f)(V(Aya)(hello))DE(Aya,f)}D("~Sm~")"; + assert(serialize!TestSerializer(s) == s_ser, serialize!TestSerializer(s)); + assert(deserialize!(TestSerializer, S)(s_ser) == s); + + static class C { string f; } + enum Cm = C.mangleof; + C c; + assert(serialize!TestSerializer(c) == "null"); + c = new C; + c.f = "hello"; + enum c_ser = "D("~Cm~"){DE(Aya,f)(V(Aya)(hello))DE(Aya,f)}D("~Cm~")"; + assert(serialize!TestSerializer(c) == c_ser); + assert(deserialize!(TestSerializer, C)(c_ser).f == c.f); + + enum E { hello, world } + assert(serialize!TestSerializer(E.hello) == "V(i)(0)"); + assert(serialize!TestSerializer(E.world) == "V(i)(1)"); +} + +unittest { // tuple serialization + import std.typecons : Tuple; + + static struct S(T...) { T f; } + enum Sm = S!(int, string).mangleof; + enum Tum = Tuple!(int, string).mangleof; + auto s = S!(int, string)(42, "hello"); + assert(serialize!TestSerializer(s) == + "D("~Sm~"){DE("~Tum~",f)(A("~Tum~")[2][AE(i,0)(V(i)(42))AE(i,0)AE(Aya,1)(V(Aya)(hello))AE(Aya,1)]A("~Tum~"))DE("~Tum~",f)}D("~Sm~")"); + + static struct T { @asArray S!(int, string) g; } + enum Tm = T.mangleof; + auto t = T(s); + assert(serialize!TestSerializer(t) == + "D("~Tm~"){DE("~Sm~",g)(A("~Sm~")[2][AE(i,0)(V(i)(42))AE(i,0)AE(Aya,1)(V(Aya)(hello))AE(Aya,1)]A("~Sm~"))DE("~Sm~",g)}D("~Tm~")"); +} + +unittest { // testing the various UDAs + enum E { hello, world } + enum Em = E.mangleof; + static struct S { + @byName E e; + @ignore int i; + @optional float f; + } + enum Sm = S.mangleof; + auto s = S(E.world, 42, 1.0f); + assert(serialize!TestSerializer(s) == + "D("~Sm~"){DE("~Em~",e)(V(Aya)(world))DE("~Em~",e)DE(f,f)(V(f)(1))DE(f,f)}D("~Sm~")"); +} + +unittest { // custom serialization support + // iso-ext + import std.datetime; + auto t = TimeOfDay(6, 31, 23); + assert(serialize!TestSerializer(t) == "V(Aya)(06:31:23)"); + auto d = Date(1964, 1, 23); + assert(serialize!TestSerializer(d) == "V(Aya)(1964-01-23)"); + auto dt = DateTime(d, t); + assert(serialize!TestSerializer(dt) == "V(Aya)(1964-01-23T06:31:23)"); + auto st = SysTime(dt, UTC()); + assert(serialize!TestSerializer(st) == "V(Aya)(1964-01-23T06:31:23Z)"); + + // string + struct S1 { int i; string toString() const { return "hello"; } static S1 fromString(string) { return S1.init; } } + struct S2 { int i; string toString() const { return "hello"; } } + enum S2m = S2.mangleof; + struct S3 { int i; static S3 fromString(string) { return S3.init; } } + enum S3m = S3.mangleof; + assert(serialize!TestSerializer(S1.init) == "V(Aya)(hello)"); + assert(serialize!TestSerializer(S2.init) == "D("~S2m~"){DE(i,i)(V(i)(0))DE(i,i)}D("~S2m~")"); + assert(serialize!TestSerializer(S3.init) == "D("~S3m~"){DE(i,i)(V(i)(0))DE(i,i)}D("~S3m~")"); + + // custom + struct C1 { int i; float toRepresentation() const { return 1.0f; } static C1 fromRepresentation(float f) { return C1.init; } } + struct C2 { int i; float toRepresentation() const { return 1.0f; } } + enum C2m = C2.mangleof; + struct C3 { int i; static C3 fromRepresentation(float f) { return C3.init; } } + enum C3m = C3.mangleof; + assert(serialize!TestSerializer(C1.init) == "V(f)(1)"); + assert(serialize!TestSerializer(C2.init) == "D("~C2m~"){DE(i,i)(V(i)(0))DE(i,i)}D("~C2m~")"); + assert(serialize!TestSerializer(C3.init) == "D("~C3m~"){DE(i,i)(V(i)(0))DE(i,i)}D("~C3m~")"); +} + +unittest // Testing corner case: member function returning by ref +{ + import dub.internal.vibecompat.data.json; + + static struct S + { + int i; + ref int foo() { return i; } + } + + static assert(__traits(compiles, { S().serializeToJson(); })); + static assert(__traits(compiles, { Json().deserializeJson!S(); })); + + auto s = S(1); + assert(s.serializeToJson().deserializeJson!S() == s); +} + +unittest // Testing corner case: Variadic template constructors and methods +{ + import dub.internal.vibecompat.data.json; + + static struct S + { + int i; + this(Args...)(Args args) {} + int foo(Args...)(Args args) { return i; } + ref int bar(Args...)(Args args) { return i; } + } + + static assert(__traits(compiles, { S().serializeToJson(); })); + static assert(__traits(compiles, { Json().deserializeJson!S(); })); + + auto s = S(1); + assert(s.serializeToJson().deserializeJson!S() == s); +} + +unittest // Make sure serializing through properties still works +{ + import dub.internal.vibecompat.data.json; + + static struct S + { + public int i; + private int privateJ; + + @property int j() { return privateJ; } + @property void j(int j) { privateJ = j; } + } + + auto s = S(1, 2); + assert(s.serializeToJson().deserializeJson!S() == s); +} + +static if (__VERSION__ >= 2067) +unittest { // test BitFlags serialization + import std.typecons : BitFlags; + + enum Flag { + a = 1<<0, + b = 1<<1, + c = 1<<2 + } + enum Flagm = Flag.mangleof; + + alias Flags = BitFlags!Flag; + enum Flagsm = Flags.mangleof; + + enum Fi_ser = "A(A"~Flagm~")[0][]A(A"~Flagm~")"; + assert(serialize!TestSerializer(Flags.init) == Fi_ser); + + enum Fac_ser = "A(A"~Flagm~")[2][AE("~Flagm~",0)(V(i)(1))AE("~Flagm~",0)AE("~Flagm~",1)(V(i)(4))AE("~Flagm~",1)]A(A"~Flagm~")"; + assert(serialize!TestSerializer(Flags(Flag.a, Flag.c)) == Fac_ser); + + struct S { @byName Flags f; } + enum Sm = S.mangleof; + enum Sac_ser = "D("~Sm~"){DE("~Flagsm~",f)(A(A"~Flagm~")[2][AE("~Flagm~",0)(V(Aya)(a))AE("~Flagm~",0)AE("~Flagm~",1)(V(Aya)(c))AE("~Flagm~",1)]A(A"~Flagm~"))DE("~Flagsm~",f)}D("~Sm~")"; + + assert(serialize!TestSerializer(S(Flags(Flag.a, Flag.c))) == Sac_ser); + + assert(deserialize!(TestSerializer, Flags)(Fi_ser) == Flags.init); + assert(deserialize!(TestSerializer, Flags)(Fac_ser) == Flags(Flag.a, Flag.c)); + assert(deserialize!(TestSerializer, S)(Sac_ser) == S(Flags(Flag.a, Flag.c))); +} diff --git a/source/dub/internal/vibecompat/data/utils.d b/source/dub/internal/vibecompat/data/utils.d index 5269fb3..7d4359c 100644 --- a/source/dub/internal/vibecompat/data/utils.d +++ b/source/dub/internal/vibecompat/data/utils.d @@ -7,24 +7,714 @@ */ module dub.internal.vibecompat.data.utils; +version (Have_vibe_d) {} +else: + public import std.traits; +/** + Checks if given type is a getter function type -template isRWPlainField(T, string M) + Returns: `true` if argument is a getter + */ +template isPropertyGetter(T...) + if (T.length == 1) { - static if( !__traits(compiles, typeof(__traits(getMember, T, M))) ){ - enum isRWPlainField = false; - } else { - //pragma(msg, T.stringof~"."~M~":"~typeof(__traits(getMember, T, M)).stringof); - enum isRWPlainField = isRWField!(T, M) && __traits(compiles, *(&__traits(getMember, Tgen!T(), M)) = *(&__traits(getMember, Tgen!T(), M))); + import std.traits : functionAttributes, FunctionAttribute, ReturnType, + isSomeFunction; + static if (isSomeFunction!(T[0])) { + enum isPropertyGetter = + (functionAttributes!(T[0]) & FunctionAttribute.property) != 0 + && !is(ReturnType!T == void); + } + else + enum isPropertyGetter = false; +} + +/// +unittest +{ + interface Test + { + @property int getter(); + @property void setter(int); + int simple(); + } + + static assert(isPropertyGetter!(typeof(&Test.getter))); + static assert(!isPropertyGetter!(typeof(&Test.setter))); + static assert(!isPropertyGetter!(typeof(&Test.simple))); + static assert(!isPropertyGetter!int); +} + +/** + Checks if given type is a setter function type + + Returns: `true` if argument is a setter + */ +template isPropertySetter(T...) + if (T.length == 1) +{ + import std.traits : functionAttributes, FunctionAttribute, ReturnType, + isSomeFunction; + + static if (isSomeFunction!(T[0])) { + enum isPropertySetter = + (functionAttributes!(T) & FunctionAttribute.property) != 0 + && is(ReturnType!(T[0]) == void); + } + else + enum isPropertySetter = false; +} + +/// +unittest +{ + interface Test + { + @property int getter(); + @property void setter(int); + int simple(); + } + + static assert(isPropertySetter!(typeof(&Test.setter))); + static assert(!isPropertySetter!(typeof(&Test.getter))); + static assert(!isPropertySetter!(typeof(&Test.simple))); + static assert(!isPropertySetter!int); +} + +/** + Deduces single base interface for a type. Multiple interfaces + will result in compile-time error. + + Params: + T = interface or class type + + Returns: + T if it is an interface. If T is a class, interface it implements. +*/ +template baseInterface(T) + if (is(T == interface) || is(T == class)) +{ + import std.traits : InterfacesTuple; + + static if (is(T == interface)) { + alias baseInterface = T; + } + else + { + alias Ifaces = InterfacesTuple!T; + static assert ( + Ifaces.length == 1, + "Type must be either provided as an interface or implement only one interface" + ); + alias baseInterface = Ifaces[0]; } } -template isRWField(T, string M) +/// +unittest { - enum isRWField = __traits(compiles, __traits(getMember, Tgen!T(), M) = __traits(getMember, Tgen!T(), M)); - //pragma(msg, T.stringof~"."~M~": "~(isRWField?"1":"0")); + interface I1 { } + class A : I1 { } + interface I2 { } + class B : I1, I2 { } + + static assert (is(baseInterface!I1 == I1)); + static assert (is(baseInterface!A == I1)); + static assert (!is(typeof(baseInterface!B))); } -/// private -private T Tgen(T)(){ return T.init; } + +/** + Determins if a member is a public, non-static data field. +*/ +template isRWPlainField(T, string M) +{ + static if (!isRWField!(T, M)) enum isRWPlainField = false; + else { + //pragma(msg, T.stringof~"."~M~":"~typeof(__traits(getMember, T, M)).stringof); + enum isRWPlainField = __traits(compiles, *(&__traits(getMember, Tgen!T(), M)) = *(&__traits(getMember, Tgen!T(), M))); + } +} + +/** + Determines if a member is a public, non-static, de-facto data field. + + In addition to plain data fields, R/W properties are also accepted. +*/ +template isRWField(T, string M) +{ + import std.traits; + import std.typetuple; + + static void testAssign()() { + T t = void; + __traits(getMember, t, M) = __traits(getMember, t, M); + } + + // reject type aliases + static if (is(TypeTuple!(__traits(getMember, T, M)))) enum isRWField = false; + // reject non-public members + else static if (!isPublicMember!(T, M)) enum isRWField = false; + // reject static members + else static if (!isNonStaticMember!(T, M)) enum isRWField = false; + // reject non-typed members + else static if (!is(typeof(__traits(getMember, T, M)))) enum isRWField = false; + // reject void typed members (includes templates) + else static if (is(typeof(__traits(getMember, T, M)) == void)) enum isRWField = false; + // reject non-assignable members + else static if (!__traits(compiles, testAssign!()())) enum isRWField = false; + else static if (anySatisfy!(isSomeFunction, __traits(getMember, T, M))) { + // If M is a function, reject if not @property or returns by ref + private enum FA = functionAttributes!(__traits(getMember, T, M)); + enum isRWField = (FA & FunctionAttribute.property) != 0; + } else { + enum isRWField = true; + } +} + +unittest { + import std.algorithm; + + struct S { + alias a = int; // alias + int i; // plain RW field + enum j = 42; // manifest constant + static int k = 42; // static field + private int privateJ; // private RW field + + this(Args...)(Args args) {} + + // read-write property (OK) + @property int p1() { return privateJ; } + @property void p1(int j) { privateJ = j; } + // read-only property (NO) + @property int p2() { return privateJ; } + // write-only property (NO) + @property void p3(int value) { privateJ = value; } + // ref returning property (OK) + @property ref int p4() { return i; } + // parameter-less template property (OK) + @property ref int p5()() { return i; } + // not treated as a property by DMD, so not a field + @property int p6()() { return privateJ; } + @property void p6(int j)() { privateJ = j; } + + static @property int p7() { return k; } + static @property void p7(int value) { k = value; } + + ref int f1() { return i; } // ref returning function (no field) + + int f2(Args...)(Args args) { return i; } + + ref int f3(Args...)(Args args) { return i; } + + void someMethod() {} + + ref int someTempl()() { return i; } + } + + enum plainFields = ["i"]; + enum fields = ["i", "p1", "p4", "p5"]; + + foreach (mem; __traits(allMembers, S)) { + static if (isRWField!(S, mem)) static assert(fields.canFind(mem), mem~" detected as field."); + else static assert(!fields.canFind(mem), mem~" not detected as field."); + + static if (isRWPlainField!(S, mem)) static assert(plainFields.canFind(mem), mem~" not detected as plain field."); + else static assert(!plainFields.canFind(mem), mem~" not detected as plain field."); + } +} + +package T Tgen(T)(){ return T.init; } + + +/** + Tests if the protection of a member is public. +*/ +template isPublicMember(T, string M) +{ + import std.algorithm, std.typetuple : TypeTuple; + + static if (!__traits(compiles, TypeTuple!(__traits(getMember, T, M)))) enum isPublicMember = false; + else { + alias MEM = TypeTuple!(__traits(getMember, T, M)); + enum _prot = __traits(getProtection, MEM); + enum isPublicMember = _prot == "public" || _prot == "export"; + } +} + +unittest { + class C { + int a; + export int b; + protected int c; + private int d; + package int e; + void f() {} + static void g() {} + private void h() {} + private static void i() {} + } + + static assert (isPublicMember!(C, "a")); + static assert (isPublicMember!(C, "b")); + static assert (!isPublicMember!(C, "c")); + static assert (!isPublicMember!(C, "d")); + static assert (!isPublicMember!(C, "e")); + static assert (isPublicMember!(C, "f")); + static assert (isPublicMember!(C, "g")); + static assert (!isPublicMember!(C, "h")); + static assert (!isPublicMember!(C, "i")); + + struct S { + int a; + export int b; + private int d; + package int e; + } + static assert (isPublicMember!(S, "a")); + static assert (isPublicMember!(S, "b")); + static assert (!isPublicMember!(S, "d")); + static assert (!isPublicMember!(S, "e")); + + S s; + s.a = 21; + assert(s.a == 21); +} + +/** + Tests if a member requires $(D this) to be used. +*/ +template isNonStaticMember(T, string M) +{ + import std.typetuple; + import std.traits; + + alias MF = TypeTuple!(__traits(getMember, T, M)); + static if (M.length == 0) { + enum isNonStaticMember = false; + } else static if (anySatisfy!(isSomeFunction, MF)) { + enum isNonStaticMember = !__traits(isStaticFunction, MF); + } else { + enum isNonStaticMember = !__traits(compiles, (){ auto x = __traits(getMember, T, M); }()); + } +} + +unittest { // normal fields + struct S { + int a; + static int b; + enum c = 42; + void f(); + static void g(); + ref int h() { return a; } + static ref int i() { return b; } + } + static assert(isNonStaticMember!(S, "a")); + static assert(!isNonStaticMember!(S, "b")); + static assert(!isNonStaticMember!(S, "c")); + static assert(isNonStaticMember!(S, "f")); + static assert(!isNonStaticMember!(S, "g")); + static assert(isNonStaticMember!(S, "h")); + static assert(!isNonStaticMember!(S, "i")); +} + +unittest { // tuple fields + struct S(T...) { + T a; + static T b; + } + + alias T = S!(int, float); + auto p = T.b; + static assert(isNonStaticMember!(T, "a")); + static assert(!isNonStaticMember!(T, "b")); + + alias U = S!(); + static assert(!isNonStaticMember!(U, "a")); + static assert(!isNonStaticMember!(U, "b")); +} + + +/** + Tests if a Group of types is implicitly convertible to a Group of target types. +*/ +bool areConvertibleTo(alias TYPES, alias TARGET_TYPES)() + if (isGroup!TYPES && isGroup!TARGET_TYPES) +{ + static assert(TYPES.expand.length == TARGET_TYPES.expand.length); + foreach (i, V; TYPES.expand) + if (!is(V : TARGET_TYPES.expand[i])) + return false; + return true; +} + +/// Test if the type $(D DG) is a correct delegate for an opApply where the +/// key/index is of type $(D TKEY) and the value of type $(D TVALUE). +template isOpApplyDg(DG, TKEY, TVALUE) { + import std.traits; + static if (is(DG == delegate) && is(ReturnType!DG : int)) { + private alias PTT = ParameterTypeTuple!(DG); + private alias PSCT = ParameterStorageClassTuple!(DG); + private alias STC = ParameterStorageClass; + // Just a value + static if (PTT.length == 1) { + enum isOpApplyDg = (is(PTT[0] == TVALUE) && PSCT[0] == STC.ref_); + } else static if (PTT.length == 2) { + enum isOpApplyDg = (is(PTT[0] == TKEY) && PSCT[0] == STC.ref_) + && (is(PTT[1] == TKEY) && PSCT[1] == STC.ref_); + } else + enum isOpApplyDg = false; + } else { + enum isOpApplyDg = false; + } +} + +/** + TypeTuple which does not auto-expand. + + Useful when you need + to multiple several type tuples as different template argument + list parameters, without merging those. +*/ +template Group(T...) +{ + alias expand = T; +} + +/// +unittest +{ + alias group = Group!(int, double, string); + static assert (!is(typeof(group.length))); + static assert (group.expand.length == 3); + static assert (is(group.expand[1] == double)); +} + +/** +*/ +template isGroup(T...) +{ + static if (T.length != 1) enum isGroup = false; + else enum isGroup = + !is(T[0]) && is(typeof(T[0]) == void) // does not evaluate to something + && is(typeof(T[0].expand.length) : size_t) // expands to something with length + && !is(typeof(&(T[0].expand))); // expands to not addressable +} + +version (unittest) // NOTE: GDC complains about template definitions in unittest blocks +{ + import std.typetuple; + + alias group = Group!(int, double, string); + alias group2 = Group!(); + + template Fake(T...) + { + int[] expand; + } + alias fake = Fake!(int, double, string); + + alias fake2 = TypeTuple!(int, double, string); + + static assert (isGroup!group); + static assert (isGroup!group2); + static assert (!isGroup!fake); + static assert (!isGroup!fake2); +} + +/* Copied from Phobos as it is private there. + */ +private template isSame(ab...) + if (ab.length == 2) +{ + static if (is(ab[0]) && is(ab[1])) + { + enum isSame = is(ab[0] == ab[1]); + } + else static if (!is(ab[0]) && + !is(ab[1]) && + is(typeof(ab[0] == ab[1]) == bool) && + (ab[0] == ab[1])) + { + static if (!__traits(compiles, &ab[0]) || + !__traits(compiles, &ab[1])) + enum isSame = (ab[0] == ab[1]); + else + enum isSame = __traits(isSame, ab[0], ab[1]); + } + else + { + enum isSame = __traits(isSame, ab[0], ab[1]); + } +} + +/** + Compares two groups for element identity + + Params: + Group1, Group2 = any instances of `Group` + + Returns: + `true` if each element of Group1 is identical to + the one of Group2 at the same index +*/ +template Compare(alias Group1, alias Group2) + if (isGroup!Group1 && isGroup!Group2) +{ + private bool implementation() + { + static if (Group1.expand.length == Group2.expand.length) { + foreach (index, element; Group1.expand) + { + static if (!isSame!(Group1.expand[index], Group2.expand[index])) { + return false; + } + } + return true; + } + else { + return false; + } + } + + enum Compare = implementation(); +} + +/// +unittest +{ + alias one = Group!(int, double); + alias two = Group!(int, double); + alias three = Group!(double, int); + static assert (Compare!(one, two)); + static assert (!Compare!(one, three)); +} + +/** + Small convenience wrapper to find and extract certain UDA from given type. + Will stop on first element which is of required type. + + Params: + UDA = type or template to search for in UDA list + Symbol = symbol to query for UDA's + allow_types = if set to `false` considers attached `UDA` types an error + (only accepts instances/values) + + Returns: aggregated search result struct with 3 field. `value` aliases found UDA. + `found` is boolean flag for having a valid find. `index` is integer index in + attribute list this UDA was found at. +*/ +template findFirstUDA(alias UDA, alias Symbol, bool allow_types = false) if (!is(UDA)) +{ + enum findFirstUDA = findNextUDA!(UDA, Symbol, 0, allow_types); +} + +/// Ditto +template findFirstUDA(UDA, alias Symbol, bool allow_types = false) +{ + enum findFirstUDA = findNextUDA!(UDA, Symbol, 0, allow_types); +} + +private struct UdaSearchResult(alias UDA) +{ + alias value = UDA; + bool found = false; + long index = -1; +} + +/** + Small convenience wrapper to find and extract certain UDA from given type. + Will start at the given index and stop on the next element which is of required type. + + Params: + UDA = type or template to search for in UDA list + Symbol = symbol to query for UDA's + idx = 0-based index to start at. Should be positive, and under the total number of attributes. + allow_types = if set to `false` considers attached `UDA` types an error + (only accepts instances/values) + + Returns: aggregated search result struct with 3 field. `value` aliases found UDA. + `found` is boolean flag for having a valid find. `index` is integer index in + attribute list this UDA was found at. + */ +template findNextUDA(alias UDA, alias Symbol, long idx, bool allow_types = false) if (!is(UDA)) +{ + import std.traits : isInstanceOf; + import std.typetuple : TypeTuple; + + private alias udaTuple = TypeTuple!(__traits(getAttributes, Symbol)); + + static assert(idx >= 0, "Index given to findNextUDA can't be negative"); + static assert(idx <= udaTuple.length, "Index given to findNextUDA is above the number of attribute"); + + public template extract(size_t index, list...) + { + static if (!list.length) enum extract = UdaSearchResult!(null)(false, -1); + else { + static if (is(list[0])) { + static if (is(UDA) && is(list[0] == UDA) || !is(UDA) && isInstanceOf!(UDA, list[0])) { + static assert (allow_types, "findNextUDA is designed to look up values, not types"); + enum extract = UdaSearchResult!(list[0])(true, index); + } else enum extract = extract!(index + 1, list[1..$]); + } else { + static if (is(UDA) && is(typeof(list[0]) == UDA) || !is(UDA) && isInstanceOf!(UDA, typeof(list[0]))) { + import vibe.internal.meta.traits : isPropertyGetter; + static if (isPropertyGetter!(list[0])) { + enum value = list[0]; + enum extract = UdaSearchResult!(value)(true, index); + } else enum extract = UdaSearchResult!(list[0])(true, index); + } else enum extract = extract!(index + 1, list[1..$]); + } + } + } + + enum findNextUDA = extract!(idx, udaTuple[idx .. $]); +} +/// ditto +template findNextUDA(UDA, alias Symbol, long idx, bool allow_types = false) +{ + import std.traits : isInstanceOf; + import std.typetuple : TypeTuple; + + private alias udaTuple = TypeTuple!(__traits(getAttributes, Symbol)); + + static assert(idx >= 0, "Index given to findNextUDA can't be negative"); + static assert(idx <= udaTuple.length, "Index given to findNextUDA is above the number of attribute"); + + public template extract(size_t index, list...) + { + static if (!list.length) enum extract = UdaSearchResult!(null)(false, -1); + else { + static if (is(list[0])) { + static if (is(list[0] == UDA)) { + static assert (allow_types, "findNextUDA is designed to look up values, not types"); + enum extract = UdaSearchResult!(list[0])(true, index); + } else enum extract = extract!(index + 1, list[1..$]); + } else { + static if (is(typeof(list[0]) == UDA)) { + static if (isPropertyGetter!(list[0])) { + enum value = list[0]; + enum extract = UdaSearchResult!(value)(true, index); + } else enum extract = UdaSearchResult!(list[0])(true, index); + } else enum extract = extract!(index + 1, list[1..$]); + } + } + } + + enum findNextUDA = extract!(idx, udaTuple[idx .. $]); +} + + +/// +unittest +{ + struct Attribute { int x; } + + @("something", Attribute(42), Attribute(41)) + void symbol(); + + enum result0 = findNextUDA!(string, symbol, 0); + static assert (result0.found); + static assert (result0.index == 0); + static assert (result0.value == "something"); + + enum result1 = findNextUDA!(Attribute, symbol, 0); + static assert (result1.found); + static assert (result1.index == 1); + static assert (result1.value == Attribute(42)); + + enum result2 = findNextUDA!(int, symbol, 0); + static assert (!result2.found); + + enum result3 = findNextUDA!(Attribute, symbol, result1.index + 1); + static assert (result3.found); + static assert (result3.index == 2); + static assert (result3.value == Attribute(41)); +} + +unittest +{ + struct Attribute { int x; } + + @(Attribute) void symbol(); + + static assert (!is(findNextUDA!(Attribute, symbol, 0))); + + enum result0 = findNextUDA!(Attribute, symbol, 0, true); + static assert (result0.found); + static assert (result0.index == 0); + static assert (is(result0.value == Attribute)); +} + +unittest +{ + struct Attribute { int x; } + enum Dummy; + + @property static Attribute getter() + { + return Attribute(42); + } + + @Dummy @getter void symbol(); + + enum result0 = findNextUDA!(Attribute, symbol, 0); + static assert (result0.found); + static assert (result0.index == 1); + static assert (result0.value == Attribute(42)); +} + +/// Eager version of findNextUDA that represent all instances of UDA in a Tuple. +/// If one of the attribute is a type instead of an instance, compilation will fail. +template UDATuple(alias UDA, alias Sym) { + import std.typetuple : TypeTuple; + + private template extract(size_t maxSize, Founds...) + { + private alias LastFound = Founds[$ - 1]; + // No more to find + static if (!LastFound.found) + enum extract = Founds[0 .. $ - 1]; + else { + // For ease of use, this is a Tuple of UDA, not a tuple of UdaSearchResult!(...) + private alias Result = TypeTuple!(Founds[0 .. $ - 1], LastFound.value); + // We're at the last parameter + static if (LastFound.index == maxSize) + enum extract = Result; + else + enum extract = extract!(maxSize, Result, findNextUDA!(UDA, Sym, LastFound.index + 1)); + } + } + + private enum maxIndex = TypeTuple!(__traits(getAttributes, Sym)).length; + enum UDATuple = extract!(maxIndex, findNextUDA!(UDA, Sym, 0)); +} + +unittest +{ + import std.typetuple : TypeTuple; + + struct Attribute { int x; } + enum Dummy; + + @(Dummy, Attribute(21), Dummy, Attribute(42), Attribute(84)) void symbol() {} + @(Dummy, Attribute(21), Dummy, Attribute(42), Attribute) void wrong() {} + + alias Cmp = TypeTuple!(Attribute(21), Attribute(42), Attribute(84)); + static assert(Cmp == UDATuple!(Attribute, symbol)); + static assert(!is(UDATuple!(Attribute, wrong))); +} + +/// Avoid repeating the same error message again and again. +/// ---- +/// if (!__ctfe) +/// assert(0, onlyAsUda!func); +/// ---- +template onlyAsUda(string from /*= __FUNCTION__*/) +{ + // With default param, DMD think expression is void, even when writing 'enum string onlyAsUda = ...' + enum onlyAsUda = from~" must only be used as an attribute - not called as a runtime function."; +} diff --git a/source/dub/package_.d b/source/dub/package_.d index ee1b6a0..9e88c2d 100644 --- a/source/dub/package_.d +++ b/source/dub/package_.d @@ -11,6 +11,7 @@ import dub.compilers.compiler; import dub.dependency; +import dub.description; import dub.recipe.json; import dub.recipe.sdl; @@ -286,7 +287,7 @@ logDiagnostic("Using custom build type '%s'.", build_type); pbt.getPlatformSettings(settings, platform, this.path); } else { - with(BuildOptions) switch (build_type) { + with(BuildOption) switch (build_type) { default: throw new Exception(format("Unknown build type for %s: '%s'", this.name, build_type)); case "plain": break; case "debug": settings.addOptions(debugMode, debugInfo); break; @@ -360,61 +361,78 @@ return false; } - void describe(ref Json dst, BuildPlatform platform, string config) - { - dst.path = m_path.toNativeString(); - dst.name = this.name; - dst["version"] = this.vers; - dst.description = m_info.description; - dst.homepage = m_info.homepage; - dst.authors = m_info.authors.serializeToJson(); - dst.copyright = m_info.copyright; - dst.license = m_info.license; - dst.dependencies = m_info.dependencies.keys.serializeToJson(); + /** Returns a description of the package for use in IDEs or build tools. + */ + PackageDescription describe(BuildPlatform platform, string config) + const { + PackageDescription ret; + ret.path = m_path.toNativeString(); + ret.name = this.name; + ret.version_ = this.ver; + ret.description = m_info.description; + ret.homepage = m_info.homepage; + ret.authors = m_info.authors.dup; + ret.copyright = m_info.copyright; + ret.license = m_info.license; + ret.dependencies = getDependencies(config).keys; // save build settings BuildSettings bs = getBuildSettings(platform, config); BuildSettings allbs = getCombinedBuildSettings(); - foreach (string k, v; bs.serializeToJson()) dst[k] = v; - dst.remove("requirements"); - dst.remove("sourceFiles"); - dst.remove("importFiles"); - dst.remove("stringImportFiles"); - dst.targetType = bs.targetType.to!string(); - if (dst.targetType != TargetType.none) - dst.targetFileName = getTargetFileName(bs, platform); + ret.targetType = bs.targetType; + ret.targetPath = bs.targetPath; + ret.targetName = bs.targetName; + if (ret.targetType != TargetType.none) + ret.targetFileName = getTargetFileName(bs, platform); + ret.workingDirectory = bs.workingDirectory; + ret.mainSourceFile = bs.mainSourceFile; + ret.dflags = bs.dflags; + ret.lflags = bs.lflags; + ret.libs = bs.libs; + ret.copyFiles = bs.copyFiles; + ret.versions = bs.versions; + ret.debugVersions = bs.debugVersions; + ret.importPaths = bs.importPaths; + ret.stringImportPaths = bs.stringImportPaths; + ret.preGenerateCommands = bs.preGenerateCommands; + ret.postGenerateCommands = bs.postGenerateCommands; + ret.preBuildCommands = bs.preBuildCommands; + ret.postBuildCommands = bs.postBuildCommands; // prettify build requirements output - Json[] breqs; - for (int i = 1; i <= BuildRequirements.max; i <<= 1) - if (bs.requirements & i) - breqs ~= Json(to!string(cast(BuildRequirements)i)); - dst.buildRequirements = breqs; + for (int i = 1; i <= BuildRequirement.max; i <<= 1) + if (bs.requirements & cast(BuildRequirement)i) + ret.buildRequirements ~= cast(BuildRequirement)i; // prettify options output - Json[] bopts; - for (int i = 1; i <= BuildOptions.max; i <<= 1) - if (bs.options & i) - bopts ~= Json(to!string(cast(BuildOptions)i)); - dst.options = bopts; + for (int i = 1; i <= BuildOption.max; i <<= 1) + if (bs.options & cast(BuildOption)i) + ret.options ~= cast(BuildOption)i; // collect all possible source files and determine their types - string[string] sourceFileTypes; - foreach (f; allbs.stringImportFiles) sourceFileTypes[f] = "unusedStringImport"; - foreach (f; allbs.importFiles) sourceFileTypes[f] = "unusedImport"; - foreach (f; allbs.sourceFiles) sourceFileTypes[f] = "unusedSource"; - foreach (f; bs.stringImportFiles) sourceFileTypes[f] = "stringImport"; - foreach (f; bs.importFiles) sourceFileTypes[f] = "import"; - foreach (f; bs.sourceFiles) sourceFileTypes[f] = "source"; - Json[] files; + SourceFileRole[string] sourceFileTypes; + foreach (f; allbs.stringImportFiles) sourceFileTypes[f] = SourceFileRole.unusedStringImport; + foreach (f; allbs.importFiles) sourceFileTypes[f] = SourceFileRole.unusedImport; + foreach (f; allbs.sourceFiles) sourceFileTypes[f] = SourceFileRole.unusedSource; + foreach (f; bs.stringImportFiles) sourceFileTypes[f] = SourceFileRole.stringImport; + foreach (f; bs.importFiles) sourceFileTypes[f] = SourceFileRole.import_; + foreach (f; bs.sourceFiles) sourceFileTypes[f] = SourceFileRole.source; foreach (f; sourceFileTypes.byKey.array.sort()) { - auto jf = Json.emptyObject; - jf["path"] = f; - jf["type"] = sourceFileTypes[f]; - files ~= jf; + SourceFileDescription sf; + sf.path = f; + sf.type = sourceFileTypes[f]; + ret.files ~= sf; } - dst.files = Json(files); + + return ret; + } + // ditto + deprecated void describe(ref Json dst, BuildPlatform platform, string config) + { + auto res = describe(platform, config); + foreach (string key, value; res.serializeToJson()) + dst[key] = value; } private void fillWithDefaults() @@ -571,7 +589,6 @@ } } - private string determineVersionFromSCM(Path path) { import std.process; diff --git a/source/dub/project.d b/source/dub/project.d index 36191a9..9da370a 100644 --- a/source/dub/project.d +++ b/source/dub/project.d @@ -9,6 +9,7 @@ import dub.compilers.compiler; import dub.dependency; +import dub.description; import dub.internal.utils; import dub.internal.vibecompat.core.file; import dub.internal.vibecompat.core.log; @@ -530,7 +531,7 @@ void addBuildTypeSettings(ref BuildSettings dst, in BuildPlatform platform, string build_type) { - bool usedefflags = !(dst.requirements & BuildRequirements.noDefaultFlags); + bool usedefflags = !(dst.requirements & BuildRequirement.noDefaultFlags); if (usedefflags) { BuildSettings btsettings; m_rootPackage.addBuildTypeSettings(btsettings, platform, build_type); @@ -571,66 +572,371 @@ return all_found; }*/ - /// Outputs a JSON description of the project, including its deoendencies. - void describe(ref Json dst, BuildPlatform platform, string config) + /// Outputs a build description of the project, including its dependencies. + ProjectDescription describe(BuildPlatform platform, string config, string build_type = null) { - dst.mainPackage = m_rootPackage.name; // deprecated - dst.rootPackage = m_rootPackage.name; + import dub.generators.targetdescription; + // store basic build parameters + ProjectDescription ret; + ret.rootPackage = m_rootPackage.name; + ret.configuration = config; + ret.buildType = build_type; + ret.compiler = platform.compiler; + ret.architecture = platform.architecture; + ret.platform = platform.platform; + + // collect high level information about projects (useful for IDE display) auto configs = getPackageConfigs(platform, config); + ret.packages ~= m_rootPackage.describe(platform, config); + foreach (dep; m_dependencies) + ret.packages ~= dep.describe(platform, configs[dep.name]); - // FIXME: use the generator system to collect the list of actually used build dependencies and source files - - auto mp = Json.emptyObject; - m_rootPackage.describe(mp, platform, config); - dst.packages = Json([mp]); - - foreach (dep; m_dependencies) { - auto dp = Json.emptyObject; - dep.describe(dp, platform, configs[dep.name]); - dst.packages = dst.packages.get!(Json[]) ~ dp; + if (build_type.length) { + // collect build target information (useful for build tools) + GeneratorSettings settings; + settings.platform = platform; + settings.compiler = getCompiler(platform.compilerBinary); + settings.config = config; + settings.buildType = build_type; + auto gen = new TargetDescriptionGenerator(this); + try { + gen.generate(settings); + ret.targets = gen.targetDescriptions; + ret.targetLookup = gen.targetDescriptionLookup; + } catch (Exception e) { + logDiagnostic("Skipping targets description: %s", e.msg); + logDebug("Full error: %s", e.toString().sanitize); + } } + + return ret; + } + /// ditto + deprecated void describe(ref Json dst, BuildPlatform platform, string config) + { + auto desc = describe(platform, config); + foreach (string key, value; desc.serializeToJson()) + dst[key] = value; } - private string[] listPaths(string attributeName)(BuildPlatform platform, string config) + private string[] listBuildSetting(string attributeName)(BuildPlatform platform, + string config, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping) { - import std.path : buildPath, dirSeparator; + return listBuildSetting!attributeName(platform, getPackageConfigs(platform, config), + projectDescription, compiler, disableEscaping); + } + + private string[] listBuildSetting(string attributeName)(BuildPlatform platform, + string[string] configs, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping) + { + if (compiler) + return formatBuildSettingCompiler!attributeName(platform, configs, projectDescription, compiler, disableEscaping); + else + return formatBuildSettingPlain!attributeName(platform, configs, projectDescription); + } + + // Output a build setting formatted for a compiler + private string[] formatBuildSettingCompiler(string attributeName)(BuildPlatform platform, + string[string] configs, ProjectDescription projectDescription, Compiler compiler, bool disableEscaping) + { + import std.process : escapeShellFileName; + import std.path : dirSeparator; - auto configs = getPackageConfigs(platform, config); + assert(compiler); + + auto targetDescription = projectDescription.lookupTarget(projectDescription.rootPackage); + auto buildSettings = targetDescription.buildSettings; + + string[] values; + switch (attributeName) + { + case "dflags": + case "linkerFiles": + case "mainSourceFile": + case "importFiles": + values = formatBuildSettingPlain!attributeName(platform, configs, projectDescription); + break; + + case "lflags": + case "sourceFiles": + case "versions": + case "debugVersions": + case "importPaths": + case "stringImportPaths": + case "options": + auto bs = buildSettings.dup; + bs.dflags = null; + + // Ensure trailing slash on directory paths + auto ensureTrailingSlash = (string path) => path.endsWith(dirSeparator) ? path : path ~ dirSeparator; + static if (attributeName == "importPaths") + bs.importPaths = bs.importPaths.map!(ensureTrailingSlash).array(); + else static if (attributeName == "stringImportPaths") + bs.stringImportPaths = bs.stringImportPaths.map!(ensureTrailingSlash).array(); + + compiler.prepareBuildSettings(bs, BuildSetting.all & ~to!BuildSetting(attributeName)); + values = bs.dflags; + break; + + case "libs": + auto bs = buildSettings.dup; + bs.dflags = null; + bs.lflags = null; + bs.sourceFiles = null; + bs.targetType = TargetType.none; // Force Compiler to NOT omit dependency libs when package is a library. + + compiler.prepareBuildSettings(bs, BuildSetting.all & ~to!BuildSetting(attributeName)); + + if (bs.lflags) + values = bs.lflags; + else if (bs.sourceFiles) + values = bs.sourceFiles; + else + values = bs.dflags; + + break; + + default: assert(0); + } + + // Escape filenames and paths + if(!disableEscaping) + { + switch (attributeName) + { + case "mainSourceFile": + case "linkerFiles": + case "copyFiles": + case "importFiles": + case "stringImportFiles": + case "sourceFiles": + case "importPaths": + case "stringImportPaths": + return values.map!(escapeShellFileName).array(); + + default: + return values; + } + } + + return values; + } + + // Output a build setting without formatting for any particular compiler + private string[] formatBuildSettingPlain(string attributeName)(BuildPlatform platform, string[string] configs, ProjectDescription projectDescription) + { + import std.path : buildNormalizedPath, dirSeparator; + import std.range : only; string[] list; + + auto targetDescription = projectDescription.lookupTarget(projectDescription.rootPackage); + auto buildSettings = targetDescription.buildSettings; + + // Return any BuildSetting member attributeName as a range of strings. Don't attempt to fixup values. + // allowEmptyString: When the value is a string (as opposed to string[]), + // is empty string an actual permitted value instead of + // a missing value? + auto getRawBuildSetting(Package pack, bool allowEmptyString) { + auto value = __traits(getMember, buildSettings, attributeName); + + static if( is(typeof(value) == string[]) ) + return value; + else static if( is(typeof(value) == string) ) + { + auto ret = only(value); - auto fullPackagePaths(Package pack) { - // Return full paths for the import paths, making sure a - // directory separator is on the end of each path. - return __traits(getMember, pack.getBuildSettings(platform, configs[pack.name]), attributeName) - .map!(importPath => buildPath(pack.path.toString(), importPath)) - .map!(path => path.endsWith(dirSeparator) ? path : path ~ dirSeparator); - } + // only() has a different return type from only(value), so we + // have to empty the range rather than just returning only(). + if(value.empty && !allowEmptyString) { + ret.popFront(); + assert(ret.empty); + } - foreach(path; fullPackagePaths(m_rootPackage)) { - list ~= path; - } - - foreach (dep; m_dependencies) { - foreach(path; fullPackagePaths(dep)) { - list ~= path; + return ret; } + else static if( is(typeof(value) == enum) ) + return only(value); + else static if( is(typeof(value) == BuildRequirements) ) + return only(cast(BuildRequirement) cast(int) value.values); + else static if( is(typeof(value) == BuildOptions) ) + return only(cast(BuildOption) cast(int) value.values); + else + static assert(false, "Type of BuildSettings."~attributeName~" is unsupported."); + } + + // Adjust BuildSetting member attributeName as needed. + // Returns a range of strings. + auto getFixedBuildSetting(Package pack) { + // Is relative path(s) to a directory? + enum isRelativeDirectory = + attributeName == "importPaths" || attributeName == "stringImportPaths" || + attributeName == "targetPath" || attributeName == "workingDirectory"; + + // Is relative path(s) to a file? + enum isRelativeFile = + attributeName == "sourceFiles" || attributeName == "linkerFiles" || + attributeName == "importFiles" || attributeName == "stringImportFiles" || + attributeName == "copyFiles" || attributeName == "mainSourceFile"; + + // For these, empty string means "main project directory", not "missing value" + enum allowEmptyString = + attributeName == "targetPath" || attributeName == "workingDirectory"; + + enum isEnumBitfield = + attributeName == "targetType" || attributeName == "requirements" || + attributeName == "options"; + + auto values = getRawBuildSetting(pack, allowEmptyString); + auto fixRelativePath = (string importPath) => buildNormalizedPath(pack.path.toString(), importPath); + auto ensureTrailingSlash = (string path) => path.endsWith(dirSeparator) ? path : path ~ dirSeparator; + + static if(isRelativeDirectory) { + // Return full paths for the paths, making sure a + // directory separator is on the end of each path. + return values.map!(fixRelativePath).map!(ensureTrailingSlash); + } + else static if(isRelativeFile) { + // Return full paths. + return values.map!(fixRelativePath); + } + else static if(isEnumBitfield) + return bitFieldNames(values.front); + else + return values; + } + + foreach(value; getFixedBuildSetting(m_rootPackage)) { + list ~= value; } return list; } - /// Outputs the import paths for the project, including its dependencies. - string [] listImportPaths(BuildPlatform platform, string config) + // The "compiler" arg is for choosing which compiler the output should be formatted for, + // or null to imply "list" format. + private string[] listBuildSetting(BuildPlatform platform, string[string] configs, + ProjectDescription projectDescription, string requestedData, Compiler compiler, bool disableEscaping) { - return listPaths!"importPaths"(platform, config); + // Certain data cannot be formatter for a compiler + if (compiler) + { + switch (requestedData) + { + case "target-type": + case "target-path": + case "target-name": + case "working-directory": + case "string-import-files": + case "copy-files": + case "pre-generate-commands": + case "post-generate-commands": + case "pre-build-commands": + case "post-build-commands": + enforce(false, "--data="~requestedData~" can only be used with --data-format=list."); + break; + + case "requirements": + enforce(false, "--data=requirements can only be used with --data-format=list. Use --data=options instead."); + break; + + default: break; + } + } + + import std.typetuple : TypeTuple; + auto args = TypeTuple!(platform, configs, projectDescription, compiler, disableEscaping); + switch (requestedData) + { + case "target-type": return listBuildSetting!"targetType"(args); + case "target-path": return listBuildSetting!"targetPath"(args); + case "target-name": return listBuildSetting!"targetName"(args); + case "working-directory": return listBuildSetting!"workingDirectory"(args); + case "main-source-file": return listBuildSetting!"mainSourceFile"(args); + case "dflags": return listBuildSetting!"dflags"(args); + case "lflags": return listBuildSetting!"lflags"(args); + case "libs": return listBuildSetting!"libs"(args); + case "linker-files": return listBuildSetting!"linkerFiles"(args); + case "source-files": return listBuildSetting!"sourceFiles"(args); + case "copy-files": return listBuildSetting!"copyFiles"(args); + case "versions": return listBuildSetting!"versions"(args); + case "debug-versions": return listBuildSetting!"debugVersions"(args); + case "import-paths": return listBuildSetting!"importPaths"(args); + case "string-import-paths": return listBuildSetting!"stringImportPaths"(args); + case "import-files": return listBuildSetting!"importFiles"(args); + case "string-import-files": return listBuildSetting!"stringImportFiles"(args); + case "pre-generate-commands": return listBuildSetting!"preGenerateCommands"(args); + case "post-generate-commands": return listBuildSetting!"postGenerateCommands"(args); + case "pre-build-commands": return listBuildSetting!"preBuildCommands"(args); + case "post-build-commands": return listBuildSetting!"postBuildCommands"(args); + case "requirements": return listBuildSetting!"requirements"(args); + case "options": return listBuildSetting!"options"(args); + + default: + enforce(false, "--data="~requestedData~ + " is not a valid option. See 'dub describe --help' for accepted --data= values."); + } + + assert(0); + } + + /// Outputs requested data for the project, optionally including its dependencies. + string[] listBuildSettings(BuildPlatform platform, string config, string buildType, + string[] requestedData, Compiler formattingCompiler, bool nullDelim) + { + auto projectDescription = describe(platform, config, buildType); + auto configs = getPackageConfigs(platform, config); + PackageDescription packageDescription; + foreach (pack; projectDescription.packages) { + if (pack.name == projectDescription.rootPackage) + packageDescription = pack; + } + + // Copy linker files from sourceFiles to linkerFiles + auto target = projectDescription.lookupTarget(projectDescription.rootPackage); + foreach (file; target.buildSettings.sourceFiles.filter!(isLinkerFile)) + target.buildSettings.addLinkerFiles(file); + + // Remove linker files from sourceFiles + target.buildSettings.sourceFiles = + target.buildSettings.sourceFiles + .filter!(a => !isLinkerFile(a)) + .array(); + projectDescription.lookupTarget(projectDescription.rootPackage) = target; + + // Genrate results + if (formattingCompiler) + { + // Format for a compiler + return [ + requestedData + .map!(dataName => listBuildSetting(platform, configs, projectDescription, dataName, formattingCompiler, nullDelim)) + .join().join(nullDelim? "\0" : " ") + ]; + } + else + { + // Format list-style + return requestedData + .map!(dataName => listBuildSetting(platform, configs, projectDescription, dataName, null, nullDelim)) + .joiner([""]) // Blank entry between each type of requestedData + .array(); + } + } + + /// Outputs the import paths for the project, including its dependencies. + string[] listImportPaths(BuildPlatform platform, string config, string buildType, bool nullDelim) + { + auto projectDescription = describe(platform, config, buildType); + return listBuildSetting!"importPaths"(platform, config, projectDescription, null, nullDelim); } /// Outputs the string import paths for the project, including its dependencies. - string[] listStringImportPaths(BuildPlatform platform, string config) + string[] listStringImportPaths(BuildPlatform platform, string config, string buildType, bool nullDelim) { - return listPaths!"stringImportPaths"(platform, config); + auto projectDescription = describe(platform, config, buildType); + return listBuildSetting!"stringImportPaths"(platform, config, projectDescription, null, nullDelim); } void saveSelections() @@ -675,7 +981,7 @@ { logDebug("markUpToDate"); Json create(ref Json json, string object) { - if( object !in json ) json[object] = Json.emptyObject; + if (json[object].type == Json.Type.undefined) json[object] = Json.emptyObject; return json[object]; } create(m_packageSettings, "dub"); diff --git a/source/dub/recipe/json.d b/source/dub/recipe/json.d index 2ffe0a9..24cb3f7 100644 --- a/source/dub/recipe/json.d +++ b/source/dub/recipe/json.d @@ -221,13 +221,13 @@ case "buildRequirements": BuildRequirements reqs; foreach (req; deserializeJson!(string[])(value)) - reqs |= to!BuildRequirements(req); + reqs |= to!BuildRequirement(req); bs.buildRequirements[suffix] = reqs; break; case "buildOptions": BuildOptions options; foreach (opt; deserializeJson!(string[])(value)) - options |= to!BuildOptions(opt); + options |= to!BuildOption(opt); bs.buildOptions[suffix] = options; break; } @@ -266,13 +266,13 @@ foreach (suffix, arr; bs.postBuildCommands) ret["postBuildCommands"~suffix] = serializeToJson(arr); foreach (suffix, arr; bs.buildRequirements) { string[] val; - foreach (i; [EnumMembers!BuildRequirements]) + foreach (i; [EnumMembers!BuildRequirement]) if (arr & i) val ~= to!string(i); ret["buildRequirements"~suffix] = serializeToJson(val); } foreach (suffix, arr; bs.buildOptions) { string[] val; - foreach (i; [EnumMembers!BuildOptions]) + foreach (i; [EnumMembers!BuildOption]) if (arr & i) val ~= to!string(i); ret["buildOptions"~suffix] = serializeToJson(val); } diff --git a/source/dub/recipe/packagerecipe.d b/source/dub/recipe/packagerecipe.d index d844424..0a26059 100644 --- a/source/dub/recipe/packagerecipe.d +++ b/source/dub/recipe/packagerecipe.d @@ -235,8 +235,8 @@ auto nodef = false; auto noprop = false; foreach (req; this.buildRequirements) { - if (req & BuildRequirements.noDefaultFlags) nodef = true; - if (req & BuildRequirements.relaxProperties) noprop = true; + if (req & BuildRequirement.noDefaultFlags) nodef = true; + if (req & BuildRequirement.relaxProperties) noprop = true; } if (noprop) { diff --git a/source/dub/recipe/sdl.d b/source/dub/recipe/sdl.d index 5b1b173..6a82abb 100644 --- a/source/dub/recipe/sdl.d +++ b/source/dub/recipe/sdl.d @@ -112,8 +112,8 @@ case "postGenerateCommands": setting.parsePlatformStringArray(bs.postGenerateCommands); break; case "preBuildCommands": setting.parsePlatformStringArray(bs.preBuildCommands); break; case "postBuildCommands": setting.parsePlatformStringArray(bs.postBuildCommands); break; - case "buildRequirements": setting.parsePlatformEnumArray!BuildRequirements(bs.buildRequirements); break; - case "buildOptions": setting.parsePlatformEnumArray!BuildOptions(bs.buildOptions); break; + case "buildRequirements": setting.parsePlatformEnumArray!BuildRequirement(bs.buildRequirements); break; + case "buildOptions": setting.parsePlatformEnumArray!BuildOption(bs.buildOptions); break; } } @@ -205,8 +205,10 @@ string platform; if ("platform" in t.attributes) platform = t.attributes["platform"][0].value.get!string; - foreach (v; t.values) + foreach (v; t.values) { + if (platform !in dst) dst[platform] = Es.init; dst[platform] |= v.get!string.to!E; + } } private void enforceSDL(bool condition, lazy string message, Tag tag, string file = __FILE__, int line = __LINE__) @@ -334,8 +336,8 @@ assert(rec.buildSettings.workingDirectory == "working directory"); assert(rec.buildSettings.subConfigurations.length == 1); assert(rec.buildSettings.subConfigurations["projectname:subpackage2"] == "library"); - assert(rec.buildSettings.buildRequirements == ["": BuildRequirements.allowWarnings | BuildRequirements.silenceDeprecations]); - assert(rec.buildSettings.buildOptions == ["": BuildOptions.verbose | BuildOptions.ignoreUnknownPragmas]); + assert(rec.buildSettings.buildRequirements == ["": cast(BuildRequirements)(BuildRequirement.allowWarnings | BuildRequirement.silenceDeprecations)]); + assert(rec.buildSettings.buildOptions == ["": cast(BuildOptions)(BuildOption.verbose | BuildOption.ignoreUnknownPragmas)]); assert(rec.buildSettings.libs == ["": ["lib1", "lib2", "lib3"]]); assert(rec.buildSettings.sourceFiles == ["": ["source1", "source2", "source3"]]); assert(rec.buildSettings.sourcePaths == ["": ["sourcepath1", "sourcepath2", "sourcepath3"]]); diff --git a/test/4-describe-data-1-list.sh b/test/4-describe-data-1-list.sh new file mode 100755 index 0000000..021188f --- /dev/null +++ b/test/4-describe-data-1-list.sh @@ -0,0 +1,139 @@ +#!/bin/bash + +set -e -o pipefail + +cd "$CURR_DIR"/describe-project + +temp_file=`mktemp` + +function cleanup { + rm $temp_file +} + +trap cleanup EXIT + +if ! $DUB describe --compiler=$COMPILER --data-list \ + '--data= target-type , target-path , target-name ' \ + '--data= working-directory ' \ + --data=main-source-file \ + '--data=dflags,lflags' \ + '--data=libs, linker-files' \ + '--data=source-files, copy-files' \ + '--data=versions, debug-versions' \ + --data=import-paths \ + --data=string-import-paths \ + --data=import-files \ + --data=string-import-files \ + --data=pre-generate-commands \ + --data=post-generate-commands \ + --data=pre-build-commands \ + --data=post-build-commands \ + '--data=requirements, options' \ + > "$temp_file"; then + die 'Printing project data failed!' +fi + +# Create the expected output path file to compare against. +expected_file="$CURR_DIR/expected-describe-data-1-list-output" +# --data=target-type +echo "executable" > "$expected_file" +echo >> "$expected_file" +# --data=target-path +echo "$CURR_DIR/describe-project/" >> "$expected_file" +echo >> "$expected_file" +# --data=target-name +echo "describe-project" >> "$expected_file" +echo >> "$expected_file" +# --data=working-directory +echo "$CURR_DIR/describe-project/" >> "$expected_file" +echo >> "$expected_file" +# --data=main-source-file +echo "$CURR_DIR/describe-project/src/dummy.d" >> "$expected_file" +echo >> "$expected_file" +# --data=dflags +echo "--some-dflag" >> "$expected_file" +echo "--another-dflag" >> "$expected_file" +echo >> "$expected_file" +# --data=lflags +echo "--some-lflag" >> "$expected_file" +echo "--another-lflag" >> "$expected_file" +echo >> "$expected_file" +# --data=libs +echo "crypto" >> "$expected_file" +echo "curl" >> "$expected_file" +echo >> "$expected_file" +# --data=linker-files +echo "$CURR_DIR/describe-dependency-3/libdescribe-dependency-3.a" >> "$expected_file" +echo "$CURR_DIR/describe-project/some.a" >> "$expected_file" +echo "$CURR_DIR/describe-dependency-1/dep.a" >> "$expected_file" +echo >> "$expected_file" +# --data=source-files +echo "$CURR_DIR/describe-project/src/dummy.d" >> "$expected_file" +echo "$CURR_DIR/describe-dependency-1/source/dummy.d" >> "$expected_file" +echo >> "$expected_file" +# --data=copy-files +echo "$CURR_DIR/describe-project/data/dummy.dat" >> "$expected_file" +echo "$CURR_DIR/describe-dependency-1/data/*" >> "$expected_file" +echo >> "$expected_file" +# --data=versions +echo "someVerIdent" >> "$expected_file" +echo "anotherVerIdent" >> "$expected_file" +echo "Have_describe_project" >> "$expected_file" +echo "Have_describe_dependency_1" >> "$expected_file" +echo "Have_describe_dependency_2" >> "$expected_file" +echo "Have_describe_dependency_3" >> "$expected_file" +echo >> "$expected_file" +# --data=debug-versions +echo "someDebugVerIdent" >> "$expected_file" +echo "anotherDebugVerIdent" >> "$expected_file" +echo >> "$expected_file" +# --data=import-paths +echo "$CURR_DIR/describe-project/src/" >> "$expected_file" +echo "$CURR_DIR/describe-dependency-1/source/" >> "$expected_file" +echo "$CURR_DIR/describe-dependency-2/some-path/" >> "$expected_file" +echo "$CURR_DIR/describe-dependency-3/dep3-source/" >> "$expected_file" +echo >> "$expected_file" +# --data=string-import-paths +echo "$CURR_DIR/describe-project/views/" >> "$expected_file" +echo "$CURR_DIR/describe-dependency-2/some-extra-string-import-path/" >> "$expected_file" +echo "$CURR_DIR/describe-dependency-3/dep3-string-import-path/" >> "$expected_file" +echo >> "$expected_file" +# --data=import-files +echo "$CURR_DIR/describe-dependency-2/some-path/dummy.d" >> "$expected_file" +echo >> "$expected_file" +# --data=string-import-files +echo "$CURR_DIR/describe-project/views/dummy.d" >> "$expected_file" +#echo "$CURR_DIR/describe-dependency-2/some-extra-string-import-path/dummy.d" >> "$expected_file" # This is missing from result, is that a bug? +echo >> "$expected_file" +# --data=pre-generate-commands +echo "./do-preGenerateCommands.sh" >> "$expected_file" +echo "../describe-dependency-1/dependency-preGenerateCommands.sh" >> "$expected_file" +echo >> "$expected_file" +# --data=post-generate-commands +echo "./do-postGenerateCommands.sh" >> "$expected_file" +echo "../describe-dependency-1/dependency-postGenerateCommands.sh" >> "$expected_file" +echo >> "$expected_file" +# --data=pre-build-commands +echo "./do-preBuildCommands.sh" >> "$expected_file" +echo "../describe-dependency-1/dependency-preBuildCommands.sh" >> "$expected_file" +echo >> "$expected_file" +# --data=post-build-commands +echo "./do-postBuildCommands.sh" >> "$expected_file" +echo "../describe-dependency-1/dependency-postBuildCommands.sh" >> "$expected_file" +echo >> "$expected_file" +# --data=requirements +echo "allowWarnings" >> "$expected_file" +echo "disallowInlining" >> "$expected_file" +#echo "requireContracts" >> "$expected_file" # Not sure if this (from a sourceLib dependency) should be missing from the result +echo >> "$expected_file" +# --data=options +echo "debugMode" >> "$expected_file" +echo "releaseMode" >> "$expected_file" +echo "debugInfo" >> "$expected_file" +echo "warnings" >> "$expected_file" +#echo "stackStomping" >> "$expected_file" # Not sure if this (from a sourceLib dependency) should be missing from the result + +if ! diff "$expected_file" "$temp_file"; then + die 'The project data did not match the expected output!' +fi + diff --git a/test/4-describe-data-2-dmd.sh b/test/4-describe-data-2-dmd.sh new file mode 100755 index 0000000..af0681c --- /dev/null +++ b/test/4-describe-data-2-dmd.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +set -e -o pipefail + +if ! dmd --help >/dev/null; then + echo Skipping DMD-centric test on configuration that lacks DMD. + exit +fi + +cd "$CURR_DIR"/describe-project + +temp_file=`mktemp` + +function cleanup { + rm $temp_file +} + +trap cleanup EXIT + +if ! $DUB describe --compiler=dmd \ + --data=main-source-file \ + --data=dflags,lflags \ + --data=libs,linker-files \ + --data=source-files \ + --data=versions \ + --data=debug-versions \ + --data=import-paths \ + --data=string-import-paths \ + --data=import-files \ + --data=options \ + > "$temp_file"; then + die 'Printing project data failed!' +fi + +# Create the expected output path file to compare against. +expected_file="$CURR_DIR/expected-describe-data-2-dmd-output" +# --data=main-source-file +echo -n "'$CURR_DIR/describe-project/src/dummy.d' " > "$expected_file" +# --data=dflags +echo -n "--some-dflag " >> "$expected_file" +echo -n "--another-dflag " >> "$expected_file" +# --data=lflags +echo -n "-L--some-lflag " >> "$expected_file" +echo -n "-L--another-lflag " >> "$expected_file" +# --data=libs +echo -n "-lcrypto " >> "$expected_file" +echo -n "-lcurl " >> "$expected_file" +# --data=linker-files +echo -n "'$CURR_DIR/describe-dependency-3/libdescribe-dependency-3.a' " >> "$expected_file" +echo -n "'$CURR_DIR/describe-project/some.a' " >> "$expected_file" +echo -n "'$CURR_DIR/describe-dependency-1/dep.a' " >> "$expected_file" +# --data=source-files +echo -n "'$CURR_DIR/describe-project/src/dummy.d' " >> "$expected_file" +echo -n "'$CURR_DIR/describe-dependency-1/source/dummy.d' " >> "$expected_file" +# --data=versions +echo -n "-version=someVerIdent " >> "$expected_file" +echo -n "-version=anotherVerIdent " >> "$expected_file" +echo -n "-version=Have_describe_project " >> "$expected_file" +echo -n "-version=Have_describe_dependency_1 " >> "$expected_file" +echo -n "-version=Have_describe_dependency_2 " >> "$expected_file" +echo -n "-version=Have_describe_dependency_3 " >> "$expected_file" +# --data=debug-versions +echo -n "-debug=someDebugVerIdent " >> "$expected_file" +echo -n "-debug=anotherDebugVerIdent " >> "$expected_file" +# --data=import-paths +echo -n "'-I$CURR_DIR/describe-project/src/' " >> "$expected_file" +echo -n "'-I$CURR_DIR/describe-dependency-1/source/' " >> "$expected_file" +echo -n "'-I$CURR_DIR/describe-dependency-2/some-path/' " >> "$expected_file" +echo -n "'-I$CURR_DIR/describe-dependency-3/dep3-source/' " >> "$expected_file" +# --data=string-import-paths +echo -n "'-J$CURR_DIR/describe-project/views/' " >> "$expected_file" +echo -n "'-J$CURR_DIR/describe-dependency-2/some-extra-string-import-path/' " >> "$expected_file" +echo -n "'-J$CURR_DIR/describe-dependency-3/dep3-string-import-path/' " >> "$expected_file" +# --data=import-files +echo -n "'$CURR_DIR/describe-dependency-2/some-path/dummy.d' " >> "$expected_file" +# --data=options +echo -n "-debug " >> "$expected_file" +echo -n "-release " >> "$expected_file" +echo -n "-g " >> "$expected_file" +echo -n "-wi" >> "$expected_file" +#echo -n "-gx " >> "$expected_file" # Not sure if this (from a sourceLib dependency) should be missing from the result +echo "" >> "$expected_file" + +if ! diff "$expected_file" "$temp_file"; then + die 'The project data did not match the expected output!' +fi + diff --git a/test/4-describe-data-3-zero-delim.sh b/test/4-describe-data-3-zero-delim.sh new file mode 100755 index 0000000..68a749b --- /dev/null +++ b/test/4-describe-data-3-zero-delim.sh @@ -0,0 +1,127 @@ +#!/bin/bash + +set -e -o pipefail + +cd "$CURR_DIR"/describe-project + +temp_file_normal=`mktemp` +temp_file_zero_delim=`mktemp` + +function cleanup { + rm $temp_file_normal + rm $temp_file_zero_delim +} + +trap cleanup EXIT + +# Test list-style project data +if ! $DUB describe --compiler=$COMPILER --data-list \ + --data=target-type \ + --data=target-path \ + --data=target-name \ + --data=working-directory \ + --data=main-source-file \ + --data=dflags \ + --data=lflags \ + --data=libs \ + --data=linker-files \ + --data=source-files \ + --data=copy-files \ + --data=versions \ + --data=debug-versions \ + --data=import-paths \ + --data=string-import-paths \ + --data=import-files \ + --data=string-import-files \ + --data=pre-generate-commands \ + --data=post-generate-commands \ + --data=pre-build-commands \ + --data=post-build-commands \ + --data=requirements \ + --data=options \ + > "$temp_file_normal"; then + die 'Printing list-style project data failed!' +fi + +if ! $DUB describe --compiler=$COMPILER --data-0 --data-list \ + --data=target-type \ + --data=target-path \ + --data=target-name \ + --data=working-directory \ + --data=main-source-file \ + --data=dflags \ + --data=lflags \ + --data=libs \ + --data=linker-files \ + --data=source-files \ + --data=copy-files \ + --data=versions \ + --data=debug-versions \ + --data=import-paths \ + --data=string-import-paths \ + --data=import-files \ + --data=string-import-files \ + --data=pre-generate-commands \ + --data=post-generate-commands \ + --data=pre-build-commands \ + --data=post-build-commands \ + --data=requirements \ + --data=options \ + | xargs -0 printf "%s\n" > "$temp_file_zero_delim"; then + die 'Printing null-delimited list-style project data failed!' +fi + +if ! diff -Z "$temp_file_normal" "$temp_file_zero_delim"; then + die 'The null-delimited list-style project data did not match the expected output!' +fi + +# Test --import-paths +if ! $DUB describe --compiler=$COMPILER --import-paths \ + > "$temp_file_normal"; then + die 'Printing --import-paths failed!' +fi + +if ! $DUB describe --compiler=$COMPILER --data-0 --import-paths \ + | xargs -0 printf "%s\n" > "$temp_file_zero_delim"; then + die 'Printing null-delimited --import-paths failed!' +fi + +if ! diff -Z -B "$temp_file_normal" "$temp_file_zero_delim"; then + die 'The null-delimited --import-paths data did not match the expected output!' +fi + +# DMD-only beyond this point +if ! dmd --help >/dev/null; then + echo Skipping DMD-centric tests on configuration that lacks DMD. + exit +fi + +# Test dmd-style --data=versions +if ! $DUB describe --compiler=dmd --data=versions \ + > "$temp_file_normal"; then + die 'Printing dmd-style --data=versions failed!' +fi + +if ! $DUB describe --compiler=dmd --data-0 --data=versions \ + | xargs -0 printf "%s " > "$temp_file_zero_delim"; then + die 'Printing null-delimited dmd-style --data=versions failed!' +fi + +if ! diff -Z "$temp_file_normal" "$temp_file_zero_delim"; then + die 'The null-delimited dmd-style --data=versions did not match the expected output!' +fi + +# Test dmd-style --data=source-files +if ! $DUB describe --compiler=dmd --data=source-files \ + > "$temp_file_normal"; then + die 'Printing dmd-style --data=source-files failed!' +fi + +if ! $DUB describe --compiler=dmd --data-0 --data=source-files \ + | xargs -0 printf "'%s' " > "$temp_file_zero_delim"; then + die 'Printing null-delimited dmd-style --data=source-files failed!' +fi + +if ! diff -Z "$temp_file_normal" "$temp_file_zero_delim"; then + die 'The null-delimited dmd-style --data=source-files did not match the expected output!' +fi diff --git a/test/4-describe-import-paths.sh b/test/4-describe-import-paths.sh index b896daf..acbf736 100755 --- a/test/4-describe-import-paths.sh +++ b/test/4-describe-import-paths.sh @@ -20,6 +20,7 @@ echo "$CURR_DIR/describe-project/src/" > "$CURR_DIR/expected-import-path-output" echo "$CURR_DIR/describe-dependency-1/source/" >> "$CURR_DIR/expected-import-path-output" echo "$CURR_DIR/describe-dependency-2/some-path/" >> "$CURR_DIR/expected-import-path-output" +echo "$CURR_DIR/describe-dependency-3/dep3-source/" >> "$CURR_DIR/expected-import-path-output" if ! diff "$CURR_DIR"/expected-import-path-output "$temp_file"; then die 'The import paths did not match the expected output!' diff --git a/test/4-describe-string-import-paths.sh b/test/4-describe-string-import-paths.sh new file mode 100755 index 0000000..c2a3f23 --- /dev/null +++ b/test/4-describe-string-import-paths.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +set -e -o pipefail + +cd "$CURR_DIR"/describe-project + +temp_file=`mktemp` + +function cleanup { + rm $temp_file +} + +trap cleanup EXIT + +if ! $DUB describe --compiler=$COMPILER --string-import-paths > "$temp_file"; then + die 'Printing string import paths failed!' +fi + +# Create the expected output path file to compare against. +echo "$CURR_DIR/describe-project/views/" > "$CURR_DIR/expected-string-import-path-output" +echo "$CURR_DIR/describe-dependency-2/some-extra-string-import-path/" >> "$CURR_DIR/expected-string-import-path-output" +echo "$CURR_DIR/describe-dependency-3/dep3-string-import-path/" >> "$CURR_DIR/expected-string-import-path-output" + +if ! diff "$CURR_DIR"/expected-string-import-path-output "$temp_file"; then + die 'The string import paths did not match the expected output!' +fi + diff --git a/test/4-describe-string-importh-paths.sh b/test/4-describe-string-importh-paths.sh deleted file mode 100755 index 790af85..0000000 --- a/test/4-describe-string-importh-paths.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -set -e -o pipefail - -cd "$CURR_DIR"/describe-project - -temp_file=`mktemp` - -function cleanup { - rm $temp_file -} - -trap cleanup EXIT - -if ! $DUB describe --compiler=$COMPILER --string-import-paths > "$temp_file"; then - die 'Printing string import paths failed!' -fi - -# Create the expected output path file to compare against. -echo "$CURR_DIR/describe-project/views/" > "$CURR_DIR/expected-string-import-path-output" -echo "$CURR_DIR/describe-dependency-2/some-extra-string-import-path/" >> "$CURR_DIR/expected-string-import-path-output" - -if ! diff "$CURR_DIR"/expected-string-import-path-output "$temp_file"; then - die 'The string import paths did not match the expected output!' -fi - diff --git a/test/describe-dependency-1/data/dummy-dep1.dat b/test/describe-dependency-1/data/dummy-dep1.dat new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/test/describe-dependency-1/data/dummy-dep1.dat @@ -0,0 +1 @@ + diff --git a/test/describe-dependency-1/dependency-postGenerateCommands.sh b/test/describe-dependency-1/dependency-postGenerateCommands.sh new file mode 100755 index 0000000..1a24852 --- /dev/null +++ b/test/describe-dependency-1/dependency-postGenerateCommands.sh @@ -0,0 +1 @@ +#!/bin/sh diff --git a/test/describe-dependency-1/dependency-preGenerateCommands.sh b/test/describe-dependency-1/dependency-preGenerateCommands.sh new file mode 100755 index 0000000..1a24852 --- /dev/null +++ b/test/describe-dependency-1/dependency-preGenerateCommands.sh @@ -0,0 +1 @@ +#!/bin/sh diff --git a/test/describe-dependency-1/dub.json b/test/describe-dependency-1/dub.json index 236897d..fba1c01 100644 --- a/test/describe-dependency-1/dub.json +++ b/test/describe-dependency-1/dub.json @@ -6,9 +6,23 @@ "homepage": "fake.com", "license": "BSD 2-clause", "copyright": "Copyright © 2015, nobody", - "configurations": [ - { - "name": "my-dependency-1-config" - } - ], + "sourceFiles-posix": ["dep.a"], + "sourceFiles-windows": ["dep.lib"], + "dflags": ["--another-dflag"], + "lflags": ["--another-lflag"], + "libs": ["curl"], + "copyFiles": ["data/*"], + "versions": ["anotherVerIdent"], + "debugVersions": ["anotherDebugVerIdent"], + "preGenerateCommands": ["../describe-dependency-1/dependency-preGenerateCommands.sh"], + "postGenerateCommands": ["../describe-dependency-1/dependency-postGenerateCommands.sh"], + "preBuildCommands": ["../describe-dependency-1/dependency-preBuildCommands.sh"], + "postBuildCommands": ["../describe-dependency-1/dependency-postBuildCommands.sh"], + "buildRequirements": ["requireContracts"], + "buildOptions": ["stackStomping"], + "configurations": [ + { + "name": "my-dependency-1-config" + } + ], } diff --git a/test/describe-dependency-3/.no_build b/test/describe-dependency-3/.no_build new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/test/describe-dependency-3/.no_build @@ -0,0 +1 @@ + diff --git a/test/describe-dependency-3/dep3-source/dummy.d b/test/describe-dependency-3/dep3-source/dummy.d new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/test/describe-dependency-3/dep3-source/dummy.d @@ -0,0 +1 @@ + diff --git a/test/describe-dependency-3/dep3-string-import-path/dummy.d b/test/describe-dependency-3/dep3-string-import-path/dummy.d new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/test/describe-dependency-3/dep3-string-import-path/dummy.d @@ -0,0 +1 @@ + diff --git a/test/describe-dependency-3/dub.json b/test/describe-dependency-3/dub.json new file mode 100644 index 0000000..40f6991 --- /dev/null +++ b/test/describe-dependency-3/dub.json @@ -0,0 +1,13 @@ +{ + "name": "describe-dependency-3", + "targetType": "staticLibrary", + "description": "A test describe project", + "authors": ["nobody"], + "homepage": "fake.com", + "license": "BSD 2-clause", + "copyright": "Copyright © 2015, nobody", + "importPaths": ["dep3-source"], + "sourcePaths": ["dep3-source"], + "stringImportPaths": ["dep3-string-import-path"], + "buildOptions": ["profile"] +} diff --git a/test/describe-project/data/dummy.dat b/test/describe-project/data/dummy.dat new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/test/describe-project/data/dummy.dat @@ -0,0 +1 @@ + diff --git a/test/describe-project/do-postGenerateCommands.sh b/test/describe-project/do-postGenerateCommands.sh new file mode 100755 index 0000000..1a24852 --- /dev/null +++ b/test/describe-project/do-postGenerateCommands.sh @@ -0,0 +1 @@ +#!/bin/sh diff --git a/test/describe-project/do-preGenerateCommands.sh b/test/describe-project/do-preGenerateCommands.sh new file mode 100755 index 0000000..1a24852 --- /dev/null +++ b/test/describe-project/do-preGenerateCommands.sh @@ -0,0 +1 @@ +#!/bin/sh diff --git a/test/describe-project/dub.json b/test/describe-project/dub.json index 21b163f..71dddd0 100644 --- a/test/describe-project/dub.json +++ b/test/describe-project/dub.json @@ -1,11 +1,26 @@ { "name": "describe-project", - "targetType": "sourceLibrary", + "targetType": "executable", "description": "A test describe project", "authors": ["nobody"], "homepage": "fake.com", "license": "BSD 2-clause", "copyright": "Copyright © 2015, nobody", + "mainSourceFile": "src/dummy.d", + "sourceFiles-posix": ["./some.a"], + "sourceFiles-windows": ["./some.lib"], + "dflags": ["--some-dflag"], + "lflags": ["--some-lflag"], + "libs": ["crypto"], + "copyFiles": ["data/dummy.dat"], + "versions": ["someVerIdent"], + "debugVersions": ["someDebugVerIdent"], + "preGenerateCommands": ["./do-preGenerateCommands.sh"], + "postGenerateCommands": ["./do-postGenerateCommands.sh"], + "preBuildCommands": ["./do-preBuildCommands.sh"], + "postBuildCommands": ["./do-postBuildCommands.sh"], + "buildRequirements": ["allowWarnings", "disallowInlining"], + "buildOptions": ["releaseMode", "debugInfo"], "dependencies": { "describe-dependency-1": { "version": "1.0", @@ -14,14 +29,18 @@ "describe-dependency-2": { "version": "1.0", "path": "../describe-dependency-2" + }, + "describe-dependency-3": { + "version": "1.0", + "path": "../describe-dependency-3" } }, - "configurations": [ - { - "name": "my-project-config" - } - ], - "subConfigurations": { - "describe-dependency-1": "my-dependency-1-config" - }, + "configurations": [ + { + "name": "my-project-config" + } + ], + "subConfigurations": { + "describe-dependency-1": "my-dependency-1-config" + }, }