diff --git a/source/app.d b/source/app.d index 2cd7afe..77d8c7b 100644 --- a/source/app.d +++ b/source/app.d @@ -15,9 +15,9 @@ import dub.project; import dub.registry; -import vibe.core.file; -import vibe.core.log; -import vibe.inet.url; +import vibecompat.core.file; +import vibecompat.core.log; +import vibecompat.inet.url; import std.algorithm; import std.array; diff --git a/source/dub/compilers/compiler.d b/source/dub/compilers/compiler.d index fe3846f..81ed9fc 100644 --- a/source/dub/compilers/compiler.d +++ b/source/dub/compilers/compiler.d @@ -13,8 +13,8 @@ import std.algorithm; import std.array; -import vibe.data.json; -import vibe.inet.path; +import vibecompat.data.json; +import vibecompat.inet.path; static this() diff --git a/source/dub/compilers/dmd.d b/source/dub/compilers/dmd.d index a3c95c3..4d0f1e3 100644 --- a/source/dub/compilers/dmd.d +++ b/source/dub/compilers/dmd.d @@ -15,8 +15,8 @@ import std.conv; import std.exception; import stdx.process; -import vibe.core.log; -import vibe.inet.path; +import vibecompat.core.log; +import vibecompat.inet.path; class DmdCompiler : Compiler { diff --git a/source/dub/compilers/gdc.d b/source/dub/compilers/gdc.d index 76af13a..39a47de 100644 --- a/source/dub/compilers/gdc.d +++ b/source/dub/compilers/gdc.d @@ -15,8 +15,8 @@ import std.conv; import std.exception; import stdx.process; -import vibe.core.log; -import vibe.inet.path; +import vibecompat.core.log; +import vibecompat.inet.path; class GdcCompiler : Compiler { diff --git a/source/dub/compilers/ldc.d b/source/dub/compilers/ldc.d index 655ce11..d94a67c 100644 --- a/source/dub/compilers/ldc.d +++ b/source/dub/compilers/ldc.d @@ -15,8 +15,8 @@ import std.conv; import std.exception; import stdx.process; -import vibe.core.log; -import vibe.inet.path; +import vibecompat.core.log; +import vibecompat.inet.path; class LdcCompiler : Compiler { diff --git a/source/dub/dependency.d b/source/dub/dependency.d index 4eea5f8..b9948d7 100644 --- a/source/dub/dependency.d +++ b/source/dub/dependency.d @@ -10,10 +10,10 @@ import dub.utils; import dub.package_; -import vibe.core.log; -import vibe.core.file; -import vibe.data.json; -import vibe.inet.url; +import vibecompat.core.log; +import vibecompat.core.file; +import vibecompat.data.json; +import vibecompat.inet.url; // todo: cleanup imports import std.array; diff --git a/source/dub/dub.d b/source/dub/dub.d index 0763ad0..fb8c7be 100644 --- a/source/dub/dub.d +++ b/source/dub/dub.d @@ -18,10 +18,10 @@ import dub.project; import dub.generators.generator; -import vibe.core.file; -import vibe.core.log; -import vibe.data.json; -import vibe.inet.url; +import vibecompat.core.file; +import vibecompat.core.log; +import vibecompat.data.json; +import vibecompat.inet.url; // todo: cleanup imports. import std.algorithm; diff --git a/source/dub/generators/build.d b/source/dub/generators/build.d index cd1ffa2..7ed19b2 100644 --- a/source/dub/generators/build.d +++ b/source/dub/generators/build.d @@ -21,9 +21,9 @@ import std.string; import stdx.process; -import vibe.core.file; -import vibe.core.log; -import vibe.inet.path; +import vibecompat.core.file; +import vibecompat.core.log; +import vibecompat.inet.path; class BuildGenerator : ProjectGenerator { diff --git a/source/dub/generators/generator.d b/source/dub/generators/generator.d index 3a238a6..90e1468 100644 --- a/source/dub/generators/generator.d +++ b/source/dub/generators/generator.d @@ -17,7 +17,7 @@ import dub.project; import std.exception; -import vibe.core.log; +import vibecompat.core.log; /** diff --git a/source/dub/generators/monod.d b/source/dub/generators/monod.d index 2bf3450..4858c56 100644 --- a/source/dub/generators/monod.d +++ b/source/dub/generators/monod.d @@ -20,8 +20,8 @@ import std.uuid; import std.exception; -import vibe.core.file; -import vibe.core.log; +import vibecompat.core.file; +import vibecompat.core.log; class MonoDGenerator : ProjectGenerator { diff --git a/source/dub/generators/rdmd.d b/source/dub/generators/rdmd.d index 900b601..8602001 100644 --- a/source/dub/generators/rdmd.d +++ b/source/dub/generators/rdmd.d @@ -20,9 +20,9 @@ import std.string; import stdx.process; -import vibe.core.file; -import vibe.core.log; -import vibe.inet.path; +import vibecompat.core.file; +import vibecompat.core.log; +import vibecompat.inet.path; class RdmdGenerator : ProjectGenerator { diff --git a/source/dub/generators/visuald.d b/source/dub/generators/visuald.d index 03ccc10..7c3a21e 100644 --- a/source/dub/generators/visuald.d +++ b/source/dub/generators/visuald.d @@ -20,8 +20,8 @@ import std.uuid; import std.exception; -import vibe.core.file; -import vibe.core.log; +import vibecompat.core.file; +import vibecompat.core.log; version = VISUALD_SEPERATE_PROJECT_FILES; //version = VISUALD_SINGLE_PROJECT_FILE; diff --git a/source/dub/installation.d b/source/dub/installation.d index 74c8495..97b94ec 100644 --- a/source/dub/installation.d +++ b/source/dub/installation.d Binary files differ diff --git a/source/dub/package_.d b/source/dub/package_.d index 8fceb36..d09b7c8 100644 --- a/source/dub/package_.d +++ b/source/dub/package_.d @@ -15,10 +15,10 @@ import std.conv; import std.exception; import std.file; -import vibe.core.log; -import vibe.core.file; -import vibe.data.json; -import vibe.inet.url; +import vibecompat.core.log; +import vibecompat.core.file; +import vibecompat.data.json; +import vibecompat.inet.url; enum PackageJsonFilename = "package.json"; diff --git a/source/dub/packagemanager.d b/source/dub/packagemanager.d index f3c1dc8..b73a1eb 100644 --- a/source/dub/packagemanager.d +++ b/source/dub/packagemanager.d @@ -18,10 +18,10 @@ import std.file; import std.string; import std.zip; -import vibe.core.file; -import vibe.core.log; -import vibe.data.json; -import vibe.inet.path; +import vibecompat.core.file; +import vibecompat.core.log; +import vibecompat.data.json; +import vibecompat.inet.path; enum JournalJsonFilename = "journal.json"; diff --git a/source/dub/packagesupplier.d b/source/dub/packagesupplier.d index 6c2eb03..bf80316 100644 --- a/source/dub/packagesupplier.d +++ b/source/dub/packagesupplier.d @@ -15,11 +15,11 @@ import std.zip; import std.conv; -import vibe.core.log; -import vibe.core.file; -import vibe.data.json; -import vibe.inet.url; -import vibe.inet.urltransfer; +import vibecompat.core.log; +import vibecompat.core.file; +import vibecompat.data.json; +import vibecompat.inet.url; +import vibecompat.inet.urltransfer; /// Supplies packages, this is done by supplying the latest possible version /// which is available. diff --git a/source/dub/project.d b/source/dub/project.d index ce81b97..d980e21 100644 --- a/source/dub/project.d +++ b/source/dub/project.d @@ -17,10 +17,10 @@ import dub.packagesupplier; import dub.generators.generator; -import vibe.core.file; -import vibe.core.log; -import vibe.data.json; -import vibe.inet.url; +import vibecompat.core.file; +import vibecompat.core.log; +import vibecompat.data.json; +import vibecompat.inet.url; // todo: cleanup imports. import std.algorithm; diff --git a/source/dub/registry.d b/source/dub/registry.d index 10bb7e9..7a7acb8 100644 --- a/source/dub/registry.d +++ b/source/dub/registry.d Binary files differ diff --git a/source/dub/utils.d b/source/dub/utils.d index 78ac3b3..8ea6c54 100644 --- a/source/dub/utils.d +++ b/source/dub/utils.d @@ -7,10 +7,10 @@ */ module dub.utils; -import vibe.core.file; -import vibe.core.log; -import vibe.data.json; -import vibe.inet.url; +import vibecompat.core.file; +import vibecompat.core.log; +import vibecompat.data.json; +import vibecompat.inet.url; // todo: cleanup imports. import std.array; diff --git a/source/vibe/core/file.d b/source/vibe/core/file.d deleted file mode 100644 index d0f6464..0000000 --- a/source/vibe/core/file.d +++ /dev/null @@ -1,299 +0,0 @@ -/** - File handling. - - Copyright: © 2012 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 vibe.core.file; - -public import vibe.inet.url; -public import std.stdio; - -import vibe.core.log; - -import std.conv; -import std.c.stdio; -import std.datetime; -import std.exception; -import std.file; -import std.path; -import std.string; -import std.utf; - - -version(Posix){ - private extern(C) int mkstemps(char* templ, int suffixlen); -} - - -/* Add output range support to File -*/ -struct RangeFile { - File file; - alias file this; - - void put(in ubyte[] bytes) { file.rawWrite(bytes); } - void put(in char[] str) { put(cast(ubyte[])str); } - void put(char ch) { put((&ch)[0 .. 1]); } - void put(dchar ch) { char[4] chars; put(chars[0 .. encode(chars, ch)]); } - - ubyte[] readAll() - { - file.seek(0, SEEK_END); - auto sz = file.tell(); - enforce(sz <= size_t.max, "File is too big to read to memory."); - file.seek(0, SEEK_SET); - auto ret = new ubyte[cast(size_t)sz]; - return file.rawRead(ret); - } -} - - -/** - Opens a file stream with the specified mode. -*/ -RangeFile openFile(Path path, FileMode mode = FileMode.Read) -{ - string strmode; - final switch(mode){ - case FileMode.Read: strmode = "rb"; break; - case FileMode.ReadWrite: strmode = "rb+"; break; - case FileMode.CreateTrunc: strmode = "wb+"; break; - case FileMode.Append: strmode = "ab"; break; - } - auto ret = File(path.toNativeString(), strmode); - assert(ret.isOpen()); - return RangeFile(ret); -} -/// ditto -RangeFile openFile(string path, FileMode mode = FileMode.Read) -{ - return openFile(Path(path), mode); -} - -/** - Creates and opens a temporary file for writing. -*/ -RangeFile createTempFile(string suffix = null) -{ - version(Windows){ - char[L_tmpnam] tmp; - tmpnam(tmp.ptr); - auto tmpname = to!string(tmp.ptr); - if( tmpname.startsWith("\\") ) tmpname = tmpname[1 .. $]; - tmpname ~= suffix; - logDebug("tmp %s", tmpname); - return openFile(tmpname, FileMode.CreateTrunc); - } else { - import core.sys.posix.stdio; - enum pattern ="/tmp/vtmp.XXXXXX"; - scope templ = new char[pattern.length+suffix.length+1]; - templ[0 .. pattern.length] = pattern; - templ[pattern.length .. $-1] = suffix; - templ[$-1] = '\0'; - assert(suffix.length <= int.max); - auto fd = mkstemps(templ.ptr, cast(int)suffix.length); - enforce(fd >= 0, "Failed to create temporary file."); - auto ret = File.wrapFile(fdopen(fd, "wb+")); - return RangeFile(ret); - } -} - -/** - Moves or renames a file. -*/ -void moveFile(Path from, Path to) -{ - moveFile(from.toNativeString(), to.toNativeString()); -} -/// ditto -void moveFile(string from, string to) -{ - std.file.rename(from, to); -} - -/** - Copies a file. - - Note that attributes and time stamps are currently not retained. - - Params: - from = Path of the source file - to = Path for the destination file - overwrite = If true, any file existing at the destination path will be - overwritten. If this is false, an excpetion will be thrown should - a file already exist at the destination path. - - Throws: - An Exception if the copy operation fails for some reason. -*/ -void copyFile(Path from, Path to, bool overwrite = false) -{ - { - auto src = openFile(from, FileMode.Read); - scope(exit) src.close(); - enforce(overwrite || !existsFile(to), "Destination file already exists."); - auto dst = openFile(to, FileMode.CreateTrunc); - scope(exit) dst.close(); - dst.write(src); - } - - // TODO: retain attributes and time stamps -} -/// ditto -void copyFile(string from, string to) -{ - copyFile(Path(from), Path(to)); -} - -/** - Removes a file -*/ -void removeFile(Path path) -{ - removeFile(path.toNativeString()); -} -/// ditto -void removeFile(string path) { - std.file.remove(path); -} - -/** - Checks if a file exists -*/ -bool existsFile(Path path) { - return existsFile(path.toNativeString()); -} -/// ditto -bool existsFile(string path) -{ - return std.file.exists(path); -} - -/** Stores information about the specified file/directory into 'info' - - Returns false if the file does not exist. -*/ -FileInfo getFileInfo(Path path) -{ - auto ent = std.file.dirEntry(path.toNativeString()); - return makeFileInfo(ent); -} -/// ditto -FileInfo getFileInfo(string path) -{ - return getFileInfo(Path(path)); -} - -/** - Creates a new directory. -*/ -void createDirectory(Path path) -{ - mkdir(path.toNativeString()); -} -/// ditto -void createDirectory(string path) -{ - createDirectory(Path(path)); -} - -/** - Enumerates all files in the specified directory. -*/ -void listDirectory(Path path, scope bool delegate(FileInfo info) del) -{ - foreach( DirEntry ent; dirEntries(path.toNativeString(), SpanMode.shallow) ) - if( !del(makeFileInfo(ent)) ) - break; -} -/// ditto -void listDirectory(string path, scope bool delegate(FileInfo info) del) -{ - listDirectory(Path(path), del); -} -/// ditto -int delegate(scope int delegate(ref FileInfo)) iterateDirectory(Path path) -{ - int iterator(scope int delegate(ref FileInfo) del){ - int ret = 0; - listDirectory(path, (fi){ - ret = del(fi); - return ret == 0; - }); - return ret; - } - return &iterator; -} -/// ditto -int delegate(scope int delegate(ref FileInfo)) iterateDirectory(string path) -{ - return iterateDirectory(Path(path)); -} - - -/** - Returns the current working directory. -*/ -Path getWorkingDirectory() -{ - return Path(std.file.getcwd()); -} - - -/** Contains general information about a file. -*/ -struct FileInfo { - /// Name of the file (not including the path) - string name; - - /// Size of the file (zero for directories) - ulong size; - - /// Time of the last modification - SysTime timeModified; - - /// Time of creation (not available on all operating systems/file systems) - SysTime timeCreated; - - /// True if this is a symlink to an actual file - bool isSymlink; - - /// True if this is a directory or a symlink pointing to a directory - bool isDirectory; -} - -/** - Specifies how a file is manipulated on disk. -*/ -enum FileMode { - /// The file is opened read-only. - Read, - /// The file is opened for read-write random access. - ReadWrite, - /// The file is truncated if it exists and created otherwise and the opened for read-write access. - CreateTrunc, - /// The file is opened for appending data to it and created if it does not exist. - Append -} - -/** - Accesses the contents of a file as a stream. -*/ - -private FileInfo makeFileInfo(DirEntry ent) -{ - FileInfo ret; - ret.name = baseName(ent.name); - if( ret.name.length == 0 ) ret.name = ent.name; - assert(ret.name.length > 0); - ret.size = ent.size; - ret.timeModified = ent.timeLastModified; - version(Windows) ret.timeCreated = ent.timeCreated; - else ret.timeCreated = ent.timeLastModified; - ret.isSymlink = ent.isSymlink; - ret.isDirectory = ent.isDir; - return ret; -} - diff --git a/source/vibe/core/log.d b/source/vibe/core/log.d deleted file mode 100644 index 00579e8..0000000 --- a/source/vibe/core/log.d +++ /dev/null @@ -1,97 +0,0 @@ -/** - Central logging facility for vibe. - - Copyright: © 2012 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 vibe.core.log; - -import std.array; -import std.datetime; -import std.format; -import std.stdio; -import core.thread; - -private { - shared LogLevel s_minLevel = LogLevel.Info; - shared LogLevel s_logFileLevel; - shared bool s_plainLogging = false; -} - -/// Sets the minimum log level to be printed. -void setLogLevel(LogLevel level) nothrow -{ - s_minLevel = level; -} - -/// Disables output of thread/task ids with each log message -void setPlainLogging(bool enable) -{ - s_plainLogging = enable; -} - -/** - Logs a message. - - Params: - level = The log level for the logged message - fmt = See http://dlang.org/phobos/std_format.html#format-string -*/ -void logTrace(T...)(string fmt, auto ref T args) nothrow { log(LogLevel.Trace, fmt, args); } -/// ditto -void logDebug(T...)(string fmt, auto ref T args) nothrow { log(LogLevel.Debug, fmt, args); } -/// ditto -void logInfo(T...)(string fmt, auto ref T args) nothrow { log(LogLevel.Info, fmt, args); } -/// ditto -void logWarn(T...)(string fmt, auto ref T args) nothrow { log(LogLevel.Warn, fmt, args); } -/// ditto -void logError(T...)(string fmt, auto ref T args) nothrow { log(LogLevel.Error, fmt, args); } - -/// ditto -void log(T...)(LogLevel level, string fmt, auto ref T args) -nothrow { - if( level < s_minLevel ) return; - string pref; - final switch( level ){ - case LogLevel.Trace: pref = "trc"; break; - case LogLevel.Debug: pref = "dbg"; break; - case LogLevel.Info: pref = "INF"; break; - case LogLevel.Warn: pref = "WRN"; break; - case LogLevel.Error: pref = "ERR"; break; - case LogLevel.Fatal: pref = "FATAL"; break; - case LogLevel.None: assert(false); - } - - try { - auto txt = appender!string(); - txt.reserve(256); - formattedWrite(txt, fmt, args); - - auto threadid = cast(ulong)cast(void*)Thread.getThis(); - auto fiberid = cast(ulong)cast(void*)Fiber.getThis(); - threadid ^= threadid >> 32; - fiberid ^= fiberid >> 32; - - if( level >= s_minLevel ){ - if( s_plainLogging ) writeln(txt.data()); - else writefln("[%08X:%08X %s] %s", threadid, fiberid, pref, txt.data()); - stdout.flush(); - } - } catch( Exception e ){ - // this is bad but what can we do.. - debug assert(false, e.msg); - } -} - -/// Specifies the log level for a particular log message. -enum LogLevel { - Trace, - Debug, - Info, - Warn, - Error, - Fatal, - None -} - diff --git a/source/vibe/data/json.d b/source/vibe/data/json.d deleted file mode 100644 index 0d6169b..0000000 --- a/source/vibe/data/json.d +++ /dev/null @@ -1,1282 +0,0 @@ -/** - JSON serialization and value handling. - - This module provides the Json struct for reading, writing and manipulating JSON values in a seamless, - JavaScript like way. De(serialization) of arbitrary D types is also supported. - - Examples: - - --- - void manipulateJson(Json j) - { - // object members can be accessed using member syntax, just like in JavaScript - j = Json.EmptyObject; - j.name = "Example"; - j.id = 1; - - // retrieving the values is done using get() - assert(j["name"].get!string == "Example"); - assert(j["id"].get!int == 1); - - // semantic convertions can be done using to() - assert(j.id.to!string == "1"); - - // prints: - // name: "Example" - // id: 1 - foreach( string key, value; j ){ - writefln("%s: %s", key, value); - } - - // print out as JSON: {"name": "Example", "id": 1} - writefln("JSON: %s", j.toString()); - } - --- - - Copyright: © 2012 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 vibe.data.json; - -import vibe.data.utils; - -import std.array; -import std.conv; -import std.datetime; -import std.exception; -import std.format; -import std.string; -import std.range; -import std.traits; - - -/******************************************************************************/ -/* public types */ -/******************************************************************************/ - -/** - Represents a single JSON value. - - Json values can have one of the types defined in the Json.Type enum. They - 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 - 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; - }; - Type m_type = Type.Undefined; - } - - /** 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 - } - - /// New JSON value of Type.Undefined - static @property Json Undefined() { return Json(); } - - /// New JSON value of Type.Object - static @property Json EmptyObject() { return Json(cast(Json[string])null); } - - /// New JSON value of Type.Array - static @property Json EmptyArray() { return Json(cast(Json[])null); } - - version(JsonLineNumbers) int line; - - /** - Constructor for a JSON object. - */ - this(typeof(null)) { m_type = Type.Null; } - /// ditto - this(bool v) { m_type = Type.Bool; m_bool = v; } - /// ditto - this(int v) { m_type = Type.Int; m_int = v; } - /// ditto - this(long v) { m_type = Type.Int; m_int = v; } - /// ditto - this(double v) { m_type = Type.Float; m_float = v; } - /// ditto - this(string v) { m_type = Type.String; m_string = v; } - /// ditto - this(Json[] v) { m_type = Type.Array; m_array = v; } - /// ditto - this(Json[string] v) { m_type = Type.Object; m_object = v; } - - /** - Allows assignment of D values to a JSON value. - */ - ref Json opAssign(Json v){ - m_type = v.m_type; - final switch(m_type){ - case Type.Undefined: m_string = null; break; - case Type.Null: m_string = null; break; - case Type.Bool: m_bool = v.m_bool; break; - 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; break; - case Type.Object: m_object = v.m_object; break; - } - return this; - } - /// ditto - void opAssign(typeof(null)) { m_type = Type.Null; m_string = null; } - /// ditto - bool opAssign(bool v) { m_type = Type.Bool; m_bool = v; return v; } - /// ditto - int opAssign(int v) { m_type = Type.Int; m_int = v; return v; } - /// ditto - long opAssign(long v) { m_type = Type.Int; m_int = v; return v; } - /// ditto - double opAssign(double v) { m_type = Type.Float; m_float = v; return v; } - /// ditto - string opAssign(string v) { m_type = Type.String; m_string = v; return v; } - /// ditto - Json[] opAssign(Json[] v) { m_type = Type.Array; m_array = v; return v; } - /// ditto - Json[string] opAssign(Json[string] v) { m_type = Type.Object; m_object = v; return v; } - - /** - The current type id of this JSON object. - */ - @property Type type() const { return m_type; } - - /** - Allows direct indexing of array typed JSON values. - */ - ref inout(Json) opIndex(size_t idx) inout { checkType!(Json[])(); return m_array[idx]; } - - /** - Allows direct indexing of object typed JSON values using a string as - the key. - */ - 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; - return ret; - } - /// ditto - ref Json opIndex(string key){ - checkType!(Json[string])(); - if( auto pv = key in m_object ) - return *pv; - m_object[key] = Json(); - m_object[key].m_type = Type.Undefined; // DMDBUG: AAs are teh $H1T!!!11 - assert(m_object[key].type == Type.Undefined); - m_object[key].m_string = key; - return m_object[key]; - } - - /** - Returns a slice of a JSON array. - */ - inout(Json[]) opSlice() inout { checkType!(Json[])(); return m_array; } - /// - inout(Json[]) opSlice(size_t from, size_t to) inout { checkType!(Json[])(); return m_array[from .. to]; } - - /** - Returns the number of entries of string, array or object typed JSON values. - */ - @property size_t length() - const { - switch(m_type){ - case Type.String: return m_string.length; - case Type.Array: return m_array.length; - case Type.Object: return m_object.length; - default: - enforce(false, "Json.length() can only be called on strings, arrays and objects, not "~.to!string(m_type)~"."); - return 0; - } - } - - /** - Allows foreach iterating over JSON objects and arrays. - */ - int opApply(int delegate(ref Json obj) del) - { - enforce(m_type == Type.Array || m_type == Type.Object, "opApply may only be called on objects and arrays, not "~.to!string(m_type)~"."); - if( m_type == Type.Array ){ - foreach( ref v; m_array ) - if( auto ret = del(v) ) - return ret; - return 0; - } else { - foreach( ref v; m_object ) - if( v.type != Type.Undefined ) - if( auto ret = del(v) ) - return ret; - return 0; - } - } - /// ditto - int opApply(int delegate(ref const Json obj) del) - const { - enforce(m_type == Type.Array || m_type == Type.Object, "opApply may only be called on objects and arrays, not "~.to!string(m_type)~"."); - if( m_type == Type.Array ){ - foreach( ref v; m_array ) - if( auto ret = del(v) ) - return ret; - return 0; - } else { - foreach( ref v; m_object ) - if( v.type != Type.Undefined ) - if( auto ret = del(v) ) - return ret; - return 0; - } - } - /// ditto - int opApply(int delegate(ref size_t idx, ref Json obj) del) - { - enforce(m_type == Type.Array, "opApply may only be called on arrays, not "~.to!string(m_type)~""); - foreach( idx, ref v; m_array ) - if( auto ret = del(idx, v) ) - return ret; - return 0; - } - /// ditto - int opApply(int delegate(ref size_t idx, ref const Json obj) del) - const { - enforce(m_type == Type.Array, "opApply may only be called on arrays, not "~.to!string(m_type)~"."); - foreach( idx, ref v; m_array ) - if( auto ret = del(idx, v) ) - return ret; - return 0; - } - /// ditto - int opApply(int delegate(ref string idx, ref Json obj) del) - { - enforce(m_type == Type.Object, "opApply may only be called on objects, not "~.to!string(m_type)~"."); - foreach( idx, ref v; m_object ) - if( v.type != Type.Undefined ) - if( auto ret = del(idx, v) ) - return ret; - return 0; - } - /// ditto - int opApply(int delegate(ref string idx, ref const Json obj) del) - const { - enforce(m_type == Type.Object, "opApply may only be called on objects, not "~.to!string(m_type)~"."); - foreach( idx, ref v; m_object ) - if( v.type != Type.Undefined ) - if( auto ret = del(idx, v) ) - return ret; - return 0; - } - - /** - Converts the JSON value to the corresponding D type - types must match exactly. - */ - 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~"."); - } - /// ditto - @property const(T) opt(T)(const(T) def = T.init) - const { - if( typeId!T != m_type ) return def; - return get!T; - } - /// ditto - @property T opt(T)(T def = T.init) - { - if( typeId!T != m_type ) return def; - return get!T; - } - - /** - Converts the JSON value to the corresponding D type - types are converted as neccessary. - */ - @property inout(T) to(T)() - inout { - static if( is(T == bool) ){ - final switch( m_type ){ - case Type.Undefined: return false; - case Type.Null: return false; - case Type.Bool: return m_bool; - case Type.Int: return m_int != 0; - case Type.Float: return m_float != 0; - case Type.String: return m_string.length > 0; - case Type.Array: return m_array.length > 0; - case Type.Object: return m_object.length > 0; - } - } else static if( is(T == double) ){ - final switch( m_type ){ - case Type.Undefined: return T.init; - case Type.Null: return 0; - case Type.Bool: return m_bool ? 1 : 0; - case Type.Int: return m_int; - case Type.Float: return m_float; - case Type.String: return .to!double(cast(string)m_string); - case Type.Array: return double.init; - case Type.Object: return double.init; - } - } else static if( is(T == float) ){ - final switch( m_type ){ - case Type.Undefined: return T.init; - case Type.Null: return 0; - case Type.Bool: return m_bool ? 1 : 0; - case Type.Int: return m_int; - case Type.Float: return m_float; - case Type.String: return .to!float(cast(string)m_string); - case Type.Array: return float.init; - case Type.Object: return float.init; - } - } - else static if( is(T == long) ){ - final switch( m_type ){ - case Type.Undefined: return 0; - case Type.Null: return 0; - case Type.Bool: return m_bool ? 1 : 0; - case Type.Int: return m_int; - case Type.Float: return cast(long)m_float; - case Type.String: return .to!long(m_string); - case Type.Array: return 0; - case Type.Object: return 0; - } - } else static if( is(T : long) ){ - final switch( m_type ){ - case Type.Undefined: return 0; - case Type.Null: return 0; - case Type.Bool: return m_bool ? 1 : 0; - case Type.Int: return cast(T)m_int; - case Type.Float: return cast(T)m_float; - case Type.String: return cast(T).to!long(cast(string)m_string); - case Type.Array: return 0; - case Type.Object: return 0; - } - } else static if( is(T == string) ){ - switch( m_type ){ - default: return toString(); - case Type.String: return m_string; - } - } else static if( is(T == Json[]) ){ - switch( m_type ){ - default: return Json([this]); - case Type.Array: return m_array; - } - } else static if( is(T == Json[string]) ){ - switch( m_type ){ - 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~"."); - } - - /** - Performs unary operations on the JSON value. - - The following operations are supported for each type: - - $(DL - $(DT Null) $(DD none) - $(DT Bool) $(DD ~) - $(DT Int) $(DD +, -, ++, --) - $(DT Float) $(DD +, -, ++, --) - $(DT String) $(DD none) - $(DT Array) $(DD none) - $(DT Object) $(DD none) - ) - */ - Json opUnary(string op)() - const { - static if( op == "~" ){ - checkType!bool(); - return Json(~m_bool); - } else static if( op == "+" || op == "-" || op == "++" || 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 enforce(false, "'"~op~"' only allowed on scalar types, not on "~.to!string(m_type)~"."); - } else static assert("Unsupported operator '"~op~"' for type JSON."); - } - - /** - Performs binary operations between JSON values. - - The two JSON values must be of the same run time type or an exception - will be thrown. Only the operations listed are allowed for each of the - types. - - $(DL - $(DT Null) $(DD none) - $(DT Bool) $(DD &&, ||) - $(DT Int) $(DD +, -, *, /, %) - $(DT Float) $(DD +, -, *, /, %) - $(DT String) $(DD ~) - $(DT Array) $(DD ~) - $(DT Object) $(DD none) - ) - */ - 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."); - static if( op == "&&" ){ - enforce(m_type == Type.Bool, "'&&' only allowed for Type.Bool, not "~.to!string(m_type)~"."); - return Json(m_bool && other.m_bool); - } else static if( op == "||" ){ - enforce(m_type == Type.Bool, "'||' only allowed for Type.Bool, not "~.to!string(m_type)~"."); - return Json(m_bool || other.m_bool); - } else static if( 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 enforce(false, "'+' only allowed for scalar types, not "~.to!string(m_type)~"."); - } else static if( 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 enforce(false, "'-' only allowed for scalar types, not "~.to!string(m_type)~"."); - } else static if( 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 enforce(false, "'*' only allowed for scalar types, not "~.to!string(m_type)~"."); - } else static if( 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 enforce(false, "'/' only allowed for scalar types, not "~.to!string(m_type)~"."); - } else static if( 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 enforce(false, "'%' only allowed for scalar types, not "~.to!string(m_type)~"."); - } else static if( op == "~" ){ - if( m_type == Type.String ) return Json(m_string ~ other.m_string); - else enforce(false, "'~' only allowed for strings, not "~.to!string(m_type)~"."); - } else static assert("Unsupported operator '"~op~"' for type JSON."); - assert(false); - } - /// ditto - Json opBinary(string op)(Json other) - if( op == "~" ) - { - static if( 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 enforce(false, "'~' only allowed for strings and arrays, not "~.to!string(m_type)~"."); - } else static assert("Unsupported operator '"~op~"' for type JSON."); - assert(false); - } - /// ditto - void opOpAssign(string op)(Json other) - if( 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."); - 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.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.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.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.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); - } - /// ditto - Json opBinary(string op)(bool other) const { checkType!bool(); mixin("return Json(m_bool "~op~" other);"); } - /// ditto - Json opBinary(string op)(long other) const { checkType!long(); mixin("return Json(m_int "~op~" other);"); } - /// ditto - Json opBinary(string op)(double other) const { checkType!double(); mixin("return Json(m_float "~op~" other);"); } - /// ditto - Json opBinary(string op)(string other) const { checkType!string(); mixin("return Json(m_string "~op~" other);"); } - /// ditto - 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 - Json opBinaryRight(string op)(long other) const { checkType!long(); mixin("return Json(other "~op~" m_int);"); } - /// ditto - Json opBinaryRight(string op)(double other) const { checkType!double(); mixin("return Json(other "~op~" m_float);"); } - /// ditto - Json opBinaryRight(string op)(string other) const if(op == "~") { checkType!string(); return Json(other ~ m_string); } - /// ditto - inout(Json)* opBinaryRight(string op)(string other) inout if(op == "in") { - checkType!(Json[string])(); - auto pv = other in m_object; - if( !pv ) return null; - 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);"); } - - /** - Allows to access existing fields of a JSON object using dot syntax. - */ - @property const(Json) opDispatch(string prop)() const { return opIndex(prop); } - /// ditto - @property ref Json opDispatch(string prop)() { return opIndex(prop); } - - /** - Compares two JSON values for equality. - - If the two values have different types, they are considered unequal. - This differs with ECMA script, which performs a type conversion before - comparing the values. - */ - bool opEquals(ref const Json other) - const { - if( m_type != other.m_type ) return false; - final switch(m_type){ - case Type.Undefined: return false; - case Type.Null: return true; - case Type.Bool: return m_bool == other.m_bool; - case Type.Int: return m_int == other.m_int; - case Type.Float: return m_float == other.m_float; - case Type.String: return m_string == other.m_string; - case Type.Array: return m_array == other.m_array; - case Type.Object: return m_object == other.m_object; - } - } - /// ditto - bool opEquals(const Json other) const { return opEquals(other); } - /// ditto - bool opEquals(typeof(null)) const { return m_type == Type.Null; } - /// ditto - bool opEquals(bool v) const { return m_type == Type.Bool && m_bool == 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; } - /// ditto - bool opEquals(string v) const { return m_type == Type.String && m_string == v; } - - /** - Compares two JSON values. - - If the types of the two values differ, the value with the smaller type - id is considered the smaller value. This differs from ECMA script, which - performs a type conversion before comparing the values. - - JSON values of type Object cannot be compared and will throw an - exception. - */ - int opCmp(ref const Json other) - const { - if( m_type != other.m_type ) return m_type < other.m_type ? -1 : 1; - final switch(m_type){ - case Type.Undefined: return 0; - case Type.Null: return 0; - case Type.Bool: return m_bool < other.m_bool ? -1 : m_bool == other.m_bool ? 0 : 1; - case Type.Int: return m_int < other.m_int ? -1 : m_int == other.m_int ? 0 : 1; - case Type.Float: return m_float < other.m_float ? -1 : m_float == other.m_float ? 0 : 1; - 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."); - assert(false); - } - } - - - - /** - Returns the type id corresponding to the given D type. - */ - static @property Type typeId(T)() { - static if( is(T == typeof(null)) ) return Type.Null; - else static if( is(T == bool) ) return Type.Bool; - else static if( is(T == double) ) return Type.Float; - else static if( is(T == float) ) return Type.Float; - else static if( is(T : long) ) return Type.Int; - else static if( is(T == string) ) return Type.String; - else static if( is(T == Json[]) ) return Type.Array; - else static if( is(T == Json[string]) ) return Type.Object; - else static assert(false, "Unsupported JSON type '"~T.stringof~"'. Only bool, long, double, string, Json[] and Json[string] are allowed."); - } - - /** - Returns the JSON object as a string. - - For large JSON values use writeJsonString instead as this function will store the whole string - in memory, whereas writeJsonString writes it out bit for bit. - - See_Also: writeJsonString, toPrettyString - */ - string toString() - const { - auto ret = appender!string(); - writeJsonString(ret, this); - return ret.data; - } - - /** - Returns the JSON object as a "pretty" string. - - --- - auto json = Json(["foo": Json("bar")]); - writeln(json.toPrettyString()); - - // output: - // { - // "foo": "bar" - // } - --- - - Params: - level = Specifies the base amount of indentation for the output. Indentation is always - done using tab characters. - - See_Also: writePrettyJsonString, toString - */ - string toPrettyString(int level = 0) - const { - auto ret = appender!string(); - writePrettyJsonString(ret, this, level); - return ret.data; - } - - private void checkType(T)() - const { - string dbg; - if( m_type == Type.Undefined ) dbg = " field "~m_string; - enforce(typeId!T == m_type, "Trying to access JSON"~dbg~" of type "~.to!string(m_type)~" as "~T.stringof~"."); - } - - /*invariant() - { - assert(m_type >= Type.Undefined && m_type <= Type.Object); - }*/ -} - - -/******************************************************************************/ -/* public functions */ -/******************************************************************************/ - -/** - Parses the given range as a JSON string and returns the corresponding Json object. - - 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. -*/ -Json parseJson(R)(ref R range, int* line = null) - if( is(R == string) ) -{ - Json ret; - enforce(!range.empty, "JSON string is empty."); - - skipWhitespace(range, line); - - version(JsonLineNumbers){ - import vibe.core.log; - int curline = line ? *line : 0; - scope(failure) logError("Error in line: %d", curline); - } - - switch( range.front ){ - case 'f': - enforce(range[1 .. $].startsWith("alse"), "Expected 'false', got '"~range[0 .. 5]~"'."); - range.popFrontN(5); - ret = false; - break; - case 'n': - enforce(range[1 .. $].startsWith("ull"), "Expected 'null', got '"~range[0 .. 4]~"'."); - range.popFrontN(4); - ret = null; - break; - case 't': - enforce(range[1 .. $].startsWith("rue"), "Expected 'true', got '"~range[0 .. 4]~"'."); - range.popFrontN(4); - ret = true; - break; - case '0': .. case '9'+1: - case '-': - bool is_float; - auto num = skipNumber(range, is_float); - if( is_float ) ret = to!double(num); - else ret = to!long(num); - break; - case '\"': - ret = skipJsonString(range); - break; - case '[': - Json[] arr; - range.popFront(); - while(true) { - skipWhitespace(range, line); - enforce(!range.empty); - if(range.front == ']') break; - arr ~= parseJson(range, line); - skipWhitespace(range, line); - enforce(!range.empty && (range.front == ',' || range.front == ']'), "Expected ']' or ','."); - if( range.front == ']' ) break; - else range.popFront(); - } - range.popFront(); - ret = arr; - break; - case '{': - Json[string] obj; - range.popFront(); - while(true) { - skipWhitespace(range, line); - enforce(!range.empty); - if(range.front == '}') break; - string key = skipJsonString(range); - skipWhitespace(range, line); - enforce(range.startsWith(":"), "Expected ':' for key '" ~ key ~ "'"); - range.popFront(); - skipWhitespace(range, line); - Json itm = parseJson(range, line); - obj[key] = itm; - skipWhitespace(range, line); - enforce(!range.empty && (range.front == ',' || range.front == '}'), "Expected '}' or ',' - got '"~range[0]~"'."); - if( range.front == '}' ) break; - else range.popFront(); - } - range.popFront(); - ret = obj; - break; - default: - enforce(false, "Expected valid json token, got '"~to!string(range.length)~range[0 .. range.length>12?12:range.length]~"'."); - } - - assert(ret.type != Json.Type.Undefined); - version(JsonLineNumbers) ret.line = curline; - return ret; -} - -/** - Parses the given JSON string and returns the corresponding Json object. - - Throws an Exception if any parsing error occurs. -*/ -Json parseJsonString(string str) -{ - auto ret = parseJson(str); - enforce(str.strip().length == 0, "Expected end of string after JSON value."); - return ret; -} - -unittest { - assert(parseJsonString("null") == Json(null)); - assert(parseJsonString("true") == Json(true)); - assert(parseJsonString("false") == Json(false)); - assert(parseJsonString("1") == Json(1)); - assert(parseJsonString("2.0") == Json(2.0)); - assert(parseJsonString("\"test\"") == Json("test")); - 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"); -} - - -/** - Serializes the given value to JSON. - - 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) - ) - - All entries of an array or an associative array, as well as all R/W properties and - all public fields of a struct/class are recursively serialized using the same rules. - - Fields ending with an underscore will have the last underscore stripped in the - serialized output. This makes it possible to use fields with D keywords as their name - by simply appending an underscore. - - The following methods can be used to customize the serialization of structs/classes: - - --- - Json toJson() const; - static T fromJson(Json src); - - string toString() const; - static T fromString(string src); - --- - - 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). -*/ -Json serializeToJson(T)(T value) -{ - alias Unqual!T TU; - 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 ){ - auto ret = new Json[value.length]; - foreach( i; 0 .. value.length ) - ret[i] = serializeToJson(value[i]); - return Json(ret); - } else static if( isAssociativeArray!TU ){ - Json[string] ret; - foreach( string key, value; value ) - ret[key] = serializeToJson(value); - return Json(ret); - } else static if( __traits(compiles, value = T.fromJson(value.toJson())) ){ - return value.toJson(); - } else static if( __traits(compiles, value = T.fromString(value.toString())) ){ - return Json(value.toString()); - } else static if( is(TU == struct) ){ - Json[string] ret; - 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); - Json[string] ret; - 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); - return serializeToJson(*value); - } else { - static assert(false, "Unsupported type '"~T.stringof~"' for JSON serialization."); - } -} - - -/** - Deserializes a JSON value into the destination variable. - - The same types as for serializeToJson() are supported and handled inversely. -*/ -void deserializeJson(T)(ref T dst, Json src) -{ - dst = deserializeJson!T(src); -} -/// 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 ){ - alias typeof(T.init[0]) TV; - auto dst = new Unqual!TV[src.length]; - foreach( size_t i, v; src ) - dst[i] = deserializeJson!(Unqual!TV)(v); - return dst; - } else static if( isAssociativeArray!T ){ - alias typeof(T.init.values[0]) TV; - Unqual!TV[string] dst; - foreach( string key, value; src ) - dst[key] = deserializeJson!(Unqual!TV)(value); - return dst; - } else static if( __traits(compiles, { T dst; dst = T.fromJson(dst.toJson()); }()) ){ - return T.fromJson(src); - } else static if( __traits(compiles, { T dst; dst = T.fromString(dst.toString()); }()) ){ - return T.fromString(src.get!string); - } else static if( is(T == struct) ){ - T dst; - foreach( m; __traits(allMembers, T) ){ - static if( isRWPlainField!(T, m) || isRWField!(T, m) ){ - alias typeof(__traits(getMember, dst, m)) TM; - __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; - auto dst = new T; - foreach( m; __traits(allMembers, T) ){ - static if( isRWPlainField!(T, m) || isRWField!(T, m) ){ - alias typeof(__traits(getMember, dst, m)) TM; - __traits(getMember, dst, m) = deserializeJson!TM(src[underscoreStrip(m)]); - } - } - return dst; - } else static if( isPointer!T ){ - if( src.type == Json.Type.Null ) return null; - alias typeof(*T.init) TD; - dst = new TD; - *dst = deserializeJson!TD(src); - return dst; - } else { - static assert(false, "Unsupported type '"~T.stringof~"' for JSON serialization."); - } -} - -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]}; - S u; - deserializeJson(u, serializeToJson(t)); - assert(t.a == u.a); - assert(t.b == u.b); - assert(t.c == u.c); - assert(t.d == u.d); - assert(t.e == u.e); - assert(t.f == u.f); - assert(t.g == u.g); - assert(t.h == u.h); - assert(t.i == u.i); - assert(t.j == u.j); -} - -unittest { - static class C { - int a; - private int _b; - @property int b() const { return _b; } - @property void b(int v) { _b = v; } - - @property int test() const { return 10; } - - void test2() {} - } - C c = new C; - c.a = 1; - c.b = 2; - - C d; - deserializeJson(d, serializeToJson(c)); - assert(c.a == d.a); - assert(c.b == d.b); -} - - -/** - 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. - - Params: - dst = References the string output range to which the result is written. - json = Specifies the JSON value that is to be stringified. - - See_Also: Json.toString, writePrettyJsonString -*/ -void writeJsonString(R)(ref R dst, in Json json) -// 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( ref const Json e; json ){ - if( e.type == Json.Type.Undefined ) continue; - if( !first ) dst.put(","); - first = false; - writeJsonString(dst, e); - } - dst.put("]"); - break; - case Json.Type.Object: - dst.put("{"); - bool first = true; - foreach( string k, ref const Json e; json ){ - if( e.type == Json.Type.Undefined ) continue; - if( !first ) dst.put(","); - first = false; - dst.put("\""); - jsonEscape(dst, k); - dst.put("\":"); - writeJsonString(dst, e); - } - dst.put("}"); - break; - } -} - -/** - Writes the given JSON object as a prettified JSON string into the destination range. - - The output will contain newlines and indents to make the output human readable. - - 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. - - 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 ) dst.put('\t'); - writePrettyJsonString(dst, e, level+1); - } - if( json.length > 0 ) { - dst.put('\n'); - foreach( tab; 0 .. (level-1) ) 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 ) 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-1) ) dst.put('\t'); - } - dst.put("}"); - break; - } -} - - -/** Deprecated aliases for backwards compatibility. - - Use writeJsonString and writePrettyJsonString instead. -*/ -deprecated("Please use writeJsonString instead.") alias writeJsonString toJson; -/// -deprecated("Please use writePrettyJsonString instead.") alias writePrettyJsonString toPrettyJson; - - -/// private -private void jsonEscape(R)(ref R dst, string s) -{ - foreach( ch; s ){ - switch(ch){ - default: dst.put(ch); break; - case '\\': dst.put("\\\\"); break; - case '\r': dst.put("\\r"); break; - case '\n': dst.put("\\n"); break; - case '\t': dst.put("\\t"); break; - case '\"': dst.put("\\\""); break; - } - } -} - -/// private -private string jsonUnescape(R)(ref R range) -{ - auto ret = appender!string(); - while(!range.empty){ - auto ch = range.front; - switch( ch ){ - case '"': return ret.data; - case '\\': - range.popFront(); - enforce(!range.empty, "Unterminated string escape sequence."); - switch(range.front){ - default: enforce("Invalid string escape sequence."); break; - case '"': ret.put('\"'); range.popFront(); break; - case '\\': ret.put('\\'); range.popFront(); break; - case '/': ret.put('/'); range.popFront(); break; - case 'b': ret.put('\b'); range.popFront(); break; - case 'f': ret.put('\f'); range.popFront(); break; - case 'n': ret.put('\n'); range.popFront(); break; - 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; - 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'."); - } - ret.put(uch); - break; - } - break; - default: - ret.put(ch); - range.popFront(); - break; - } - } - return ret.data; -} - -private string skipNumber(ref string s, out bool is_float) -{ - size_t idx = 0; - is_float = false; - if( s[idx] == '-' ) idx++; - if( s[idx] == '0' ) idx++; - else { - enforce(isDigit(s[idx++]), "Digit expected at beginning of number."); - while( idx < s.length && isDigit(s[idx]) ) idx++; - } - - if( idx < s.length && s[idx] == '.' ){ - idx++; - is_float = true; - while( idx < s.length && isDigit(s[idx]) ) idx++; - } - - if( idx < s.length && (s[idx] == 'e' || s[idx] == 'E') ){ - idx++; - is_float = true; - if( idx < s.length && (s[idx] == '+' || s[idx] == '-') ) idx++; - enforce( idx < s.length && isDigit(s[idx]), "Expected exponent." ~ s[0 .. idx]); - idx++; - while( idx < s.length && isDigit(s[idx]) ) idx++; - } - - string ret = s[0 .. idx]; - s = s[idx .. $]; - return ret; -} - -private string skipJsonString(ref string s, int* line = null) -{ - enforce(s.length >= 2 && s[0] == '\"', "too small: '" ~ s ~ "'"); - s = s[1 .. $]; - string ret = jsonUnescape(s); - enforce(s.length > 0 && s[0] == '\"', "Unterminated string literal."); - s = s[1 .. $]; - return ret; -} - -private void skipWhitespace(ref string s, int* line = null) -{ - while( s.length > 0 ){ - switch( s[0] ){ - default: return; - case ' ', '\t': s = s[1 .. $]; break; - case '\n': - s = s[1 .. $]; - if( s.length > 0 && s[0] == '\r' ) s = s[1 .. $]; - if( line ) (*line)++; - break; - case '\r': - s = s[1 .. $]; - if( s.length > 0 && s[0] == '\n' ) s = s[1 .. $]; - if( line ) (*line)++; - break; - } - } -} - -/// private -private bool isDigit(T)(T ch){ return ch >= '0' && ch <= '9'; } - -private string underscoreStrip(string field_name) -{ - if( field_name.length < 1 || field_name[$-1] != '_' ) return field_name; - else return field_name[0 .. $-1]; -} diff --git a/source/vibe/data/utils.d b/source/vibe/data/utils.d deleted file mode 100644 index 0f4cc99..0000000 --- a/source/vibe/data/utils.d +++ /dev/null @@ -1,30 +0,0 @@ -/** - Utility functions for data serialization - - Copyright: © 2012 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 vibe.data.utils; - -public import std.traits; - - -template isRWPlainField(T, string M) -{ - 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))); - } -} - -template isRWField(T, string M) -{ - enum isRWField = __traits(compiles, __traits(getMember, Tgen!T(), M) = __traits(getMember, Tgen!T(), M)); - //pragma(msg, T.stringof~"."~M~": "~(isRWField?"1":"0")); -} - -/// private -private T Tgen(T)(){ return T.init; } diff --git a/source/vibe/inet/path.d b/source/vibe/inet/path.d deleted file mode 100644 index b02b899..0000000 --- a/source/vibe/inet/path.d +++ /dev/null @@ -1,295 +0,0 @@ -/** - Contains routines for high level path handling. - - Copyright: © 2012 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 vibe.inet.path; - -import std.algorithm; -import std.array; -import std.conv; -import std.exception; -import std.string; - - -/** - Represents an absolute or relative file system path. - - This struct allows to do safe operations on paths, such as concatenation and sub paths. Checks - are done to disallow invalid operations such as concatenating two absolute paths. It also - validates path strings and allows for easy checking of malicious relative paths. -*/ -struct Path { - private { - immutable(PathEntry)[] m_nodes; - bool m_absolute = false; - bool m_endsWithSlash = false; - } - - /// Constructs a Path object by parsing a path string. - this(string pathstr) - { - m_nodes = cast(immutable)splitPath(pathstr); - m_absolute = (pathstr.startsWith("/") || m_nodes.length > 0 && m_nodes[0].toString().countUntil(':')>0); - m_endsWithSlash = pathstr.endsWith("/"); - foreach( e; m_nodes ) assert(e.toString().length > 0); - } - - /// Constructs a path object from a list of PathEntry objects. - this(immutable(PathEntry)[] nodes, bool absolute) - { - m_nodes = nodes; - m_absolute = absolute; - } - - /// Constructs a relative path with one path entry. - this(PathEntry entry){ - m_nodes = [entry]; - m_absolute = false; - } - - /// Determines if the path is absolute. - @property bool absolute() const { return m_absolute; } - - /// Resolves all '.' and '..' path entries as far as possible. - void normalize() - { - immutable(PathEntry)[] newnodes; - foreach( n; m_nodes ){ - switch(n.toString()){ - default: - newnodes ~= n; - break; - case ".": break; - case "..": - enforce(!m_absolute || newnodes.length > 0, "Path goes below root node."); - if( newnodes.length > 0 && newnodes[$-1] != ".." ) newnodes = newnodes[0 .. $-1]; - else newnodes ~= n; - break; - } - } - m_nodes = newnodes; - } - - /// Converts the Path back to a string representation using slashes. - string toString() - const { - if( m_nodes.empty ) return absolute ? "/" : ""; - - Appender!string ret; - - // for absolute paths start with / - if( absolute ) ret.put('/'); - - foreach( i, f; m_nodes ){ - if( i > 0 ) ret.put('/'); - ret.put(f.toString()); - } - - if( m_nodes.length > 0 && m_endsWithSlash ) - ret.put('/'); - - return ret.data; - } - - /// Converts the Path object to a native path string (backslash as path separator on Windows). - string toNativeString() - const { - Appender!string ret; - - // for absolute unix paths start with / - version(Posix) { if(absolute) ret.put('/'); } - - foreach( i, f; m_nodes ){ - version(Windows) { if( i > 0 ) ret.put('\\'); } - version(Posix) { if( i > 0 ) ret.put('/'); } - else { enforce("Unsupported OS"); } - ret.put(f.toString()); - } - - if( m_nodes.length > 0 && m_endsWithSlash ){ - version(Windows) { ret.put('\\'); } - version(Posix) { ret.put('/'); } - } - - return ret.data; - } - - /// Tests if `rhs` is an anchestor or the same as this path. - bool startsWith(const Path rhs) const { - if( rhs.m_nodes.length > m_nodes.length ) return false; - foreach( i; 0 .. rhs.m_nodes.length ) - if( m_nodes[i] != rhs.m_nodes[i] ) - return false; - return true; - } - - /// Computes the relative path from `parentPath` to this path. - Path relativeTo(const Path parentPath) const { - int nup = 0; - while( parentPath.length > nup && !startsWith(parentPath[0 .. parentPath.length-nup]) ){ - nup++; - } - Path ret = Path(null, false); - ret.m_endsWithSlash = true; - foreach( i; 0 .. nup ) ret ~= ".."; - ret ~= Path(m_nodes[parentPath.length-nup .. $], false); - return ret; - } - - /// The last entry of the path - @property ref immutable(PathEntry) head() const { enforce(m_nodes.length > 0); return m_nodes[$-1]; } - - /// The parent path - @property Path parentPath() const { return this[0 .. length-1]; } - - /// The ist of path entries of which this path is composed - @property immutable(PathEntry)[] nodes() const { return m_nodes; } - - /// The number of path entries of which this path is composed - @property size_t length() const { return m_nodes.length; } - - /// True if the path contains no entries - @property bool empty() const { return m_nodes.length == 0; } - - /// Determines if the path ends with a slash (i.e. is a directory) - @property bool endsWithSlash() const { return m_endsWithSlash; } - /// ditto - @property void endsWithSlash(bool v) { m_endsWithSlash = v; } - - /// Determines if this path goes outside of its base path (i.e. begins with '..'). - @property bool external() const { return !m_absolute && m_nodes.length > 0 && m_nodes[0].m_name == ".."; } - - ref immutable(PathEntry) opIndex(size_t idx) const { return m_nodes[idx]; } - Path opSlice(size_t start, size_t end) const { - auto ret = Path(m_nodes[start .. end], start == 0 ? absolute : false); - if( end == m_nodes.length ) ret.m_endsWithSlash = m_endsWithSlash; - return ret; - } - size_t opDollar(int dim)() const if(dim == 0) { return m_nodes.length; } - - - Path opBinary(string OP)(const Path rhs) const if( OP == "~" ) { - Path ret; - ret.m_nodes = m_nodes; - ret.m_absolute = m_absolute; - ret.m_endsWithSlash = rhs.m_endsWithSlash; - ret.normalize(); // needed to avoid "."~".." become "" instead of ".." - - assert(!rhs.absolute, "Trying to append absolute path."); - size_t idx = m_nodes.length; - foreach(folder; rhs.m_nodes){ - switch(folder.toString()){ - default: ret.m_nodes = ret.m_nodes ~ folder; break; - case ".": break; - case "..": - enforce(!ret.absolute || ret.m_nodes.length > 0, "Relative path goes below root node!"); - if( ret.m_nodes.length > 0 && ret.m_nodes[$-1].toString() != ".." ) - ret.m_nodes = ret.m_nodes[0 .. $-1]; - else ret.m_nodes = ret.m_nodes ~ folder; - break; - } - } - return ret; - } - - Path opBinary(string OP)(string rhs) const if( OP == "~" ) { assert(rhs.length > 0); return opBinary!"~"(Path(rhs)); } - Path opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { assert(rhs.toString().length > 0); return opBinary!"~"(Path(rhs)); } - void opOpAssign(string OP)(string rhs) if( OP == "~" ) { assert(rhs.length > 0); opOpAssign!"~"(Path(rhs)); } - void opOpAssign(string OP)(PathEntry rhs) if( OP == "~" ) { assert(rhs.toString().length > 0); opOpAssign!"~"(Path(rhs)); } - void opOpAssign(string OP)(Path rhs) if( OP == "~" ) { auto p = this ~ rhs; m_nodes = p.m_nodes; m_endsWithSlash = rhs.m_endsWithSlash; } - - /// Tests two paths for equality using '=='. - bool opEquals(ref const Path rhs) const { - if( m_absolute != rhs.m_absolute ) return false; - if( m_endsWithSlash != rhs.m_endsWithSlash ) return false; - if( m_nodes.length != rhs.length ) return false; - foreach( i; 0 .. m_nodes.length ) - if( m_nodes[i] != rhs.m_nodes[i] ) - return false; - return true; - } - /// ditto - bool opEquals(const Path other) const { return opEquals(other); } - - int opCmp(ref const Path rhs) const { - if( m_absolute != rhs.m_absolute ) return cast(int)m_absolute - cast(int)rhs.m_absolute; - foreach( i; 0 .. min(m_nodes.length, rhs.m_nodes.length) ) - if( m_nodes[i] != rhs.m_nodes[i] ) - return m_nodes[i].opCmp(rhs.m_nodes[i]); - if( m_nodes.length > rhs.m_nodes.length ) return 1; - if( m_nodes.length < rhs.m_nodes.length ) return -1; - return 0; - } -} - -struct PathEntry { - private { - string m_name; - } - - this(string str) - { - assert(str.countUntil('/') < 0 && str.countUntil('\\') < 0); - m_name = str; - } - - string toString() const { return m_name; } - - Path opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return Path(cast(immutable)[this, rhs], false); } - - bool opEquals(ref const PathEntry rhs) const { return m_name == rhs.m_name; } - bool opEquals(PathEntry rhs) const { return m_name == rhs.m_name; } - bool opEquals(string rhs) const { return m_name == rhs; } - int opCmp(ref const PathEntry rhs) const { return m_name.cmp(rhs.m_name); } - int opCmp(string rhs) const { return m_name.cmp(rhs); } -} - -private bool isValidFilename(string str) -{ - foreach( ch; str ) - if( ch == '/' || /*ch == ':' ||*/ ch == '\\' ) return false; - return true; -} - -/// Joins two path strings. subpath must be relative. -string joinPath(string basepath, string subpath) -{ - Path p1 = Path(basepath); - Path p2 = Path(subpath); - return (p1 ~ p2).toString(); -} - -/// Splits up a path string into its elements/folders -PathEntry[] splitPath(string path) -{ - if( path.startsWith("/") || path.startsWith("\\") ) path = path[1 .. $]; - if( path.empty ) return null; - if( path.endsWith("/") || path.endsWith("\\") ) path = path[0 .. $-1]; - - // count the number of path nodes - size_t nelements = 0; - foreach( i, char ch; path ) - if( ch == '\\' || ch == '/' ) - nelements++; - nelements++; - - // reserve space for the elements - auto elements = new PathEntry[nelements]; - - // read and return the elements - size_t startidx = 0; - size_t eidx = 0; - foreach( i, char ch; path ) - if( ch == '\\' || ch == '/' ){ - enforce(i - startidx > 0, "Empty path entries not allowed."); - elements[eidx++] = PathEntry(path[startidx .. i]); - startidx = i+1; - } - elements[eidx++] = PathEntry(path[startidx .. $]); - enforce(path.length - startidx > 0, "Empty path entries not allowed."); - assert(eidx == nelements); - return elements; -} diff --git a/source/vibe/inet/url.d b/source/vibe/inet/url.d deleted file mode 100644 index b0736ee..0000000 --- a/source/vibe/inet/url.d +++ /dev/null @@ -1,277 +0,0 @@ -/** - URL parsing routines. - - Copyright: © 2012 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 vibe.inet.url; - -public import vibe.inet.path; - -import std.algorithm; -import std.array; -import std.conv; -import std.exception; -import std.string; -import std.uri; - - -/** - Represents a URL decomposed into its components. -*/ -struct Url { - private { - string m_schema; - string m_pathString; - Path m_path; - string m_host; - ushort m_port; - string m_username; - string m_password; - string m_queryString; - string m_anchor; - } - - /// Constructs a new URL object from its components. - this(string schema, string host, ushort port, Path path) - { - m_schema = schema; - m_host = host; - m_port = port; - m_path = path; - m_pathString = path.toString(); - } - /// ditto - this(string schema, Path path) - { - this(schema, null, 0, path); - } - - /** Constructs a URL from its string representation. - - TODO: additional validation required (e.g. valid host and user names and port) - */ - this(string url_string) - { - auto str = url_string; - enforce(str.length > 0, "Empty URL."); - if( str[0] != '/' ){ - auto idx = str.countUntil(':'); - enforce(idx > 0, "No schema in URL:"~str); - m_schema = str[0 .. idx]; - str = str[idx+1 .. $]; - bool requires_host = false; - - switch(m_schema){ - case "http": - case "https": - case "ftp": - case "spdy": - case "sftp": - case "file": - // proto://server/path style - enforce(str.startsWith("//"), "URL must start with proto://..."); - requires_host = true; - str = str[2 .. $]; - goto default; - default: - auto si = str.countUntil('/'); - if( si < 0 ) si = str.length; - auto ai = str[0 .. si].countUntil('@'); - sizediff_t hs = 0; - if( ai >= 0 ){ - hs = ai+1; - auto ci = str[0 .. ai].countUntil(':'); - if( ci >= 0 ){ - m_username = str[0 .. ci]; - m_password = str[ci+1 .. ai]; - } else m_username = str[0 .. ai]; - enforce(m_username.length > 0, "Empty user name in URL."); - } - - m_host = str[hs .. si]; - auto pi = m_host.countUntil(':'); - if(pi > 0) { - enforce(pi < m_host.length-1, "Empty port in URL."); - m_port = to!ushort(m_host[pi+1..$]); - m_host = m_host[0 .. pi]; - } - - enforce(!requires_host || m_schema == "file" || m_host.length > 0, - "Empty server name in URL."); - str = str[si .. $]; - } - } - - this.localURI = str; - } - /// ditto - static Url parse(string url_string) - { - return Url(url_string); - } - - /// The schema/protocol part of the URL - @property string schema() const { return m_schema; } - /// ditto - @property void schema(string v) { m_schema = v; } - - /// The path part of the URL in the original string form - @property string pathString() const { return m_pathString; } - - /// The path part of the URL - @property Path path() const { return m_path; } - /// ditto - @property void path(Path p) - { - m_path = p; - auto pstr = p.toString(); - m_pathString = pstr; - } - - /// The host part of the URL (depends on the schema) - @property string host() const { return m_host; } - /// ditto - @property void host(string v) { m_host = v; } - - /// The port part of the URL (optional) - @property ushort port() const { return m_port; } - /// ditto - @property port(ushort v) { m_port = v; } - - /// The user name part of the URL (optional) - @property string username() const { return m_username; } - /// ditto - @property void username(string v) { m_username = v; } - - /// The password part of the URL (optional) - @property string password() const { return m_password; } - /// ditto - @property void password(string v) { m_password = v; } - - /// The query string part of the URL (optional) - @property string queryString() const { return m_queryString; } - /// ditto - @property void queryString(string v) { m_queryString = v; } - - /// The anchor part of the URL (optional) - @property string anchor() const { return m_anchor; } - - /// The path part plus query string and anchor - @property string localURI() - const { - auto str = appender!string(); - str.reserve(m_pathString.length + 2 + queryString.length + anchor.length); - str.put(encode(path.toString())); - if( queryString.length ) { - str.put("?"); - str.put(queryString); - } - if( anchor.length ) { - str.put("#"); - str.put(anchor); - } - return str.data; - } - /// ditto - @property void localURI(string str) - { - auto ai = str.countUntil('#'); - if( ai >= 0 ){ - m_anchor = str[ai+1 .. $]; - str = str[0 .. ai]; - } - - auto qi = str.countUntil('?'); - if( qi >= 0 ){ - m_queryString = str[qi+1 .. $]; - str = str[0 .. qi]; - } - - m_pathString = str; - m_path = Path(decode(str)); - } - - /// The URL to the parent path with query string and anchor stripped. - @property Url parentUrl() const { - Url ret; - ret.schema = schema; - ret.host = host; - ret.port = port; - ret.username = username; - ret.password = password; - ret.path = path.parentPath; - return ret; - } - - /// Converts this URL object to its string representation. - string toString() - const { - import std.format; - auto dst = appender!string(); - dst.put(schema); - dst.put(":"); - switch(schema){ - default: break; - case "file": - case "http": - case "https": - case "ftp": - case "spdy": - case "sftp": - dst.put("//"); - break; - } - dst.put(host); - if( m_port > 0 ) formattedWrite(dst, ":%d", m_port); - dst.put(localURI); - return dst.data; - } - - bool startsWith(const Url rhs) const { - if( m_schema != rhs.m_schema ) return false; - if( m_host != rhs.m_host ) return false; - // FIXME: also consider user, port, querystring, anchor etc - return path.startsWith(rhs.m_path); - } - - Url opBinary(string OP)(Path rhs) const if( OP == "~" ) { return Url(m_schema, m_host, m_port, m_path ~ rhs); } - Url opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return Url(m_schema, m_host, m_port, m_path ~ rhs); } - void opOpAssign(string OP)(Path rhs) if( OP == "~" ) { m_path ~= rhs; } - void opOpAssign(string OP)(PathEntry rhs) if( OP == "~" ) { m_path ~= rhs; } - - /// Tests two URLs for equality using '=='. - bool opEquals(ref const Url rhs) const { - if( m_schema != rhs.m_schema ) return false; - if( m_host != rhs.m_host ) return false; - if( m_path != rhs.m_path ) return false; - return true; - } - /// ditto - bool opEquals(const Url other) const { return opEquals(other); } - - int opCmp(ref const Url rhs) const { - if( m_schema != rhs.m_schema ) return m_schema.cmp(rhs.m_schema); - if( m_host != rhs.m_host ) return m_host.cmp(rhs.m_host); - if( m_path != rhs.m_path ) return m_path.opCmp(rhs.m_path); - return true; - } -} - -unittest { - auto url = Url.parse("https://www.example.net/index.html"); - assert(url.schema == "https", url.schema); - assert(url.host == "www.example.net", url.host); - assert(url.path == Path("/index.html"), url.path.toString()); - - url = Url.parse("http://jo.doe:password@sub.www.example.net:4711/sub2/index.html?query#anchor"); - assert(url.schema == "http", url.schema); - assert(url.username == "jo.doe", url.username); - assert(url.password == "password", url.password); - assert(url.port == 4711, to!string(url.port)); - assert(url.host == "sub.www.example.net", url.host); - assert(url.path.toString() == "/sub2/index.html", url.path.toString()); - assert(url.queryString == "query", url.queryString); - assert(url.anchor == "anchor", url.anchor); -} diff --git a/source/vibe/inet/urltransfer.d b/source/vibe/inet/urltransfer.d deleted file mode 100644 index 1e62f0e..0000000 --- a/source/vibe/inet/urltransfer.d +++ /dev/null @@ -1,37 +0,0 @@ -/** - Downloading and uploading of data from/to URLs. - - Copyright: © 2012 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 vibe.inet.urltransfer; - -import vibe.core.log; -import vibe.core.file; -import vibe.inet.url; - -import std.exception; -import std.net.curl; -import std.string; - - -/** - Downloads a file from the specified URL. - - Any redirects will be followed until the actual file resource is reached or if the redirection - limit of 10 is reached. Note that only HTTP(S) is currently supported. -*/ -void download(string url, string filename) -{ - auto conn = HTTP(); - static if( is(typeof(&conn.verifyPeer)) ) - conn.verifyPeer = false; - std.net.curl.download(url, filename, conn); -} - -/// ditto -void download(Url url, Path filename) -{ - download(url.toString(), filename.toNativeString()); -} diff --git a/source/vibecompat/core/file.d b/source/vibecompat/core/file.d new file mode 100644 index 0000000..de8e49b --- /dev/null +++ b/source/vibecompat/core/file.d @@ -0,0 +1,299 @@ +/** + File handling. + + Copyright: © 2012 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 vibecompat.core.file; + +public import vibecompat.inet.url; +public import std.stdio; + +import vibecompat.core.log; + +import std.conv; +import std.c.stdio; +import std.datetime; +import std.exception; +import std.file; +import std.path; +import std.string; +import std.utf; + + +version(Posix){ + private extern(C) int mkstemps(char* templ, int suffixlen); +} + + +/* Add output range support to File +*/ +struct RangeFile { + File file; + alias file this; + + void put(in ubyte[] bytes) { file.rawWrite(bytes); } + void put(in char[] str) { put(cast(ubyte[])str); } + void put(char ch) { put((&ch)[0 .. 1]); } + void put(dchar ch) { char[4] chars; put(chars[0 .. encode(chars, ch)]); } + + ubyte[] readAll() + { + file.seek(0, SEEK_END); + auto sz = file.tell(); + enforce(sz <= size_t.max, "File is too big to read to memory."); + file.seek(0, SEEK_SET); + auto ret = new ubyte[cast(size_t)sz]; + return file.rawRead(ret); + } +} + + +/** + Opens a file stream with the specified mode. +*/ +RangeFile openFile(Path path, FileMode mode = FileMode.Read) +{ + string strmode; + final switch(mode){ + case FileMode.Read: strmode = "rb"; break; + case FileMode.ReadWrite: strmode = "rb+"; break; + case FileMode.CreateTrunc: strmode = "wb+"; break; + case FileMode.Append: strmode = "ab"; break; + } + auto ret = File(path.toNativeString(), strmode); + assert(ret.isOpen()); + return RangeFile(ret); +} +/// ditto +RangeFile openFile(string path, FileMode mode = FileMode.Read) +{ + return openFile(Path(path), mode); +} + +/** + Creates and opens a temporary file for writing. +*/ +RangeFile createTempFile(string suffix = null) +{ + version(Windows){ + char[L_tmpnam] tmp; + tmpnam(tmp.ptr); + auto tmpname = to!string(tmp.ptr); + if( tmpname.startsWith("\\") ) tmpname = tmpname[1 .. $]; + tmpname ~= suffix; + logDebug("tmp %s", tmpname); + return openFile(tmpname, FileMode.CreateTrunc); + } else { + import core.sys.posix.stdio; + enum pattern ="/tmp/vtmp.XXXXXX"; + scope templ = new char[pattern.length+suffix.length+1]; + templ[0 .. pattern.length] = pattern; + templ[pattern.length .. $-1] = suffix; + templ[$-1] = '\0'; + assert(suffix.length <= int.max); + auto fd = mkstemps(templ.ptr, cast(int)suffix.length); + enforce(fd >= 0, "Failed to create temporary file."); + auto ret = File.wrapFile(fdopen(fd, "wb+")); + return RangeFile(ret); + } +} + +/** + Moves or renames a file. +*/ +void moveFile(Path from, Path to) +{ + moveFile(from.toNativeString(), to.toNativeString()); +} +/// ditto +void moveFile(string from, string to) +{ + std.file.rename(from, to); +} + +/** + Copies a file. + + Note that attributes and time stamps are currently not retained. + + Params: + from = Path of the source file + to = Path for the destination file + overwrite = If true, any file existing at the destination path will be + overwritten. If this is false, an excpetion will be thrown should + a file already exist at the destination path. + + Throws: + An Exception if the copy operation fails for some reason. +*/ +void copyFile(Path from, Path to, bool overwrite = false) +{ + { + auto src = openFile(from, FileMode.Read); + scope(exit) src.close(); + enforce(overwrite || !existsFile(to), "Destination file already exists."); + auto dst = openFile(to, FileMode.CreateTrunc); + scope(exit) dst.close(); + dst.write(src); + } + + // TODO: retain attributes and time stamps +} +/// ditto +void copyFile(string from, string to) +{ + copyFile(Path(from), Path(to)); +} + +/** + Removes a file +*/ +void removeFile(Path path) +{ + removeFile(path.toNativeString()); +} +/// ditto +void removeFile(string path) { + std.file.remove(path); +} + +/** + Checks if a file exists +*/ +bool existsFile(Path path) { + return existsFile(path.toNativeString()); +} +/// ditto +bool existsFile(string path) +{ + return std.file.exists(path); +} + +/** Stores information about the specified file/directory into 'info' + + Returns false if the file does not exist. +*/ +FileInfo getFileInfo(Path path) +{ + auto ent = std.file.dirEntry(path.toNativeString()); + return makeFileInfo(ent); +} +/// ditto +FileInfo getFileInfo(string path) +{ + return getFileInfo(Path(path)); +} + +/** + Creates a new directory. +*/ +void createDirectory(Path path) +{ + mkdir(path.toNativeString()); +} +/// ditto +void createDirectory(string path) +{ + createDirectory(Path(path)); +} + +/** + Enumerates all files in the specified directory. +*/ +void listDirectory(Path path, scope bool delegate(FileInfo info) del) +{ + foreach( DirEntry ent; dirEntries(path.toNativeString(), SpanMode.shallow) ) + if( !del(makeFileInfo(ent)) ) + break; +} +/// ditto +void listDirectory(string path, scope bool delegate(FileInfo info) del) +{ + listDirectory(Path(path), del); +} +/// ditto +int delegate(scope int delegate(ref FileInfo)) iterateDirectory(Path path) +{ + int iterator(scope int delegate(ref FileInfo) del){ + int ret = 0; + listDirectory(path, (fi){ + ret = del(fi); + return ret == 0; + }); + return ret; + } + return &iterator; +} +/// ditto +int delegate(scope int delegate(ref FileInfo)) iterateDirectory(string path) +{ + return iterateDirectory(Path(path)); +} + + +/** + Returns the current working directory. +*/ +Path getWorkingDirectory() +{ + return Path(std.file.getcwd()); +} + + +/** Contains general information about a file. +*/ +struct FileInfo { + /// Name of the file (not including the path) + string name; + + /// Size of the file (zero for directories) + ulong size; + + /// Time of the last modification + SysTime timeModified; + + /// Time of creation (not available on all operating systems/file systems) + SysTime timeCreated; + + /// True if this is a symlink to an actual file + bool isSymlink; + + /// True if this is a directory or a symlink pointing to a directory + bool isDirectory; +} + +/** + Specifies how a file is manipulated on disk. +*/ +enum FileMode { + /// The file is opened read-only. + Read, + /// The file is opened for read-write random access. + ReadWrite, + /// The file is truncated if it exists and created otherwise and the opened for read-write access. + CreateTrunc, + /// The file is opened for appending data to it and created if it does not exist. + Append +} + +/** + Accesses the contents of a file as a stream. +*/ + +private FileInfo makeFileInfo(DirEntry ent) +{ + FileInfo ret; + ret.name = baseName(ent.name); + if( ret.name.length == 0 ) ret.name = ent.name; + assert(ret.name.length > 0); + ret.size = ent.size; + ret.timeModified = ent.timeLastModified; + version(Windows) ret.timeCreated = ent.timeCreated; + else ret.timeCreated = ent.timeLastModified; + ret.isSymlink = ent.isSymlink; + ret.isDirectory = ent.isDir; + return ret; +} + diff --git a/source/vibecompat/core/log.d b/source/vibecompat/core/log.d new file mode 100644 index 0000000..c015bae --- /dev/null +++ b/source/vibecompat/core/log.d @@ -0,0 +1,97 @@ +/** + Central logging facility for vibe. + + Copyright: © 2012 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 vibecompat.core.log; + +import std.array; +import std.datetime; +import std.format; +import std.stdio; +import core.thread; + +private { + shared LogLevel s_minLevel = LogLevel.Info; + shared LogLevel s_logFileLevel; + shared bool s_plainLogging = false; +} + +/// Sets the minimum log level to be printed. +void setLogLevel(LogLevel level) nothrow +{ + s_minLevel = level; +} + +/// Disables output of thread/task ids with each log message +void setPlainLogging(bool enable) +{ + s_plainLogging = enable; +} + +/** + Logs a message. + + Params: + level = The log level for the logged message + fmt = See http://dlang.org/phobos/std_format.html#format-string +*/ +void logTrace(T...)(string fmt, auto ref T args) nothrow { log(LogLevel.Trace, fmt, args); } +/// ditto +void logDebug(T...)(string fmt, auto ref T args) nothrow { log(LogLevel.Debug, fmt, args); } +/// ditto +void logInfo(T...)(string fmt, auto ref T args) nothrow { log(LogLevel.Info, fmt, args); } +/// ditto +void logWarn(T...)(string fmt, auto ref T args) nothrow { log(LogLevel.Warn, fmt, args); } +/// ditto +void logError(T...)(string fmt, auto ref T args) nothrow { log(LogLevel.Error, fmt, args); } + +/// ditto +void log(T...)(LogLevel level, string fmt, auto ref T args) +nothrow { + if( level < s_minLevel ) return; + string pref; + final switch( level ){ + case LogLevel.Trace: pref = "trc"; break; + case LogLevel.Debug: pref = "dbg"; break; + case LogLevel.Info: pref = "INF"; break; + case LogLevel.Warn: pref = "WRN"; break; + case LogLevel.Error: pref = "ERR"; break; + case LogLevel.Fatal: pref = "FATAL"; break; + case LogLevel.None: assert(false); + } + + try { + auto txt = appender!string(); + txt.reserve(256); + formattedWrite(txt, fmt, args); + + auto threadid = cast(ulong)cast(void*)Thread.getThis(); + auto fiberid = cast(ulong)cast(void*)Fiber.getThis(); + threadid ^= threadid >> 32; + fiberid ^= fiberid >> 32; + + if( level >= s_minLevel ){ + if( s_plainLogging ) writeln(txt.data()); + else writefln("[%08X:%08X %s] %s", threadid, fiberid, pref, txt.data()); + stdout.flush(); + } + } catch( Exception e ){ + // this is bad but what can we do.. + debug assert(false, e.msg); + } +} + +/// Specifies the log level for a particular log message. +enum LogLevel { + Trace, + Debug, + Info, + Warn, + Error, + Fatal, + None +} + diff --git a/source/vibecompat/data/json.d b/source/vibecompat/data/json.d new file mode 100644 index 0000000..3fb6e9d --- /dev/null +++ b/source/vibecompat/data/json.d @@ -0,0 +1,1282 @@ +/** + JSON serialization and value handling. + + This module provides the Json struct for reading, writing and manipulating JSON values in a seamless, + JavaScript like way. De(serialization) of arbitrary D types is also supported. + + Examples: + + --- + void manipulateJson(Json j) + { + // object members can be accessed using member syntax, just like in JavaScript + j = Json.EmptyObject; + j.name = "Example"; + j.id = 1; + + // retrieving the values is done using get() + assert(j["name"].get!string == "Example"); + assert(j["id"].get!int == 1); + + // semantic convertions can be done using to() + assert(j.id.to!string == "1"); + + // prints: + // name: "Example" + // id: 1 + foreach( string key, value; j ){ + writefln("%s: %s", key, value); + } + + // print out as JSON: {"name": "Example", "id": 1} + writefln("JSON: %s", j.toString()); + } + --- + + Copyright: © 2012 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 vibecompat.data.json; + +import vibecompat.data.utils; + +import std.array; +import std.conv; +import std.datetime; +import std.exception; +import std.format; +import std.string; +import std.range; +import std.traits; + + +/******************************************************************************/ +/* public types */ +/******************************************************************************/ + +/** + Represents a single JSON value. + + Json values can have one of the types defined in the Json.Type enum. They + 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 + 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; + }; + Type m_type = Type.Undefined; + } + + /** 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 + } + + /// New JSON value of Type.Undefined + static @property Json Undefined() { return Json(); } + + /// New JSON value of Type.Object + static @property Json EmptyObject() { return Json(cast(Json[string])null); } + + /// New JSON value of Type.Array + static @property Json EmptyArray() { return Json(cast(Json[])null); } + + version(JsonLineNumbers) int line; + + /** + Constructor for a JSON object. + */ + this(typeof(null)) { m_type = Type.Null; } + /// ditto + this(bool v) { m_type = Type.Bool; m_bool = v; } + /// ditto + this(int v) { m_type = Type.Int; m_int = v; } + /// ditto + this(long v) { m_type = Type.Int; m_int = v; } + /// ditto + this(double v) { m_type = Type.Float; m_float = v; } + /// ditto + this(string v) { m_type = Type.String; m_string = v; } + /// ditto + this(Json[] v) { m_type = Type.Array; m_array = v; } + /// ditto + this(Json[string] v) { m_type = Type.Object; m_object = v; } + + /** + Allows assignment of D values to a JSON value. + */ + ref Json opAssign(Json v){ + m_type = v.m_type; + final switch(m_type){ + case Type.Undefined: m_string = null; break; + case Type.Null: m_string = null; break; + case Type.Bool: m_bool = v.m_bool; break; + 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; break; + case Type.Object: m_object = v.m_object; break; + } + return this; + } + /// ditto + void opAssign(typeof(null)) { m_type = Type.Null; m_string = null; } + /// ditto + bool opAssign(bool v) { m_type = Type.Bool; m_bool = v; return v; } + /// ditto + int opAssign(int v) { m_type = Type.Int; m_int = v; return v; } + /// ditto + long opAssign(long v) { m_type = Type.Int; m_int = v; return v; } + /// ditto + double opAssign(double v) { m_type = Type.Float; m_float = v; return v; } + /// ditto + string opAssign(string v) { m_type = Type.String; m_string = v; return v; } + /// ditto + Json[] opAssign(Json[] v) { m_type = Type.Array; m_array = v; return v; } + /// ditto + Json[string] opAssign(Json[string] v) { m_type = Type.Object; m_object = v; return v; } + + /** + The current type id of this JSON object. + */ + @property Type type() const { return m_type; } + + /** + Allows direct indexing of array typed JSON values. + */ + ref inout(Json) opIndex(size_t idx) inout { checkType!(Json[])(); return m_array[idx]; } + + /** + Allows direct indexing of object typed JSON values using a string as + the key. + */ + 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; + return ret; + } + /// ditto + ref Json opIndex(string key){ + checkType!(Json[string])(); + if( auto pv = key in m_object ) + return *pv; + m_object[key] = Json(); + m_object[key].m_type = Type.Undefined; // DMDBUG: AAs are teh $H1T!!!11 + assert(m_object[key].type == Type.Undefined); + m_object[key].m_string = key; + return m_object[key]; + } + + /** + Returns a slice of a JSON array. + */ + inout(Json[]) opSlice() inout { checkType!(Json[])(); return m_array; } + /// + inout(Json[]) opSlice(size_t from, size_t to) inout { checkType!(Json[])(); return m_array[from .. to]; } + + /** + Returns the number of entries of string, array or object typed JSON values. + */ + @property size_t length() + const { + switch(m_type){ + case Type.String: return m_string.length; + case Type.Array: return m_array.length; + case Type.Object: return m_object.length; + default: + enforce(false, "Json.length() can only be called on strings, arrays and objects, not "~.to!string(m_type)~"."); + return 0; + } + } + + /** + Allows foreach iterating over JSON objects and arrays. + */ + int opApply(int delegate(ref Json obj) del) + { + enforce(m_type == Type.Array || m_type == Type.Object, "opApply may only be called on objects and arrays, not "~.to!string(m_type)~"."); + if( m_type == Type.Array ){ + foreach( ref v; m_array ) + if( auto ret = del(v) ) + return ret; + return 0; + } else { + foreach( ref v; m_object ) + if( v.type != Type.Undefined ) + if( auto ret = del(v) ) + return ret; + return 0; + } + } + /// ditto + int opApply(int delegate(ref const Json obj) del) + const { + enforce(m_type == Type.Array || m_type == Type.Object, "opApply may only be called on objects and arrays, not "~.to!string(m_type)~"."); + if( m_type == Type.Array ){ + foreach( ref v; m_array ) + if( auto ret = del(v) ) + return ret; + return 0; + } else { + foreach( ref v; m_object ) + if( v.type != Type.Undefined ) + if( auto ret = del(v) ) + return ret; + return 0; + } + } + /// ditto + int opApply(int delegate(ref size_t idx, ref Json obj) del) + { + enforce(m_type == Type.Array, "opApply may only be called on arrays, not "~.to!string(m_type)~""); + foreach( idx, ref v; m_array ) + if( auto ret = del(idx, v) ) + return ret; + return 0; + } + /// ditto + int opApply(int delegate(ref size_t idx, ref const Json obj) del) + const { + enforce(m_type == Type.Array, "opApply may only be called on arrays, not "~.to!string(m_type)~"."); + foreach( idx, ref v; m_array ) + if( auto ret = del(idx, v) ) + return ret; + return 0; + } + /// ditto + int opApply(int delegate(ref string idx, ref Json obj) del) + { + enforce(m_type == Type.Object, "opApply may only be called on objects, not "~.to!string(m_type)~"."); + foreach( idx, ref v; m_object ) + if( v.type != Type.Undefined ) + if( auto ret = del(idx, v) ) + return ret; + return 0; + } + /// ditto + int opApply(int delegate(ref string idx, ref const Json obj) del) + const { + enforce(m_type == Type.Object, "opApply may only be called on objects, not "~.to!string(m_type)~"."); + foreach( idx, ref v; m_object ) + if( v.type != Type.Undefined ) + if( auto ret = del(idx, v) ) + return ret; + return 0; + } + + /** + Converts the JSON value to the corresponding D type - types must match exactly. + */ + 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~"."); + } + /// ditto + @property const(T) opt(T)(const(T) def = T.init) + const { + if( typeId!T != m_type ) return def; + return get!T; + } + /// ditto + @property T opt(T)(T def = T.init) + { + if( typeId!T != m_type ) return def; + return get!T; + } + + /** + Converts the JSON value to the corresponding D type - types are converted as neccessary. + */ + @property inout(T) to(T)() + inout { + static if( is(T == bool) ){ + final switch( m_type ){ + case Type.Undefined: return false; + case Type.Null: return false; + case Type.Bool: return m_bool; + case Type.Int: return m_int != 0; + case Type.Float: return m_float != 0; + case Type.String: return m_string.length > 0; + case Type.Array: return m_array.length > 0; + case Type.Object: return m_object.length > 0; + } + } else static if( is(T == double) ){ + final switch( m_type ){ + case Type.Undefined: return T.init; + case Type.Null: return 0; + case Type.Bool: return m_bool ? 1 : 0; + case Type.Int: return m_int; + case Type.Float: return m_float; + case Type.String: return .to!double(cast(string)m_string); + case Type.Array: return double.init; + case Type.Object: return double.init; + } + } else static if( is(T == float) ){ + final switch( m_type ){ + case Type.Undefined: return T.init; + case Type.Null: return 0; + case Type.Bool: return m_bool ? 1 : 0; + case Type.Int: return m_int; + case Type.Float: return m_float; + case Type.String: return .to!float(cast(string)m_string); + case Type.Array: return float.init; + case Type.Object: return float.init; + } + } + else static if( is(T == long) ){ + final switch( m_type ){ + case Type.Undefined: return 0; + case Type.Null: return 0; + case Type.Bool: return m_bool ? 1 : 0; + case Type.Int: return m_int; + case Type.Float: return cast(long)m_float; + case Type.String: return .to!long(m_string); + case Type.Array: return 0; + case Type.Object: return 0; + } + } else static if( is(T : long) ){ + final switch( m_type ){ + case Type.Undefined: return 0; + case Type.Null: return 0; + case Type.Bool: return m_bool ? 1 : 0; + case Type.Int: return cast(T)m_int; + case Type.Float: return cast(T)m_float; + case Type.String: return cast(T).to!long(cast(string)m_string); + case Type.Array: return 0; + case Type.Object: return 0; + } + } else static if( is(T == string) ){ + switch( m_type ){ + default: return toString(); + case Type.String: return m_string; + } + } else static if( is(T == Json[]) ){ + switch( m_type ){ + default: return Json([this]); + case Type.Array: return m_array; + } + } else static if( is(T == Json[string]) ){ + switch( m_type ){ + 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~"."); + } + + /** + Performs unary operations on the JSON value. + + The following operations are supported for each type: + + $(DL + $(DT Null) $(DD none) + $(DT Bool) $(DD ~) + $(DT Int) $(DD +, -, ++, --) + $(DT Float) $(DD +, -, ++, --) + $(DT String) $(DD none) + $(DT Array) $(DD none) + $(DT Object) $(DD none) + ) + */ + Json opUnary(string op)() + const { + static if( op == "~" ){ + checkType!bool(); + return Json(~m_bool); + } else static if( op == "+" || op == "-" || op == "++" || 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 enforce(false, "'"~op~"' only allowed on scalar types, not on "~.to!string(m_type)~"."); + } else static assert("Unsupported operator '"~op~"' for type JSON."); + } + + /** + Performs binary operations between JSON values. + + The two JSON values must be of the same run time type or an exception + will be thrown. Only the operations listed are allowed for each of the + types. + + $(DL + $(DT Null) $(DD none) + $(DT Bool) $(DD &&, ||) + $(DT Int) $(DD +, -, *, /, %) + $(DT Float) $(DD +, -, *, /, %) + $(DT String) $(DD ~) + $(DT Array) $(DD ~) + $(DT Object) $(DD none) + ) + */ + 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."); + static if( op == "&&" ){ + enforce(m_type == Type.Bool, "'&&' only allowed for Type.Bool, not "~.to!string(m_type)~"."); + return Json(m_bool && other.m_bool); + } else static if( op == "||" ){ + enforce(m_type == Type.Bool, "'||' only allowed for Type.Bool, not "~.to!string(m_type)~"."); + return Json(m_bool || other.m_bool); + } else static if( 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 enforce(false, "'+' only allowed for scalar types, not "~.to!string(m_type)~"."); + } else static if( 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 enforce(false, "'-' only allowed for scalar types, not "~.to!string(m_type)~"."); + } else static if( 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 enforce(false, "'*' only allowed for scalar types, not "~.to!string(m_type)~"."); + } else static if( 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 enforce(false, "'/' only allowed for scalar types, not "~.to!string(m_type)~"."); + } else static if( 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 enforce(false, "'%' only allowed for scalar types, not "~.to!string(m_type)~"."); + } else static if( op == "~" ){ + if( m_type == Type.String ) return Json(m_string ~ other.m_string); + else enforce(false, "'~' only allowed for strings, not "~.to!string(m_type)~"."); + } else static assert("Unsupported operator '"~op~"' for type JSON."); + assert(false); + } + /// ditto + Json opBinary(string op)(Json other) + if( op == "~" ) + { + static if( 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 enforce(false, "'~' only allowed for strings and arrays, not "~.to!string(m_type)~"."); + } else static assert("Unsupported operator '"~op~"' for type JSON."); + assert(false); + } + /// ditto + void opOpAssign(string op)(Json other) + if( 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."); + 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.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.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.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.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); + } + /// ditto + Json opBinary(string op)(bool other) const { checkType!bool(); mixin("return Json(m_bool "~op~" other);"); } + /// ditto + Json opBinary(string op)(long other) const { checkType!long(); mixin("return Json(m_int "~op~" other);"); } + /// ditto + Json opBinary(string op)(double other) const { checkType!double(); mixin("return Json(m_float "~op~" other);"); } + /// ditto + Json opBinary(string op)(string other) const { checkType!string(); mixin("return Json(m_string "~op~" other);"); } + /// ditto + 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 + Json opBinaryRight(string op)(long other) const { checkType!long(); mixin("return Json(other "~op~" m_int);"); } + /// ditto + Json opBinaryRight(string op)(double other) const { checkType!double(); mixin("return Json(other "~op~" m_float);"); } + /// ditto + Json opBinaryRight(string op)(string other) const if(op == "~") { checkType!string(); return Json(other ~ m_string); } + /// ditto + inout(Json)* opBinaryRight(string op)(string other) inout if(op == "in") { + checkType!(Json[string])(); + auto pv = other in m_object; + if( !pv ) return null; + 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);"); } + + /** + Allows to access existing fields of a JSON object using dot syntax. + */ + @property const(Json) opDispatch(string prop)() const { return opIndex(prop); } + /// ditto + @property ref Json opDispatch(string prop)() { return opIndex(prop); } + + /** + Compares two JSON values for equality. + + If the two values have different types, they are considered unequal. + This differs with ECMA script, which performs a type conversion before + comparing the values. + */ + bool opEquals(ref const Json other) + const { + if( m_type != other.m_type ) return false; + final switch(m_type){ + case Type.Undefined: return false; + case Type.Null: return true; + case Type.Bool: return m_bool == other.m_bool; + case Type.Int: return m_int == other.m_int; + case Type.Float: return m_float == other.m_float; + case Type.String: return m_string == other.m_string; + case Type.Array: return m_array == other.m_array; + case Type.Object: return m_object == other.m_object; + } + } + /// ditto + bool opEquals(const Json other) const { return opEquals(other); } + /// ditto + bool opEquals(typeof(null)) const { return m_type == Type.Null; } + /// ditto + bool opEquals(bool v) const { return m_type == Type.Bool && m_bool == 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; } + /// ditto + bool opEquals(string v) const { return m_type == Type.String && m_string == v; } + + /** + Compares two JSON values. + + If the types of the two values differ, the value with the smaller type + id is considered the smaller value. This differs from ECMA script, which + performs a type conversion before comparing the values. + + JSON values of type Object cannot be compared and will throw an + exception. + */ + int opCmp(ref const Json other) + const { + if( m_type != other.m_type ) return m_type < other.m_type ? -1 : 1; + final switch(m_type){ + case Type.Undefined: return 0; + case Type.Null: return 0; + case Type.Bool: return m_bool < other.m_bool ? -1 : m_bool == other.m_bool ? 0 : 1; + case Type.Int: return m_int < other.m_int ? -1 : m_int == other.m_int ? 0 : 1; + case Type.Float: return m_float < other.m_float ? -1 : m_float == other.m_float ? 0 : 1; + 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."); + assert(false); + } + } + + + + /** + Returns the type id corresponding to the given D type. + */ + static @property Type typeId(T)() { + static if( is(T == typeof(null)) ) return Type.Null; + else static if( is(T == bool) ) return Type.Bool; + else static if( is(T == double) ) return Type.Float; + else static if( is(T == float) ) return Type.Float; + else static if( is(T : long) ) return Type.Int; + else static if( is(T == string) ) return Type.String; + else static if( is(T == Json[]) ) return Type.Array; + else static if( is(T == Json[string]) ) return Type.Object; + else static assert(false, "Unsupported JSON type '"~T.stringof~"'. Only bool, long, double, string, Json[] and Json[string] are allowed."); + } + + /** + Returns the JSON object as a string. + + For large JSON values use writeJsonString instead as this function will store the whole string + in memory, whereas writeJsonString writes it out bit for bit. + + See_Also: writeJsonString, toPrettyString + */ + string toString() + const { + auto ret = appender!string(); + writeJsonString(ret, this); + return ret.data; + } + + /** + Returns the JSON object as a "pretty" string. + + --- + auto json = Json(["foo": Json("bar")]); + writeln(json.toPrettyString()); + + // output: + // { + // "foo": "bar" + // } + --- + + Params: + level = Specifies the base amount of indentation for the output. Indentation is always + done using tab characters. + + See_Also: writePrettyJsonString, toString + */ + string toPrettyString(int level = 0) + const { + auto ret = appender!string(); + writePrettyJsonString(ret, this, level); + return ret.data; + } + + private void checkType(T)() + const { + string dbg; + if( m_type == Type.Undefined ) dbg = " field "~m_string; + enforce(typeId!T == m_type, "Trying to access JSON"~dbg~" of type "~.to!string(m_type)~" as "~T.stringof~"."); + } + + /*invariant() + { + assert(m_type >= Type.Undefined && m_type <= Type.Object); + }*/ +} + + +/******************************************************************************/ +/* public functions */ +/******************************************************************************/ + +/** + Parses the given range as a JSON string and returns the corresponding Json object. + + 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. +*/ +Json parseJson(R)(ref R range, int* line = null) + if( is(R == string) ) +{ + Json ret; + enforce(!range.empty, "JSON string is empty."); + + skipWhitespace(range, line); + + version(JsonLineNumbers){ + import vibecompat.core.log; + int curline = line ? *line : 0; + scope(failure) logError("Error in line: %d", curline); + } + + switch( range.front ){ + case 'f': + enforce(range[1 .. $].startsWith("alse"), "Expected 'false', got '"~range[0 .. 5]~"'."); + range.popFrontN(5); + ret = false; + break; + case 'n': + enforce(range[1 .. $].startsWith("ull"), "Expected 'null', got '"~range[0 .. 4]~"'."); + range.popFrontN(4); + ret = null; + break; + case 't': + enforce(range[1 .. $].startsWith("rue"), "Expected 'true', got '"~range[0 .. 4]~"'."); + range.popFrontN(4); + ret = true; + break; + case '0': .. case '9'+1: + case '-': + bool is_float; + auto num = skipNumber(range, is_float); + if( is_float ) ret = to!double(num); + else ret = to!long(num); + break; + case '\"': + ret = skipJsonString(range); + break; + case '[': + Json[] arr; + range.popFront(); + while(true) { + skipWhitespace(range, line); + enforce(!range.empty); + if(range.front == ']') break; + arr ~= parseJson(range, line); + skipWhitespace(range, line); + enforce(!range.empty && (range.front == ',' || range.front == ']'), "Expected ']' or ','."); + if( range.front == ']' ) break; + else range.popFront(); + } + range.popFront(); + ret = arr; + break; + case '{': + Json[string] obj; + range.popFront(); + while(true) { + skipWhitespace(range, line); + enforce(!range.empty); + if(range.front == '}') break; + string key = skipJsonString(range); + skipWhitespace(range, line); + enforce(range.startsWith(":"), "Expected ':' for key '" ~ key ~ "'"); + range.popFront(); + skipWhitespace(range, line); + Json itm = parseJson(range, line); + obj[key] = itm; + skipWhitespace(range, line); + enforce(!range.empty && (range.front == ',' || range.front == '}'), "Expected '}' or ',' - got '"~range[0]~"'."); + if( range.front == '}' ) break; + else range.popFront(); + } + range.popFront(); + ret = obj; + break; + default: + enforce(false, "Expected valid json token, got '"~to!string(range.length)~range[0 .. range.length>12?12:range.length]~"'."); + } + + assert(ret.type != Json.Type.Undefined); + version(JsonLineNumbers) ret.line = curline; + return ret; +} + +/** + Parses the given JSON string and returns the corresponding Json object. + + Throws an Exception if any parsing error occurs. +*/ +Json parseJsonString(string str) +{ + auto ret = parseJson(str); + enforce(str.strip().length == 0, "Expected end of string after JSON value."); + return ret; +} + +unittest { + assert(parseJsonString("null") == Json(null)); + assert(parseJsonString("true") == Json(true)); + assert(parseJsonString("false") == Json(false)); + assert(parseJsonString("1") == Json(1)); + assert(parseJsonString("2.0") == Json(2.0)); + assert(parseJsonString("\"test\"") == Json("test")); + 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"); +} + + +/** + Serializes the given value to JSON. + + 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) + ) + + All entries of an array or an associative array, as well as all R/W properties and + all public fields of a struct/class are recursively serialized using the same rules. + + Fields ending with an underscore will have the last underscore stripped in the + serialized output. This makes it possible to use fields with D keywords as their name + by simply appending an underscore. + + The following methods can be used to customize the serialization of structs/classes: + + --- + Json toJson() const; + static T fromJson(Json src); + + string toString() const; + static T fromString(string src); + --- + + 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). +*/ +Json serializeToJson(T)(T value) +{ + alias Unqual!T TU; + 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 ){ + auto ret = new Json[value.length]; + foreach( i; 0 .. value.length ) + ret[i] = serializeToJson(value[i]); + return Json(ret); + } else static if( isAssociativeArray!TU ){ + Json[string] ret; + foreach( string key, value; value ) + ret[key] = serializeToJson(value); + return Json(ret); + } else static if( __traits(compiles, value = T.fromJson(value.toJson())) ){ + return value.toJson(); + } else static if( __traits(compiles, value = T.fromString(value.toString())) ){ + return Json(value.toString()); + } else static if( is(TU == struct) ){ + Json[string] ret; + 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); + Json[string] ret; + 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); + return serializeToJson(*value); + } else { + static assert(false, "Unsupported type '"~T.stringof~"' for JSON serialization."); + } +} + + +/** + Deserializes a JSON value into the destination variable. + + The same types as for serializeToJson() are supported and handled inversely. +*/ +void deserializeJson(T)(ref T dst, Json src) +{ + dst = deserializeJson!T(src); +} +/// 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 ){ + alias typeof(T.init[0]) TV; + auto dst = new Unqual!TV[src.length]; + foreach( size_t i, v; src ) + dst[i] = deserializeJson!(Unqual!TV)(v); + return dst; + } else static if( isAssociativeArray!T ){ + alias typeof(T.init.values[0]) TV; + Unqual!TV[string] dst; + foreach( string key, value; src ) + dst[key] = deserializeJson!(Unqual!TV)(value); + return dst; + } else static if( __traits(compiles, { T dst; dst = T.fromJson(dst.toJson()); }()) ){ + return T.fromJson(src); + } else static if( __traits(compiles, { T dst; dst = T.fromString(dst.toString()); }()) ){ + return T.fromString(src.get!string); + } else static if( is(T == struct) ){ + T dst; + foreach( m; __traits(allMembers, T) ){ + static if( isRWPlainField!(T, m) || isRWField!(T, m) ){ + alias typeof(__traits(getMember, dst, m)) TM; + __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; + auto dst = new T; + foreach( m; __traits(allMembers, T) ){ + static if( isRWPlainField!(T, m) || isRWField!(T, m) ){ + alias typeof(__traits(getMember, dst, m)) TM; + __traits(getMember, dst, m) = deserializeJson!TM(src[underscoreStrip(m)]); + } + } + return dst; + } else static if( isPointer!T ){ + if( src.type == Json.Type.Null ) return null; + alias typeof(*T.init) TD; + dst = new TD; + *dst = deserializeJson!TD(src); + return dst; + } else { + static assert(false, "Unsupported type '"~T.stringof~"' for JSON serialization."); + } +} + +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]}; + S u; + deserializeJson(u, serializeToJson(t)); + assert(t.a == u.a); + assert(t.b == u.b); + assert(t.c == u.c); + assert(t.d == u.d); + assert(t.e == u.e); + assert(t.f == u.f); + assert(t.g == u.g); + assert(t.h == u.h); + assert(t.i == u.i); + assert(t.j == u.j); +} + +unittest { + static class C { + int a; + private int _b; + @property int b() const { return _b; } + @property void b(int v) { _b = v; } + + @property int test() const { return 10; } + + void test2() {} + } + C c = new C; + c.a = 1; + c.b = 2; + + C d; + deserializeJson(d, serializeToJson(c)); + assert(c.a == d.a); + assert(c.b == d.b); +} + + +/** + 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. + + Params: + dst = References the string output range to which the result is written. + json = Specifies the JSON value that is to be stringified. + + See_Also: Json.toString, writePrettyJsonString +*/ +void writeJsonString(R)(ref R dst, in Json json) +// 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( ref const Json e; json ){ + if( e.type == Json.Type.Undefined ) continue; + if( !first ) dst.put(","); + first = false; + writeJsonString(dst, e); + } + dst.put("]"); + break; + case Json.Type.Object: + dst.put("{"); + bool first = true; + foreach( string k, ref const Json e; json ){ + if( e.type == Json.Type.Undefined ) continue; + if( !first ) dst.put(","); + first = false; + dst.put("\""); + jsonEscape(dst, k); + dst.put("\":"); + writeJsonString(dst, e); + } + dst.put("}"); + break; + } +} + +/** + Writes the given JSON object as a prettified JSON string into the destination range. + + The output will contain newlines and indents to make the output human readable. + + 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. + + 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 ) dst.put('\t'); + writePrettyJsonString(dst, e, level+1); + } + if( json.length > 0 ) { + dst.put('\n'); + foreach( tab; 0 .. (level-1) ) 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 ) 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-1) ) dst.put('\t'); + } + dst.put("}"); + break; + } +} + + +/** Deprecated aliases for backwards compatibility. + + Use writeJsonString and writePrettyJsonString instead. +*/ +deprecated("Please use writeJsonString instead.") alias writeJsonString toJson; +/// +deprecated("Please use writePrettyJsonString instead.") alias writePrettyJsonString toPrettyJson; + + +/// private +private void jsonEscape(R)(ref R dst, string s) +{ + foreach( ch; s ){ + switch(ch){ + default: dst.put(ch); break; + case '\\': dst.put("\\\\"); break; + case '\r': dst.put("\\r"); break; + case '\n': dst.put("\\n"); break; + case '\t': dst.put("\\t"); break; + case '\"': dst.put("\\\""); break; + } + } +} + +/// private +private string jsonUnescape(R)(ref R range) +{ + auto ret = appender!string(); + while(!range.empty){ + auto ch = range.front; + switch( ch ){ + case '"': return ret.data; + case '\\': + range.popFront(); + enforce(!range.empty, "Unterminated string escape sequence."); + switch(range.front){ + default: enforce("Invalid string escape sequence."); break; + case '"': ret.put('\"'); range.popFront(); break; + case '\\': ret.put('\\'); range.popFront(); break; + case '/': ret.put('/'); range.popFront(); break; + case 'b': ret.put('\b'); range.popFront(); break; + case 'f': ret.put('\f'); range.popFront(); break; + case 'n': ret.put('\n'); range.popFront(); break; + 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; + 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'."); + } + ret.put(uch); + break; + } + break; + default: + ret.put(ch); + range.popFront(); + break; + } + } + return ret.data; +} + +private string skipNumber(ref string s, out bool is_float) +{ + size_t idx = 0; + is_float = false; + if( s[idx] == '-' ) idx++; + if( s[idx] == '0' ) idx++; + else { + enforce(isDigit(s[idx++]), "Digit expected at beginning of number."); + while( idx < s.length && isDigit(s[idx]) ) idx++; + } + + if( idx < s.length && s[idx] == '.' ){ + idx++; + is_float = true; + while( idx < s.length && isDigit(s[idx]) ) idx++; + } + + if( idx < s.length && (s[idx] == 'e' || s[idx] == 'E') ){ + idx++; + is_float = true; + if( idx < s.length && (s[idx] == '+' || s[idx] == '-') ) idx++; + enforce( idx < s.length && isDigit(s[idx]), "Expected exponent." ~ s[0 .. idx]); + idx++; + while( idx < s.length && isDigit(s[idx]) ) idx++; + } + + string ret = s[0 .. idx]; + s = s[idx .. $]; + return ret; +} + +private string skipJsonString(ref string s, int* line = null) +{ + enforce(s.length >= 2 && s[0] == '\"', "too small: '" ~ s ~ "'"); + s = s[1 .. $]; + string ret = jsonUnescape(s); + enforce(s.length > 0 && s[0] == '\"', "Unterminated string literal."); + s = s[1 .. $]; + return ret; +} + +private void skipWhitespace(ref string s, int* line = null) +{ + while( s.length > 0 ){ + switch( s[0] ){ + default: return; + case ' ', '\t': s = s[1 .. $]; break; + case '\n': + s = s[1 .. $]; + if( s.length > 0 && s[0] == '\r' ) s = s[1 .. $]; + if( line ) (*line)++; + break; + case '\r': + s = s[1 .. $]; + if( s.length > 0 && s[0] == '\n' ) s = s[1 .. $]; + if( line ) (*line)++; + break; + } + } +} + +/// private +private bool isDigit(T)(T ch){ return ch >= '0' && ch <= '9'; } + +private string underscoreStrip(string field_name) +{ + if( field_name.length < 1 || field_name[$-1] != '_' ) return field_name; + else return field_name[0 .. $-1]; +} diff --git a/source/vibecompat/data/utils.d b/source/vibecompat/data/utils.d new file mode 100644 index 0000000..338dd61 --- /dev/null +++ b/source/vibecompat/data/utils.d @@ -0,0 +1,30 @@ +/** + Utility functions for data serialization + + Copyright: © 2012 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 vibecompat.data.utils; + +public import std.traits; + + +template isRWPlainField(T, string M) +{ + 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))); + } +} + +template isRWField(T, string M) +{ + enum isRWField = __traits(compiles, __traits(getMember, Tgen!T(), M) = __traits(getMember, Tgen!T(), M)); + //pragma(msg, T.stringof~"."~M~": "~(isRWField?"1":"0")); +} + +/// private +private T Tgen(T)(){ return T.init; } diff --git a/source/vibecompat/inet/path.d b/source/vibecompat/inet/path.d new file mode 100644 index 0000000..0157362 --- /dev/null +++ b/source/vibecompat/inet/path.d @@ -0,0 +1,295 @@ +/** + Contains routines for high level path handling. + + Copyright: © 2012 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 vibecompat.inet.path; + +import std.algorithm; +import std.array; +import std.conv; +import std.exception; +import std.string; + + +/** + Represents an absolute or relative file system path. + + This struct allows to do safe operations on paths, such as concatenation and sub paths. Checks + are done to disallow invalid operations such as concatenating two absolute paths. It also + validates path strings and allows for easy checking of malicious relative paths. +*/ +struct Path { + private { + immutable(PathEntry)[] m_nodes; + bool m_absolute = false; + bool m_endsWithSlash = false; + } + + /// Constructs a Path object by parsing a path string. + this(string pathstr) + { + m_nodes = cast(immutable)splitPath(pathstr); + m_absolute = (pathstr.startsWith("/") || m_nodes.length > 0 && m_nodes[0].toString().countUntil(':')>0); + m_endsWithSlash = pathstr.endsWith("/"); + foreach( e; m_nodes ) assert(e.toString().length > 0); + } + + /// Constructs a path object from a list of PathEntry objects. + this(immutable(PathEntry)[] nodes, bool absolute) + { + m_nodes = nodes; + m_absolute = absolute; + } + + /// Constructs a relative path with one path entry. + this(PathEntry entry){ + m_nodes = [entry]; + m_absolute = false; + } + + /// Determines if the path is absolute. + @property bool absolute() const { return m_absolute; } + + /// Resolves all '.' and '..' path entries as far as possible. + void normalize() + { + immutable(PathEntry)[] newnodes; + foreach( n; m_nodes ){ + switch(n.toString()){ + default: + newnodes ~= n; + break; + case ".": break; + case "..": + enforce(!m_absolute || newnodes.length > 0, "Path goes below root node."); + if( newnodes.length > 0 && newnodes[$-1] != ".." ) newnodes = newnodes[0 .. $-1]; + else newnodes ~= n; + break; + } + } + m_nodes = newnodes; + } + + /// Converts the Path back to a string representation using slashes. + string toString() + const { + if( m_nodes.empty ) return absolute ? "/" : ""; + + Appender!string ret; + + // for absolute paths start with / + if( absolute ) ret.put('/'); + + foreach( i, f; m_nodes ){ + if( i > 0 ) ret.put('/'); + ret.put(f.toString()); + } + + if( m_nodes.length > 0 && m_endsWithSlash ) + ret.put('/'); + + return ret.data; + } + + /// Converts the Path object to a native path string (backslash as path separator on Windows). + string toNativeString() + const { + Appender!string ret; + + // for absolute unix paths start with / + version(Posix) { if(absolute) ret.put('/'); } + + foreach( i, f; m_nodes ){ + version(Windows) { if( i > 0 ) ret.put('\\'); } + version(Posix) { if( i > 0 ) ret.put('/'); } + else { enforce("Unsupported OS"); } + ret.put(f.toString()); + } + + if( m_nodes.length > 0 && m_endsWithSlash ){ + version(Windows) { ret.put('\\'); } + version(Posix) { ret.put('/'); } + } + + return ret.data; + } + + /// Tests if `rhs` is an anchestor or the same as this path. + bool startsWith(const Path rhs) const { + if( rhs.m_nodes.length > m_nodes.length ) return false; + foreach( i; 0 .. rhs.m_nodes.length ) + if( m_nodes[i] != rhs.m_nodes[i] ) + return false; + return true; + } + + /// Computes the relative path from `parentPath` to this path. + Path relativeTo(const Path parentPath) const { + int nup = 0; + while( parentPath.length > nup && !startsWith(parentPath[0 .. parentPath.length-nup]) ){ + nup++; + } + Path ret = Path(null, false); + ret.m_endsWithSlash = true; + foreach( i; 0 .. nup ) ret ~= ".."; + ret ~= Path(m_nodes[parentPath.length-nup .. $], false); + return ret; + } + + /// The last entry of the path + @property ref immutable(PathEntry) head() const { enforce(m_nodes.length > 0); return m_nodes[$-1]; } + + /// The parent path + @property Path parentPath() const { return this[0 .. length-1]; } + + /// The ist of path entries of which this path is composed + @property immutable(PathEntry)[] nodes() const { return m_nodes; } + + /// The number of path entries of which this path is composed + @property size_t length() const { return m_nodes.length; } + + /// True if the path contains no entries + @property bool empty() const { return m_nodes.length == 0; } + + /// Determines if the path ends with a slash (i.e. is a directory) + @property bool endsWithSlash() const { return m_endsWithSlash; } + /// ditto + @property void endsWithSlash(bool v) { m_endsWithSlash = v; } + + /// Determines if this path goes outside of its base path (i.e. begins with '..'). + @property bool external() const { return !m_absolute && m_nodes.length > 0 && m_nodes[0].m_name == ".."; } + + ref immutable(PathEntry) opIndex(size_t idx) const { return m_nodes[idx]; } + Path opSlice(size_t start, size_t end) const { + auto ret = Path(m_nodes[start .. end], start == 0 ? absolute : false); + if( end == m_nodes.length ) ret.m_endsWithSlash = m_endsWithSlash; + return ret; + } + size_t opDollar(int dim)() const if(dim == 0) { return m_nodes.length; } + + + Path opBinary(string OP)(const Path rhs) const if( OP == "~" ) { + Path ret; + ret.m_nodes = m_nodes; + ret.m_absolute = m_absolute; + ret.m_endsWithSlash = rhs.m_endsWithSlash; + ret.normalize(); // needed to avoid "."~".." become "" instead of ".." + + assert(!rhs.absolute, "Trying to append absolute path."); + size_t idx = m_nodes.length; + foreach(folder; rhs.m_nodes){ + switch(folder.toString()){ + default: ret.m_nodes = ret.m_nodes ~ folder; break; + case ".": break; + case "..": + enforce(!ret.absolute || ret.m_nodes.length > 0, "Relative path goes below root node!"); + if( ret.m_nodes.length > 0 && ret.m_nodes[$-1].toString() != ".." ) + ret.m_nodes = ret.m_nodes[0 .. $-1]; + else ret.m_nodes = ret.m_nodes ~ folder; + break; + } + } + return ret; + } + + Path opBinary(string OP)(string rhs) const if( OP == "~" ) { assert(rhs.length > 0); return opBinary!"~"(Path(rhs)); } + Path opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { assert(rhs.toString().length > 0); return opBinary!"~"(Path(rhs)); } + void opOpAssign(string OP)(string rhs) if( OP == "~" ) { assert(rhs.length > 0); opOpAssign!"~"(Path(rhs)); } + void opOpAssign(string OP)(PathEntry rhs) if( OP == "~" ) { assert(rhs.toString().length > 0); opOpAssign!"~"(Path(rhs)); } + void opOpAssign(string OP)(Path rhs) if( OP == "~" ) { auto p = this ~ rhs; m_nodes = p.m_nodes; m_endsWithSlash = rhs.m_endsWithSlash; } + + /// Tests two paths for equality using '=='. + bool opEquals(ref const Path rhs) const { + if( m_absolute != rhs.m_absolute ) return false; + if( m_endsWithSlash != rhs.m_endsWithSlash ) return false; + if( m_nodes.length != rhs.length ) return false; + foreach( i; 0 .. m_nodes.length ) + if( m_nodes[i] != rhs.m_nodes[i] ) + return false; + return true; + } + /// ditto + bool opEquals(const Path other) const { return opEquals(other); } + + int opCmp(ref const Path rhs) const { + if( m_absolute != rhs.m_absolute ) return cast(int)m_absolute - cast(int)rhs.m_absolute; + foreach( i; 0 .. min(m_nodes.length, rhs.m_nodes.length) ) + if( m_nodes[i] != rhs.m_nodes[i] ) + return m_nodes[i].opCmp(rhs.m_nodes[i]); + if( m_nodes.length > rhs.m_nodes.length ) return 1; + if( m_nodes.length < rhs.m_nodes.length ) return -1; + return 0; + } +} + +struct PathEntry { + private { + string m_name; + } + + this(string str) + { + assert(str.countUntil('/') < 0 && str.countUntil('\\') < 0); + m_name = str; + } + + string toString() const { return m_name; } + + Path opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return Path(cast(immutable)[this, rhs], false); } + + bool opEquals(ref const PathEntry rhs) const { return m_name == rhs.m_name; } + bool opEquals(PathEntry rhs) const { return m_name == rhs.m_name; } + bool opEquals(string rhs) const { return m_name == rhs; } + int opCmp(ref const PathEntry rhs) const { return m_name.cmp(rhs.m_name); } + int opCmp(string rhs) const { return m_name.cmp(rhs); } +} + +private bool isValidFilename(string str) +{ + foreach( ch; str ) + if( ch == '/' || /*ch == ':' ||*/ ch == '\\' ) return false; + return true; +} + +/// Joins two path strings. subpath must be relative. +string joinPath(string basepath, string subpath) +{ + Path p1 = Path(basepath); + Path p2 = Path(subpath); + return (p1 ~ p2).toString(); +} + +/// Splits up a path string into its elements/folders +PathEntry[] splitPath(string path) +{ + if( path.startsWith("/") || path.startsWith("\\") ) path = path[1 .. $]; + if( path.empty ) return null; + if( path.endsWith("/") || path.endsWith("\\") ) path = path[0 .. $-1]; + + // count the number of path nodes + size_t nelements = 0; + foreach( i, char ch; path ) + if( ch == '\\' || ch == '/' ) + nelements++; + nelements++; + + // reserve space for the elements + auto elements = new PathEntry[nelements]; + + // read and return the elements + size_t startidx = 0; + size_t eidx = 0; + foreach( i, char ch; path ) + if( ch == '\\' || ch == '/' ){ + enforce(i - startidx > 0, "Empty path entries not allowed."); + elements[eidx++] = PathEntry(path[startidx .. i]); + startidx = i+1; + } + elements[eidx++] = PathEntry(path[startidx .. $]); + enforce(path.length - startidx > 0, "Empty path entries not allowed."); + assert(eidx == nelements); + return elements; +} diff --git a/source/vibecompat/inet/url.d b/source/vibecompat/inet/url.d new file mode 100644 index 0000000..66955c6 --- /dev/null +++ b/source/vibecompat/inet/url.d @@ -0,0 +1,277 @@ +/** + URL parsing routines. + + Copyright: © 2012 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 vibecompat.inet.url; + +public import vibecompat.inet.path; + +import std.algorithm; +import std.array; +import std.conv; +import std.exception; +import std.string; +import std.uri; + + +/** + Represents a URL decomposed into its components. +*/ +struct Url { + private { + string m_schema; + string m_pathString; + Path m_path; + string m_host; + ushort m_port; + string m_username; + string m_password; + string m_queryString; + string m_anchor; + } + + /// Constructs a new URL object from its components. + this(string schema, string host, ushort port, Path path) + { + m_schema = schema; + m_host = host; + m_port = port; + m_path = path; + m_pathString = path.toString(); + } + /// ditto + this(string schema, Path path) + { + this(schema, null, 0, path); + } + + /** Constructs a URL from its string representation. + + TODO: additional validation required (e.g. valid host and user names and port) + */ + this(string url_string) + { + auto str = url_string; + enforce(str.length > 0, "Empty URL."); + if( str[0] != '/' ){ + auto idx = str.countUntil(':'); + enforce(idx > 0, "No schema in URL:"~str); + m_schema = str[0 .. idx]; + str = str[idx+1 .. $]; + bool requires_host = false; + + switch(m_schema){ + case "http": + case "https": + case "ftp": + case "spdy": + case "sftp": + case "file": + // proto://server/path style + enforce(str.startsWith("//"), "URL must start with proto://..."); + requires_host = true; + str = str[2 .. $]; + goto default; + default: + auto si = str.countUntil('/'); + if( si < 0 ) si = str.length; + auto ai = str[0 .. si].countUntil('@'); + sizediff_t hs = 0; + if( ai >= 0 ){ + hs = ai+1; + auto ci = str[0 .. ai].countUntil(':'); + if( ci >= 0 ){ + m_username = str[0 .. ci]; + m_password = str[ci+1 .. ai]; + } else m_username = str[0 .. ai]; + enforce(m_username.length > 0, "Empty user name in URL."); + } + + m_host = str[hs .. si]; + auto pi = m_host.countUntil(':'); + if(pi > 0) { + enforce(pi < m_host.length-1, "Empty port in URL."); + m_port = to!ushort(m_host[pi+1..$]); + m_host = m_host[0 .. pi]; + } + + enforce(!requires_host || m_schema == "file" || m_host.length > 0, + "Empty server name in URL."); + str = str[si .. $]; + } + } + + this.localURI = str; + } + /// ditto + static Url parse(string url_string) + { + return Url(url_string); + } + + /// The schema/protocol part of the URL + @property string schema() const { return m_schema; } + /// ditto + @property void schema(string v) { m_schema = v; } + + /// The path part of the URL in the original string form + @property string pathString() const { return m_pathString; } + + /// The path part of the URL + @property Path path() const { return m_path; } + /// ditto + @property void path(Path p) + { + m_path = p; + auto pstr = p.toString(); + m_pathString = pstr; + } + + /// The host part of the URL (depends on the schema) + @property string host() const { return m_host; } + /// ditto + @property void host(string v) { m_host = v; } + + /// The port part of the URL (optional) + @property ushort port() const { return m_port; } + /// ditto + @property port(ushort v) { m_port = v; } + + /// The user name part of the URL (optional) + @property string username() const { return m_username; } + /// ditto + @property void username(string v) { m_username = v; } + + /// The password part of the URL (optional) + @property string password() const { return m_password; } + /// ditto + @property void password(string v) { m_password = v; } + + /// The query string part of the URL (optional) + @property string queryString() const { return m_queryString; } + /// ditto + @property void queryString(string v) { m_queryString = v; } + + /// The anchor part of the URL (optional) + @property string anchor() const { return m_anchor; } + + /// The path part plus query string and anchor + @property string localURI() + const { + auto str = appender!string(); + str.reserve(m_pathString.length + 2 + queryString.length + anchor.length); + str.put(encode(path.toString())); + if( queryString.length ) { + str.put("?"); + str.put(queryString); + } + if( anchor.length ) { + str.put("#"); + str.put(anchor); + } + return str.data; + } + /// ditto + @property void localURI(string str) + { + auto ai = str.countUntil('#'); + if( ai >= 0 ){ + m_anchor = str[ai+1 .. $]; + str = str[0 .. ai]; + } + + auto qi = str.countUntil('?'); + if( qi >= 0 ){ + m_queryString = str[qi+1 .. $]; + str = str[0 .. qi]; + } + + m_pathString = str; + m_path = Path(decode(str)); + } + + /// The URL to the parent path with query string and anchor stripped. + @property Url parentUrl() const { + Url ret; + ret.schema = schema; + ret.host = host; + ret.port = port; + ret.username = username; + ret.password = password; + ret.path = path.parentPath; + return ret; + } + + /// Converts this URL object to its string representation. + string toString() + const { + import std.format; + auto dst = appender!string(); + dst.put(schema); + dst.put(":"); + switch(schema){ + default: break; + case "file": + case "http": + case "https": + case "ftp": + case "spdy": + case "sftp": + dst.put("//"); + break; + } + dst.put(host); + if( m_port > 0 ) formattedWrite(dst, ":%d", m_port); + dst.put(localURI); + return dst.data; + } + + bool startsWith(const Url rhs) const { + if( m_schema != rhs.m_schema ) return false; + if( m_host != rhs.m_host ) return false; + // FIXME: also consider user, port, querystring, anchor etc + return path.startsWith(rhs.m_path); + } + + Url opBinary(string OP)(Path rhs) const if( OP == "~" ) { return Url(m_schema, m_host, m_port, m_path ~ rhs); } + Url opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return Url(m_schema, m_host, m_port, m_path ~ rhs); } + void opOpAssign(string OP)(Path rhs) if( OP == "~" ) { m_path ~= rhs; } + void opOpAssign(string OP)(PathEntry rhs) if( OP == "~" ) { m_path ~= rhs; } + + /// Tests two URLs for equality using '=='. + bool opEquals(ref const Url rhs) const { + if( m_schema != rhs.m_schema ) return false; + if( m_host != rhs.m_host ) return false; + if( m_path != rhs.m_path ) return false; + return true; + } + /// ditto + bool opEquals(const Url other) const { return opEquals(other); } + + int opCmp(ref const Url rhs) const { + if( m_schema != rhs.m_schema ) return m_schema.cmp(rhs.m_schema); + if( m_host != rhs.m_host ) return m_host.cmp(rhs.m_host); + if( m_path != rhs.m_path ) return m_path.opCmp(rhs.m_path); + return true; + } +} + +unittest { + auto url = Url.parse("https://www.example.net/index.html"); + assert(url.schema == "https", url.schema); + assert(url.host == "www.example.net", url.host); + assert(url.path == Path("/index.html"), url.path.toString()); + + url = Url.parse("http://jo.doe:password@sub.www.example.net:4711/sub2/index.html?query#anchor"); + assert(url.schema == "http", url.schema); + assert(url.username == "jo.doe", url.username); + assert(url.password == "password", url.password); + assert(url.port == 4711, to!string(url.port)); + assert(url.host == "sub.www.example.net", url.host); + assert(url.path.toString() == "/sub2/index.html", url.path.toString()); + assert(url.queryString == "query", url.queryString); + assert(url.anchor == "anchor", url.anchor); +} diff --git a/source/vibecompat/inet/urltransfer.d b/source/vibecompat/inet/urltransfer.d new file mode 100644 index 0000000..0daca29 --- /dev/null +++ b/source/vibecompat/inet/urltransfer.d @@ -0,0 +1,37 @@ +/** + Downloading and uploading of data from/to URLs. + + Copyright: © 2012 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 vibecompat.inet.urltransfer; + +import vibecompat.core.log; +import vibecompat.core.file; +import vibecompat.inet.url; + +import std.exception; +import std.net.curl; +import std.string; + + +/** + Downloads a file from the specified URL. + + Any redirects will be followed until the actual file resource is reached or if the redirection + limit of 10 is reached. Note that only HTTP(S) is currently supported. +*/ +void download(string url, string filename) +{ + auto conn = HTTP(); + static if( is(typeof(&conn.verifyPeer)) ) + conn.verifyPeer = false; + std.net.curl.download(url, filename, conn); +} + +/// ditto +void download(Url url, Path filename) +{ + download(url.toString(), filename.toNativeString()); +}