Files
lime/tools/haxelib/Data.hx
2017-05-18 11:20:53 -07:00

360 lines
9.6 KiB
Haxe

/*
* Copyright (C)2005-2016 Haxe Foundation
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
package haxelib;
import haxe.ds.Option;
import haxe.zip.Reader;
import haxe.zip.Entry;
import haxe.Json;
import haxelib.Validator;
using StringTools;
typedef UserInfos = {
var name : String;
var fullname : String;
var email : String;
var projects : Array<String>;
}
typedef VersionInfos = {
var date : String;
var name : SemVer;//TODO: this should eventually be called `number`
var downloads : Int;
var comments : String;
}
typedef ProjectInfos = {
var name : String;
var desc : String;
var website : String;
var owner : String;
var license : String;
var curversion : String;
var downloads : Int;
var versions : Array<VersionInfos>;
var tags : List<String>;
}
abstract DependencyVersion(String) to String from SemVer {
inline function new(s:String)
this = s;
@:to function toValidatable():Validatable
return
if (this == '') { validate: function () return None }
else @:privateAccess new SemVer(this);
static public function isValid(s:String)
return new DependencyVersion(s).toValidatable().validate() == None;
static public var DEFAULT(default, null) = new DependencyVersion('');
}
abstract Dependencies(Dynamic<DependencyVersion>) from Dynamic<DependencyVersion> {
@:to public function toArray():Array<Dependency> {
var fields = Reflect.fields(this);
fields.sort(Reflect.compare);
var result:Array<Dependency> = new Array<Dependency>();
for (f in fields) {
var value:String = Reflect.field(this, f);
var isGit = value != null && (value + "").startsWith("git:");
if ( !isGit )
{
result.push ({
name: f,
type: (DependencyType.Haxelib : DependencyType),
version: (cast value : DependencyVersion),
url: (null : String),
subDir: (null : String),
branch: (null : String),
});
}
else
{
value = value.substr(4);
var urlParts = value.split("#");
var url = urlParts[0];
var branch = urlParts.length > 1 ? urlParts[1] : null;
result.push ({
name: f,
type: (DependencyType.Git : DependencyType),
version: (DependencyVersion.DEFAULT : DependencyVersion),
url: (url : String),
subDir: (null : String),
branch: (branch : String),
});
}
}
return result;
}
public inline function iterator()
return toArray().iterator();
}
@:enum abstract DependencyType(String) {
var Haxelib = null;
var Git = 'git';
var Mercurial = 'hg';
}
typedef Dependency = {
name : String,
?version : DependencyVersion,
?type: DependencyType,
?url: String,
?subDir: String,
?branch: String,
}
typedef Infos = {
// IMPORTANT: if you change this or its fields types,
// make sure to update `schema.json` file accordingly,
// and make an update PR to https://github.com/SchemaStore/schemastore
var name : ProjectName;
@:optional var url : String;
@:optional var description : String;
var license : License;
var version : SemVer;
@:optional var classPath : String;
var releasenote : String;
@:requires('Specify at least one contributor' => _.length > 0)
var contributors : Array<String>;
@:optional var tags : Array<String>;
@:optional var dependencies : Dependencies;
@:optional var main:String;
}
@:enum abstract License(String) to String {
var Gpl = 'GPL';
var Lgpl = 'LGPL';
var Mit = 'MIT';
var Bsd = 'BSD';
var Public = 'Public';
var Apache = 'Apache';
}
abstract ProjectName(String) to String {
static var RESERVED_NAMES = ["haxe", "all"];
static var RESERVED_EXTENSIONS = ['.zip', '.hxml'];
inline function new(s:String)
this = s;
@:to function toValidatable():Validatable
return {
validate:
function ():Option<String> {
for (r in rules)
if (!r.check(this))
return Some(r.msg.replace('%VALUE', '`' + Json.stringify(this) + '`'));
return None;
}
}
static var rules = {//using an array because order might matter
var a = new Array<{ msg: String, check:String->Bool }>();
function add(m, r)
a.push( { msg: m, check: r } );
add("%VALUE is not a String", Std.is.bind(_, String));
add("%VALUE is too short", function (s) return s.length >= 3);
add("%VALUE contains invalid characters", Data.alphanum.match);
add("%VALUE is a reserved name", function(s) return RESERVED_NAMES.indexOf(s.toLowerCase()) == -1);
add("%VALUE ends with a reserved suffix", function(s) {
s = s.toLowerCase();
for (ext in RESERVED_EXTENSIONS)
if (s.endsWith(ext)) return false;
return true;
});
a;
}
public function validate()
return toValidatable().validate();
static public function ofString(s:String)
return switch new ProjectName(s) {
case _.toValidatable().validate() => Some(e): throw e;
case v: v;
}
static public var DEFAULT(default, null) = new ProjectName('unknown');
}
class Data {
public static var JSON(default, null) = "haxelib.json";
public static var DOCXML(default, null) = "haxedoc.xml";
public static var REPOSITORY(default, null) = "files/3.0";
public static var alphanum(default, null) = ~/^[A-Za-z0-9_.-]+$/;
public static function safe( name : String ) {
if( !alphanum.match(name) )
throw "Invalid parameter : "+name;
return name.split(".").join(",");
}
public static function unsafe( name : String ) {
return name.split(",").join(".");
}
public static function fileName( lib : String, ver : String ) {
return safe(lib)+"-"+safe(ver)+".zip";
}
static public function getLatest(info:ProjectInfos, ?preview:SemVer.Preview->Bool):Null<SemVer> {
if (info.versions.length == 0) return null;
if (preview == null)
preview = function (p) return p == null;
var versions = info.versions.copy();
versions.sort(function (a, b) return -SemVer.compare(a.name, b.name));
for (v in versions)
if (preview(v.name.preview)) return v.name;
return versions[0].name;
}
/**
Return the directory that contains *haxelib.json*.
If it is at the root, `""`.
If it is in a folder, the path including a trailing slash is returned.
*/
public static function locateBasePath( zip : List<Entry> ):String {
var f = getJson(zip);
return f.fileName.substr(0, f.fileName.length - JSON.length);
}
static function getJson(zip:List<Entry>)
return switch topmost(zip, fileNamed(JSON)) {
case Some(f): f;
default: throw 'No $JSON found';
}
static function fileNamed(name:String)
return function (f:Entry) return f.fileName.endsWith(name);
static function topmost(zip:List<Entry>, predicate:Entry->Bool):Option<Entry> {
var best:Entry = null,
bestDepth = 0xFFFF;
for (f in zip)
if (predicate(f)) {
var depth = f.fileName.replace('\\', '/').split('/').length;//TODO: consider Path.normalize
if ((depth == bestDepth && f.fileName < best.fileName) || depth < bestDepth) {
best = f;
bestDepth = depth;
}
}
return
if (best == null)
None;
else
Some(best);
}
public static function readDoc( zip : List<Entry> ) : Null<String>
return switch topmost(zip, fileNamed(DOCXML)) {
case Some(f): Reader.unzip(f).toString();
case None: null;
}
public static function readInfos( zip : List<Entry>, check : Bool ) : Infos
return readData(Reader.unzip(getJson(zip)).toString(), check);
public static function checkClassPath( zip : List<Entry>, infos : Infos ) {
if ( infos.classPath != "" ) {
var basePath = Data.locateBasePath(zip);
var cp = basePath + infos.classPath;
for( f in zip ) {
if( StringTools.startsWith(f.fileName,cp) )
return;
}
throw 'Class path `${infos.classPath}` not found';
}
}
public static function readData( jsondata: String, check : Bool ) : Infos {
var doc:Infos =
try Json.parse(jsondata)
catch ( e : Dynamic )
if (check)
throw 'JSON parse error: $e';
else {
name : ProjectName.DEFAULT,
url : '',
version : SemVer.DEFAULT,
releasenote: 'No haxelib.json found',
license: Mit,
description: 'No haxelib.json found',
contributors: [],
}
if (check)
Validator.validate(doc);
else {
if (!doc.version.valid)
doc.version = SemVer.DEFAULT;
}
//TODO: we have really weird ways to go about nullability and defaults
if (doc.dependencies == null)
doc.dependencies = {};
for (dep in doc.dependencies)
if (!DependencyVersion.isValid(dep.version))
Reflect.setField(doc.dependencies, dep.name, DependencyVersion.DEFAULT);//TODO: this is pure evil
if (doc.classPath == null)
doc.classPath = '';
if (doc.name.validate() != None)
doc.name = ProjectName.DEFAULT;
if (doc.description == null)
doc.description = '';
if (doc.tags == null)
doc.tags = [];
if (doc.url == null)
doc.url = '';
return doc;
}
}