Giter Site home page Giter Site logo

sm-json's Introduction

sm-json

Build Status Latest Release

This README covers documentation for v5.x. If you're looking for v4.x docs, please use the v4.x branch.

A pure SourcePawn JSON encoder/decoder. Also offers a nice way of implementing pseudo-classes with properties and methods.

Follows the JSON specification (RFC7159) almost perfectly. Singular values not contained within a structure (e.g. "string", 1, 0.1, true, false, null, etc.) are not supported.

Table of Contents

Requirements

  • SourceMod 1.10 or later

Installation

Using as a Git Submodule

If you use git while developing plugins, it is recommended to install this library as a git submodule. This makes it easy to lock to a specific major version or update as desired.

  1. Run git submodule add https://github.com/clugg/sm-json dependencies/sm-json in your repository.

  2. To lock to a specific branch, run git submodule set-branch -b YOUR_BRANCH dependencies/sm-json (e.g. git submodule set-branch -b v3.x dependencies/sm-json). To undo this/reset to the default branch, run git submodule set-branch -d dependencies/sm-json. In both cases an update needs to be run afterwards (see step 4).

  3. Whenever building plugins with spcomp, reference the library's include path using -idependencies/sm-json/addons/sourcemod/scripting (this path may differ depending on which directory spcomp is run from).

  4. To pull the latest from your selected branch, run git submodule update --remote dependencies/sm-json.

To uninstall the library, run git rm dependencies/sm-json.

Manually

Download the source code for the latest release and move all files and directories from the addons/sourcemod/scripting/include directory to your existing addons/sourcemod/scripting/include directory.

API Reference

A comprehensive API reference is available here. Certain internal methods which are not intended for outside use are not documented in this API and are subject to breaking changes within the same major version.

Usage

All of the following examples implicitly begin with the following code snippet.

// include the library
#include <json>

// this is where our encoding results will go
char output[1024];

Creating & Encoding

Arrays

JSON_Array arr = new JSON_Array();
arr.PushString("my string");
arr.PushInt(1234);
arr.PushFloat(13.37);
arr.PushBool(true);
arr.PushObject(null);
arr.PushObject(new JSON_Array());
arr.PushObject(new JSON_Object());

arr.Encode(output, sizeof(output));
// output now contains ["my string",1234,13.37,true,null,[],{}]
json_cleanup_and_delete(arr);

Objects

JSON_Object obj = new JSON_Object();
obj.SetString("strkey", "your string");
obj.SetInt("intkey", -1234);
obj.SetFloat("floatkey", -13.37);
obj.SetBool("boolkey", false);
obj.SetObject("nullkey", null);
obj.SetObject("array", new JSON_Array());
obj.SetObject("object", new JSON_Object());

obj.Encode(output, sizeof(output));
// output now contains {"strkey":"your string","intkey":-1234,"floatkey":-13.37,"boolkey":false,"nullkey":null,"array":[],"object":{}}
json_cleanup_and_delete(obj);

Note: This library will automatically keep track of the order in which keys are seen and respect this ordering when encoding output.

Options

Options which modify how the encoder works can be passed as the third parameter (or fourth in json_encode).

JSON_Array child_arr = new JSON_Array();
child_arr.PushInt(1);

JSON_Object child_obj = new JSON_Object();
child_obj.SetObject("im_indented", null);
child_obj.SetObject("second_depth", child_arr);

JSON_Object parent_obj = new JSON_Object();
parent_obj.SetBool("pretty_printing", true);
parent_obj.SetObject("first_depth", child_obj);

parent_obj.Encode(output, sizeof(output), JSON_ENCODE_PRETTY);
json_cleanup_and_delete(parent_obj);

output will contain the following:

{
    "pretty_printing": true,
    "first_depth": {
        "im_indented": null,
        "second_depth": [
            1
        ]
    }
}

Using the same parent object as last time (pretending we didn't just clean it up!):

strcopy(JSON_PP_AFTER_COLON, sizeof(JSON_PP_AFTER_COLON), " ");
strcopy(JSON_PP_INDENT, sizeof(JSON_PP_AFTER_COLON), "");
strcopy(JSON_PP_NEWLINE, sizeof(JSON_PP_NEWLINE), " ");

parent_obj.Encode(output, sizeof(output), JSON_ENCODE_PRETTY);

output will contain the following:

{ "pretty_printing": true, "first_depth": { "im_indented": null, "second_depth": [ 1, [] ] } }

Decoding

Arrays

JSON_Array arr = view_as<JSON_Array>(json_decode("[\"my string\",1234,13.37,true,null,[],{}]"));
char strval[32];
arr.GetString(0, strval, sizeof(strval));
int intval = arr.GetInt(1);
float floatval = arr.GetFloat(2);
bool boolval = arr.GetBool(3);
Handle nullval = arr.GetObject(4);
JSON_Array arrval = view_as<JSON_Array>(arr.GetObject(5));
JSON_Object objval = arr.GetObject(6);

json_cleanup_and_delete(arr);

Objects

JSON_Object obj = json_decode("{\"object\":{},\"floatkey\":-13.37,\"boolkey\":false,\"intkey\":-1234,\"array\":[],\"nullkey\":null,\"strkey\":\"your string\"}");
char strval[32];
obj.GetString("strkey", strval, sizeof(strval));
int intval = obj.GetInt("intkey");
float floatval = obj.GetFloat("floatkey");
bool boolval = obj.GetBool("boolkey");
Handle nullval = obj.GetObject("nullkey");
JSON_Array arrval = view_as<JSON_Array>(obj.GetObject("array"));
JSON_Object objval = obj.GetObject("object");

json_cleanup_and_delete(obj);

Options

Options which modify how the parser works can be passed as the second parameter (e.g. json_decode("[]", JSON_DECODE_SINGLE_QUOTES)).

  • JSON_DECODE_SINGLE_QUOTES: accepts 'single quote strings' as valid. A mixture of single and double quoted strings can be used in a structure (e.g. ['single', "double"]) as long as quotes are matched correctly. Note: encoded output will still use double quotes, and unescaping of single quotes in double quoted strings does not occur.

Iteration

Arrays

int length = arr.Length;
for (int i = 0; i < length; i += 1) {
    JSONCellType type = arr.GetType(i);
    // do whatever you want with the index and type information
}

Objects

int length = obj.Length;
int key_length = 0;
for (int i = 0; i < length; i += 1) {
    key_length = obj.GetKeySize(i);
    char[] key = new char[key_length];
    obj.GetKey(i, key, key_length);

    JSONCellType type = obj.GetType(key);
    // do whatever you want with the key and type information
}

Cleaning Up

Since this library uses StringMap under the hood, you need to make sure you manage your memory properly by cleaning up instances when you're done with them. Using the delete keyword is not sufficient with JSON instances due to their underlying structure. A helper function Cleanup() has been provided which recursively cleans up and deletes all nested instances before deleting the parent instance.

Additionally, there is a global helper function json_cleanup_and_delete() which will first call Cleanup(), then set the passed variable to null.

arr.Cleanup();
arr = null;
// or
json_cleanup_and_delete(arr);

obj.Cleanup();
obj = null;
// or
json_cleanup_and_delete(obj);

This may trip you up if you have multiple references to one shared instance, because cleaning up the first will invalidate the handle for the second. For example:

JSON_Array shared = new JSON_Array();

JSON_Object obj1 = new JSON_Object();
obj1.SetObject("shared", shared);

JSON_Object obj2 = new JSON_Object();
obj2.SetObject("shared", shared);

// this will clean up the nested "shared" array
json_cleanup_and_delete(obj1);

// this will throw an Invalid Handle exception because "shared" no longer exists
json_cleanup_and_delete(obj2);

You can avoid this by removing known shared instances from other instances before cleaning them up.

obj1.Remove("shared");
json_cleanup_and_delete(obj1);

obj2.Remove("shared");
json_cleanup_and_delete(obj2);

json_cleanup_and_delete(shared);

Pseudo-Classes

Creating & Encoding

methodmap Player < JSON_Object
{
    public bool SetAlias(const char[] value)
    {
        return this.SetString("alias", value);
    }

    public bool GetAlias(char[] buffer, int max_size)
    {
        return this.GetString("alias", buffer, max_size);
    }

    property int Score
    {
        public get()
        {
            return this.GetInt("score");
        }

        public set(int value)
        {
            this.SetInt("score", value);
        }
    }

    property float Height
    {
        public get()
        {
            return this.GetFloat("height");
        }

        public set(float value)
        {
            this.SetFloat("height", value);
        }
    }

    property bool Alive
    {
        public get()
        {
            return this.GetBool("alive");
        }

        public set(bool value)
        {
            this.SetBool("alive", value);
        }
    }

    property Handle Handle
    {
        public get()
        {
            return view_as<Handle>(this.GetObject("handle"));
        }

        public set(Handle value)
        {
            this.SetObject("handle", value);
        }
    }

    property JSON_Object Object
    {
        public get()
        {
            return this.GetObject("object");
        }

        public set(JSON_Object value)
        {
            this.SetObject("object", value);
        }
    }

    property JSON_Array Array
    {
        public get()
        {
            return view_as<JSON_Array>(this.GetObject("array"));
        }

        public set(JSON_Array value)
        {
            this.SetObject("array", value);
        }
    }

    public Player()
    {
        Player self = view_as<Player>(new JSON_Object());
        self.SetAlias("clug");
        self.Score = 9001;
        self.Height = 1.8;
        self.Alive = true;
        self.Handle = null;
        self.Object = new JSON_Object();
        self.Array = new JSON_Array();

        return self;
    }

    public void IncrementScore()
    {
        this.Score += 1;
    }
}

Player player = new Player();
player.Encode(output, sizeof(output));
// output now contains {"alias":"clug","score":9001,"height":1.8,"alive":true,"handle":null,"object":{},"array":[]}

You are also free to nest classes within one another (a continuation from the previous snippet).

methodmap Weapon < JSON_Object
{
    property Player Owner
    {
        public get()
        {
            return view_as<Player>(this.GetObject("owner"));
        }

        public set(Player value)
        {
            this.SetObject("owner", value);
        }
    }

    property int Id
    {
        public get()
        {
            return this.GetInt("id");
        }

        public set(int value)
        {
            this.SetInt("id", value);
        }
    }

    public Weapon()
    {
        Weapon self = view_as<Weapon>(new JSON_Object());
        self.Id = 1;
        self.Owner = new Player();

        return self;
    }
}

Weapon weapon = new Weapon();
weapon.Encode(output, sizeof(output));
// output now contains {"id":1,"owner":{"alias":"clug","score":9001,"height":1.8,"alive":true,"handle":null,"object":{},"array":[]}}

Decoding

You can take any JSON_Object or JSON_Array and coerce it to a custom class in order to access its properties and methods.

Weapon weapon = view_as<Weapon>(json_decode("{\"id\":1,\"owner\":{\"score\":9001,\"alive\":true,\"object\":{},\"handle\":null,\"height\":1.8,\"alias\":\"clug\",\"array\":[]}}"));
weapon.Owner.IncrementScore();
int score = weapon.Owner.Score; // 9002

Error Handling

Prior to v4.1, unrecoverable errors (usually during decoding) were logged using SourceMod's native LogError method. From v4.1 onwards, errors are stored in a buffer and the last error that was encountered can be fetched using json_get_last_error.

API

All of the following examples assume access to an existing JSON_Array and JSON_Object instance.

JSON_Array arr = new JSON_Array();
JSON_Object obj = new JSON_Object();

In every case where a method denotes that it accepts a key/index, it means the following:

  • JSON_Object methods will accept a const char[] key
  • JSON_Array methods will accept an int index

Getters & Setters

JSON_Array and JSON_Object contain the following getters. These getters also accept a second parameter specifying a default value to return if the key/index was not found. Sensible default values have been set and are listed below.

  • obj/arr.GetString(key/index, buffer, max_size), which will place the string in the buffer provided and return true, or false if it fails.
  • obj/arr.GetInt(key/index), which will return the value or -1 if it was not found.
  • obj/arr.GetFloat(key/index), which will return the value or -1.0 if it was not found.
  • obj/arr.GetBool(key/index), which will return the value or false if it was not found.
  • obj/arr.GetObject(key/index), which will return the value or null if it was not found. You should typecast objects to arrays if you know the contents to be an array: view_as<JSON_Array>(obj.GetObject("array")).

JSON_Array and JSON_Object contain the following setters. These methods will return true if setting was successful, or false otherwise.

  • obj/arr.SetString(key/index, value)
  • obj/arr.SetInt(key/index, value)
  • obj/arr.SetFloat(key/index, value)
  • obj/arr.SetBool(key/index, value)
  • obj/arr.SetObject(key/index, value): value can be a JSON_Array, a JSON_Object or null

JSON_Array also contains push methods, which will push a value to the end of the array and return its index, or -1 if pushing failed.

  • arr.PushString(value)
  • arr.PushInt(value)
  • arr.PushFloat(value)
  • arr.PushBool(value)
  • arr.PushObject(value): value can be a JSON_Array, a JSON_Object or null

Metadata

  • obj/arr.HasKey(key/index): returns true if the key exists, false otherwise.
  • obj/arr.GetType(key/index): returns the JSONCellType stored at the key.
  • obj/arr.GetSize(key/index): if the key contains a string, returns the buffer size required for the string. Example:
int len = arr.GetSize(0);
char[] val = new char[len];
arr.GetString(0, val, len);

It is possible to mark a key as 'hidden' so that it does not appear in encoder output. WARNING: When calling Clear() or Remove(), relevant hidden flags will also be removed.

  • obj/arr.SetHidden(key/index, true/false): sets the specified key to be hidden (or not hidden).
  • obj/arr.GetHidden(key/index): returns whether or not the key is hidden. Example:
obj.SetHidden("secret_key", true);
obj.SetString("secret_key", "secret_value");
obj.SetString("public_key", "public_value");
obj.Encode(output, sizeof(output));
// output now contains {"public_key":"public_value"}

Renaming Elements

obj.Rename("fromKey", "toKey"): returns true if the rename is successful, false otherwise.

Renames an existing key in an object. Takes an optional third paramater replace (default true) which, when false, will prevent the rename if the to key already exists.

This method maintains the existing element's metadata (e.g. whether or not it is hidden).

Removing Elements

obj/arr.Remove(key/index)

Removing an element will also remove all metadata associated with it (i.e. type, string length and hidden flag). When removing from an array, all following elements will be shifted down an index to ensure that all indexes fall within [0, arr.Length) and that there are no gaps in the array.

Array Helpers

There are a few functions which make working with JSON_Arrays a bit nicer.

  • arr.IndexOf(value): returns the index of the value in the array if it is found, -1 otherwise.
  • arr.IndexOfString(value): as above, but works exclusively with strings.
  • arr.Contains(value): returns true if the value is found in the array, false otherwise.
  • arr.ContainsString(value): as above, but works exclusively with strings.

Please note that due to how the any type works in SourcePawn, Contains may return false positives for values that are stored the same in memory. For example, 0, null and false are all stored as 0 in memory and 1 and true are both stored as 1 in memory. Because of this, view_as<JSON_Array>(json_decode("[0]")).Contains(null) will return true, and so on. You may use Contains in conjunction with GetType( to typecheck the returned index and ensure it matches what you expected.

Array Type Enforcement

It is possible to enforce an array to only accept a single type. You can either do this when first creating the array, or later on.

JSON_Array ints = new JSON_Array(JSON_Type_Int);
ints.PushObject(null); // fails and returns -1
ints.PushInt(1); // returns 0
json_cleanup_and_delete(ints);

JSON_Array values = new JSON_Array();
values.PushObject(null);
values.PushInt(1);
values.EnforceType(JSON_Type_Int); // fails and returns false, array doesn't only contain ints
values.Remove(0);
values.EnforceType(JSON_Type_Int); // returns true
json_cleanup_and_delete(values);

Array Importing

It is possible to import any native array of values into a JSON_Array. The following code snippet works for every native type except char[]s.

int ints[] = {1, 2, 3};
JSON_Array arr = new JSON_Array();
arr.ImportValues(JSON_Type_Int, ints, sizeof(ints));

arr.Encode(output, sizeof(output)); // output now contains [1,2,3]
json_cleanup_and_delete(arr);

For strings, you need to use a separate function.

char strings[][] = {"hello", "world"};
JSON_Array arr = new JSON_Array();
arr.ImportStrings(strings, sizeof(strings));

arr.Encode(output, sizeof(output)); // output now contains [\"hello\",\"world\"]
json_cleanup_and_delete(arr);

Array Exporting

It is possible to export a JSON_Array's values to a native array. The following code snippet works for every native type except char[]s. Note: there is no type checking done during export - it is entirely up to you to ensure that your array only contains the type that you expect (see Array Type Enforcement).

JSON_Array arr = view_as<JSON_Array>(json_decode("[1,2,3]"));
int size = arr.Length;
int[] values = new int[size];
arr.ExportValues(values, size);
json_cleanup_and_delete(arr);
// values now contains {1, 2, 3}

For strings, you need to use a separate function.

JSON_Array arr = view_as<JSON_Array>(json_decode("[\"hello\",\"world\"]"));
int size = arr.Length;
int str_length = arr.MaxStringLength;
char[][] values = new char[size][str_length];
arr.ExportStrings(values, size, str_length);
json_cleanup_and_delete(arr);
// values now contains {"hello", "world"}

Object Merging

JSON_Objects can be merged with one another.

Merging is shallow, which means that if the second object has child objects, the reference will be maintained to the existing object when merged, as opposed to copying the children.

Merged keys will respect their previous hidden state when merged on to the first object.

Options

  • JSON_MERGE_REPLACE: active by default. Tells the merger to replace any existing keys on the first object with the values from the second. For example, if you have two objects both containing key x, with replacement on, the value of x will be taken from the second object, and with replacement off, from the first object. You can explicitly disable this by passing JSON_NONE as an option.
  • JSON_MERGE_CLEANUP: tells merge to clean up any nested instances before they are replaced. Since this only has an effect while replacement is enabled, you will need to pass JSON_MERGE_REPLACE | JSON_MERGE_CLEANUP as options.
JSON_Object obj1 = new JSON_Object();
obj1.SetInt("x", 1);
obj2.SetInt("y", 2);

JSON_Object obj2 = new JSON_Object();
obj2.SetInt("y", 3);
obj2.SetInt("z", 4)

obj1.Merge(obj2); // obj1 is now equivocally {"x":1,"y":3,"z":4}, obj2 remains unchanged
// alternatively, without replacement
obj1.Merge(obj2, JSON_NONE); // obj1 is now equivocally {"x":1,"y":2,"z":4}, obj2 remains unchanged

Array Concatenation

JSON_Arrays can be concatenated to one another.

Concatenation is shallow, which means that if the second array has child objects, the reference will be maintained to the existing object when merged, as opposed to copying the children. If you wish, you can do a DeepCopy on the source array before concatenating it.

Concatenated elements will respect their previous hidden state when pushed to the target array.

JSON_Array target = new JSON_Array();
target.PushInt(1);
target.PushInt(2);
target.PushInt(3);

JSON_Array source = new JSON_Array();
source.PushInt(4);
source.PushInt(5);
source.PushInt(6);

target.Concat(source); // target is now equivocally [1,2,3,4,5,6], source remains unchanged

Copying

Shallow

A shallow copy will maintain the original reference to nested instances within the instance.

arr.PushInt(1);
arr.PushInt(2);
arr.PushInt(3);
arr.PushObject(new JSON_Array());
// arr is now equivocally [1,2,3,[]]

JSON_Array copied = arr.ShallowCopy();
JSON_Array nested = view_as<JSON_Array>(copied.GetObject(3));
nested.PushInt(4);
copied.PushInt(5);
// copied is now equivocally [1,2,3,[4],5] and arr is now equivocally [1,2,3,[4]]
obj.SetString("hello", "world");
obj.SetObject("nested", new JSON_Object());
// obj is now equivocally {"hello":"world","nested":{}}

JSON_Object copied = obj.ShallowCopy();
JSON_Object nested = copied.GetObject("nested");
nested.SetString("key", "value");
copied.SetInt("test", 1);
// copied is now equivocally {"hello":"world","nested":{"key":"value"},"test":1} and obj is now equivocally {"hello":"world","nested":{"key":"value"}}

Deep

A deep copy will recursively copy all nested instances, yielding an entirely unrelated structure with all of the same values.

JSON_Array copied = arr.DeepCopy();
JSON_Array nested = view_as<JSON_Array>(copied.GetObject(3));
nested.PushInt(4);
copied.PushInt(5);
// copied is now equivocally [1,2,3,[4],5] but arr does not change
JSON_Object copied = obj.DeepCopy();
JSON_Object nested = copied.GetObject("nested");
nested.SetString("key", "value");
copied.SetInt("test", 1);
// copied is now equivocally {"hello":"world","nested":{"key":"value"},"test":1} but obj does not change

Working with Unknowns

In some cases, you may receive JSON which you do not know the structure of. It may contain an object or an array. This is possible to handle using the IsArray property, although it can result in some messy code.

JSON_Object obj = json_decode(SOME_UNKNOWN_JSON);
JSON_Array arr = view_as<JSON_Array>(obj);

if (obj.IsArray) {
    arr.PushString("ok");
} else {
    obj.SetString("result", "ok");
}

Global Helper Functions

A few of the examples in this documentation use object-oriented syntax, while in reality, they are wrappers for global functions. A complete list of examples can be found below.

obj/arr.Encode(output, sizeof(output) /*, options */);
// or
json_encode(obj/arr, output, sizeof(output) /*, options */);

obj/arr.ShallowCopy();
// or
json_copy_shallow(obj/arr);

obj/arr.DeepCopy();
// or
json_copy_deep(obj/arr);

obj/arr.Cleanup();
// or
json_cleanup(obj/arr);

If you prefer this style you may wish to use it instead.

Testing

A number of common tests have been written here. These tests include library-specific tests (which can be considered examples of how the library can be used) as well as every relevant test from the json.org test suite.

The test plugin uses the sm-testsuite library, which is included as a submodule to this repository. If you wish to run the tests yourself, follow these steps:

  1. run git submodule update --init on your command line inside the sm-json directory
  2. compile the plugin using spcomp json_test.sp -O2 -t4 -v2 -w234 -i../../../dependencies/sm-testsuite/addons/sourcemod/scripting/include
  3. place the plugin in your sourcemod installation
  4. run srcds if it's not already running
  5. sm plugins load json_test (or reload if already loaded)
  6. take note of output and ensure that all tests pass

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

Please ensure that all tests pass before making a pull request. A description of how to compile the test plugin can be seen in the testing section.

If you are fixing a bug, please add a regression test to ensure that the bug does not sneak back in. If you are adding a feature, please add tests to ensure that it works as expected.

License

GNU General Public License v3.0

sm-json's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

sm-json's Issues

json_is_meta_key does not exist on v4

Hello

This is not really a bug per se, but I don't know where else to ask. I upgraded Get5 to run on v4, and it won't compile because json_is_meta_key no longer exists. The changelog does not explain what this feature was replaced with, nor do I know what it actually did.

Can you perhaps explain if we can just ignore this all going forward?

Some Additions and Improvements

Would be good to have new helper methods for JSON_Array like JSON_Array.Swap, JSON_Array.ShiftUp and maybe JSON_Array.Sort (only by integers, floats etc.)

EDIT: Also could you add setters for JSON_Array so we'll be able to replace values in specific index?
EDIT 2: Also i think JSON_Array.Push* should return a new index instead of bool (any positive (or null) value instead of true and -1 instead of false)

Prettify / Pretty printing

Hi,

first of all great job. I truly appreciate the work you have done!

I would like to request that json_encode includes a "prettify" parameter for pretty printing. Can be quite useful for debugging purposes.

[Feature Req] Properly handle duplicate keys when decoding

Is your feature request related to a problem? Please describe.
Not really.

Describe the solution you'd like
Instead of simply returning null when an attempt to decode fails, it would be cool if you could pass in a char[] which the function would populate with where the problem occurred (potentially on which line if it's pretty-printed JSON) and what happened, i.e. expected }, found ] on line 44.

Describe alternatives you've considered
n/a

Additional context
Users of get5 will sometimes - for reasons I don't understand, perhaps because they type in JSON manually - make mistakes in their files and not validate them, then ask us/me why their file doesn't load. If this library would tell them what kind of error they have made, it would help with debugging.

Also, while we're on JSON decoding, sm-json gets very confused when it decodes objects with duplicate keys, which is stupid, yes, but technically not invalid JSON - the last read key should determine the value of that key, and any others should be ignored/discarded. This caused one consumer of Get5 to have some very strange bugs because sm-json got the object wrong when asked to preserve ordered keys. I would propose it either return null or handle it correctly in that case.

[Feature Req] Error Buffer

Would be good to have the ability to get last error instead of just logging it to the errors log.

Example code:

public void OnPluginStart() {
    RegAdminCmd( "sm_mycmd", CMD_MyCommand, ADMFLAG_ROOT );
}

Action CMD_MyCommand(int client, int args) {
    char szBuffer[512];
    GetCmdArgString( szBuffer, sizeof szBuffer );

    JSON_Object hObject = json_decode( szBuffer, JSON_DECODE_SINGLE_QUOTES );

    if ( !hObject ) {
        json_get_last_error( szBuffer, sizeof szBuffer );
        ReplyToCommand( client, "Invalid JSON! Error: %s", szBuffer );
        LogError( "%s", szBuffer ); // you could log the error manually if you really want
        return Plugin_Handled;
    }

    // do something with hObject

    return Plugin_Handled;
}

The error buffer could be a static string in the json.inc library.

static char g_szLastError[512];

stock void json_get_last_error(char[] buffer, int maxlength) {
    strcopy( buffer, maxlength, g_szLastError );
}

// "internal" function to use in the array.inc, object.inc etc.
// this could be used instead of LogError (for example in json_decode())
stock void json_set_last_error(const char[] error) {
    strcopy( g_szLastError, sizeof g_szLastError, error );
}

[Bug] Ordered keys are not preserved on `DeepCopy()`

Description

If you create a JSON object using:

JSON_Object json = json_read_from_file("file.json", JSON_DECODE_ORDERED_KEYS);

the keys are ordered as expected. If you then do:

JSON_Object jsonCopy = json.DeepCopy();

the keys are now out-of-order.

I also tried calling json.EnableOrderedKeys() before and after copying, but it didn't help.

Reproduction

test_ordered_keys.json:

{
  "players": {
    "76561198034202275": "s1mple",
    "76561198246607476": "b1t",
    "76561198121220486": "Perfecto",
    "76561198044045107": "electronic",
    "76561198040577200": "sdy"
  }
}
char output[4096];
JSON_Object json = json_read_from_file("test_ordered_keys.json", JSON_DECODE_ORDERED_KEYS);
json.EnableOrderedKeys(); // This makes no difference.
JSON_Object jsonCopy = json.DeepCopy();
jsonCopy.EnableOrderedKeys(); // This also makes no difference.

json.Encode(output, sizeof(output), JSON_ENCODE_PRETTY);
LogMessage("Original: %s", output);
jsonCopy.Encode(output, sizeof(output), JSON_ENCODE_PRETTY);
LogMessage("DeepCopy: %s", output);

delete json;
delete jsonCopy;
Original: {
    "players": {
        "76561198034202275": "s1mple",
        "76561198246607476": "b1t",
        "76561198121220486": "Perfecto",
        "76561198044045107": "electronic",
        "76561198040577200": "sdy"
    }
}
DeepCopy: {
    "players": {
        "76561198246607476": "b1t",
        "76561198044045107": "electronic",
        "76561198040577200": "sdy",
        "76561198121220486": "Perfecto",
        "76561198034202275": "s1mple"
    }
}

Versions

sm-json: v4.1.1
SourceMod Compiler: 1.10
SourceMod Build: Not sure. Building with Get5's Docker.

[Bug] Error using json_cleanup_and_delete

Description

  • There is an error handling json that contains certain characters.
  • The error is caused when json_cleanup_and_delete is executed.

[SM] Exception reported: Invalid Handle a8500748 (error 3)
[SM] Blaming: matchmaking/match.smx
[SM] Call stack trace:
[SM] [0] StringMap.GetValue
[SM] [1] Line 98, x:...\scripting\include\json\helpers\typedstringmap.inc::TypedStringMap.GetOptionalValue
[SM] [2] Line 136, x:...\scripting\include\json\helpers\typedstringmap.inc::TypedStringMap.GetBool
[SM] [3] Line 77, x:...\scripting\include\json\object.inc::JSON_Object.OrderedKeys.get
[SM] [4] Line 109, x:...\scripting\include\json\object.inc::JSON_Object.Iterate
[SM] [5] Line 770, x:...\scripting\include\json.inc::json_cleanup
[SM] [6] Line 785, x:...\scripting\include\json.inc::json_cleanup
[SM] [7] Line 785, x:...\scripting\include\json.inc::json_cleanup
[SM] [8] Line 800, x:...\scripting\include\json.inc::json_cleanup_and_delete
[SM] [9] Line 203, x:...\scripting\forsaken.sp::PlayersTest
**

Reproduction

#include <json>

char
	g_sSteamIDTA[MAX_PLAYER_TEAM][STEAMID_LENGTH],
	g_sSteamIDTB[MAX_PLAYER_TEAM][STEAMID_LENGTH],
	g_sNameTA[MAX_PLAYER_TEAM][NAME_LENGTH],
	g_sNameTB[MAX_PLAYER_TEAM][NAME_LENGTH];

public Plugin myinfo =
{
	name		= "match",
	author		= "lechuga",
	description     = "error example",
	version		= "-",
	url		= "-"

}

public void OnPluginStart()
{
	RegConsoleCmd("sm_playersinfo", PlayersTest);
}

public Action PlayersTest(int iClient, int iArgs)
{
	char 
		sPatch[64];
	
	JSON_Object
		joMatch;
	
	JSON_Array
		jaTA,
		jaTB;

	BuildPath(Path_SM, sPatch, sizeof(sPatch), "logs/match.json");
	joMatch	= json_read_from_file(sPatch);

	jaTA = view_as<JSON_Array>(joMatch.GetObject("team1"));
	jaTB = view_as<JSON_Array>(joMatch.GetObject("team2"));

	if(joMatch == null)
	{
		ReplyToCommand(iClient, "Error: JSON_Object joMatch == null");
		return Plugin_Handled;
	}

	for (int i = 0; i <= 3; i++)
	{
		JSON_Object joPlayerTA = jaTA.GetObject(i);
		JSON_Object joPlayerTB = jaTB.GetObject(i);

		joPlayerTA.GetString("steamid", g_sSteamIDTA[i], STEAMID_LENGTH);
		joPlayerTB.GetString("steamid", g_sSteamIDTB[i], STEAMID_LENGTH);

		ReplaceString(g_sSteamIDTA[i], STEAMID_LENGTH, "STEAM_0", "STEAM_1", false);
		ReplaceString(g_sSteamIDTB[i], STEAMID_LENGTH, "STEAM_0", "STEAM_1", false);

		joPlayerTA.GetString("personaname", g_sNameTA[i], NAME_LENGTH);
		joPlayerTB.GetString("personaname", g_sNameTB[i], NAME_LENGTH);

		json_cleanup_and_delete(joPlayerTA);
		json_cleanup_and_delete(joPlayerTB);
	}

	ReplyToCommand(iClient, "TeamA Name: %s %s %s %s", g_sNameTA[0], g_sNameTA[1], g_sNameTA[2], g_sNameTA[3]);
	ReplyToCommand(iClient, "TeamA Steamid: %s %s %s %s", g_sSteamIDTA[0], g_sSteamIDTA[1], g_sSteamIDTA[2], g_sSteamIDTA[3]);

	ReplyToCommand(iClient, "TeamB Name: %s %s %s %s", g_sNameTB[0], g_sNameTB[1], g_sNameTB[2], g_sNameTB[3]);
	ReplyToCommand(iClient, "TeamB Steamid: %s %s %s %s", g_sSteamIDTB[0], g_sSteamIDTB[1], g_sSteamIDTB[2], g_sSteamIDTB[3]);

	json_cleanup_and_delete(joMatch);
        json_cleanup_and_delete(jaTA);
        json_cleanup_and_delete(jaTB);
	return Plugin_Handled;
}

Versions

sm-json: v4.11

SourceMod Compiler:
sourcemod-1.12.0-git6925

SourceMod Build:
SourceMod Version Information:
SourceMod Version: 1.11.0.6825
SourcePawn Engine: 1.11.0.6825, jit-x86 (build 1.11.0.6825)
SourcePawn API: v1 = 5, v2 = 16
Compiled on: Nov 23 2021 12:17:22
Built from: alliedmodders/sourcemod@4e3df76
Build ID: 6825:4e3df76
http://www.sourcemod.net/

Additional Information

  • I am using system2 to download a json file and then extract the data, I will leave a copy of the file.
  • This file is downloaded in ""logs/match.json".
  • "match.json" = {"team1":[{"steamid":"STEAM_0:0:695734257","personaname":"Zitro."},{"steamid":"STEAM_0:0:714227597","personaname":"Seynth-"},{"steamid":"STEAM_0:1:483378842","personaname":"Tet.off"},{"steamid":"STEAM_0:0:566567350","personaname":"Aleister"}],"team2":[{"steamid":"STEAM_0:1:461199623","personaname":"3lver"},{"steamid":"STEAM_0:0:620959933","personaname":"ewo"},{"steamid":"STEAM_0:0:530608861","personaname":"sattan"},{"steamid":"STEAM_0:1:566593219","personaname":"ꜱᴇɴᴅᴇʀᴏ"}],"region":1}

Default Values

Would be good to implement default values to most getter functions like JSON_Object.GetInt (GetBool and etc.)

Code Snippet №1 (this might be better than 2nd):

methodmap JSON_Object < StringMap {
	public int GetInt(const char[] key, int defValue=-1) {
		int iValue;
		if ( !this.GetValue( key, iValue ) )
			iValue = defValue;

		return iValue;
	}
}

Code Snippet №2:

methodmap JSON_Object < StringMap {
	public int GetInt(const char[] key, int defValue=-1) {
		int iValue = defValue;
		this.GetValue( key, iValue ); // if key doesn't exist, iValue won't be overwritten.
		return iValue;
	}
}

So there won't be need to check for key existence.

[Bug] json encoding utf-8 fails

Description

Encoding the following string produces a bad json/ascii-encoded utf-8 string.

JSON_Object player = new JSON_Object();
player.SetString("name", "Отключился.")
char output[2048];
obj.Encode(output, sizeof(output));
// output: { "name" : "\u041e\u0442\u043a\u043b\u044e\u04383b\u044 } (note the lack of parentheses)

Exception on recursive JSON decoding

Hello,

I'm having issues decoding a JSON that contains nested objects or arrays inside. Frustated with not knowing what the issue is, I copy-pasted the example in the README but fails too, so seems to be a library-issue.

public void onEvent(const char[] json) {
    // function is empty. just testing this
    // copied from README.md
    JSON_Object obj = json_decode("{\"myfloat\":73.570000,\"myobject\":[],\"myhandle\":null,\"mybool\":false,\"name\":\"my class\",\"myint\":9001}");
}

gives the following error on the console

Exception reported: Not enough space on the stack
[SM] Blaming: telemetry.smx
[SM] Call stack trace:
[SM] [1] Line 244, sourcemod\scripting\include\json.inc::json_decode
[SM] [2] Line 304, sourcemod\scripting\include\json.inc::json_decode

Thanks for your time.

[Feature Req] Dump to file function

Is your feature request related to a problem? Please describe.
I'm frustrated, to know I can't dump to a file the JSON, to check if it's complete or valid.

Describe the solution you'd like
Make a function what dump the json content to file (in SM-Jansson json_dump_file function).
I know this library is made with StringMap, and SM-Jansson is a extension.

Describe alternatives you've considered
I don't think there are any alternatives.

Additional context

/**
 * Write the JSON representation of hObject to the file sFilePath.
 * If sFilePath already exists, it is overwritten.
 *
 * @param hObject           String containing valid JSON
 * @param sFilePath         Buffer to store the created JSON string.
 * @param iIndentWidth      Indenting with iIndentWidth spaces.
 *                          The valid range for this is between 0 and 31 (inclusive),
 *                          other values result in an undefined output. If this is set
 *                          to 0, no newlines are inserted between array and object items.
 * @param bEnsureAscii      If this is set, the output is guaranteed
 *                          to consist only of ASCII characters. This is achieved
 *                          by escaping all Unicode characters outside the ASCII range.
 * @param bSortKeys         If this flag is used, all the objects in output are sorted
 *                          by key. This is useful e.g. if two JSON texts are diffed
 *                          or visually compared.
 * @param bPreserveOrder    If this flag is used, object keys in the output are sorted
 *                          into the same order in which they were first inserted to
 *                          the object. For example, decoding a JSON text and then
 *                          encoding with this flag preserves the order of object keys.
 * @return                  Length of the returned string or -1 on error.
 */

native bool:json_dump_file(Handle:hObject, const String:sFilePath[], iIndentWidth = 4, bool:bEnsureAscii = false, bool:bSortKeys = false, bool:bPreserveOrder = false);
json_dump_file(hJson, "json.txt", 0, true);
That is the function from Jansson.

[Bug] Snapshots not deleted

Hello

I'm fairly certain that a delete snap; call is missing for the Snapshots created here:

snap = from.Snapshot();
and here
StringMapSnapshot snap = result.Snapshot();

It is being done here, so I'm assuming it's an oversight:

if (snap != null) {
delete snap;
}

I'm sorry if this is wrong, but according to the documentation a Snapshot must be manually deleted, and I don't see how this can possibly be happening with the current code.

Thanks for a nice project though.

Single Quotes

I know this is probably a stupid idea but maybe you could add single quotes support for keys and string values like:

{'testKey': 'testValue'}

I want to add JSON-format objects via chat or client console but that's not working due to double quotes conflict.

[Suggestion] Some new useful functions

Would be good to implement some new helper functions like json_merge and json_copy (without need to use json_encode and json_decode).

json_merge would merge two given JSON_Objects into single object.
json_copy would copy keys from first object to second if second object doesn't have these keys.

Idk if this is a good idea/suggestion.

Some optimization

You should cache values for loop condition.

For example:

    public int IndexOf(any value)
    {
        any current;
        for (int i = 0; i < this.Length; i += 1) {
            if (this.GetValue(i, current) && value == current) {
                return i;
            }
        }

        return -1;
    }

    public int IndexOfString(const char[] value)
    {
        for (int i = 0; i < this.Length; i += 1) {
            if (this.GetKeyType(i) != JSON_Type_String) {
                continue;
            }

            int current_size = this.GetKeyLength(i) + 1;
            char[] current = new char[current_size];
            this.GetString(i, current, current_size);
            if (StrEqual(value, current)) {
                return i;
            }
        }

        return -1;
    }

for (int i = 0; i < this.Length; i += 1) {
should be replaced with

        int iLength = this.Length;
        for (int i = 0; i < iLength; i += 1) {

Stack allocation is cheap afaik so it would be better to store values that are often compared in loops.

[Bug] JSON objects passed to forwards then iterated on causes memory problems

Description

If you have two plugins where one creates an object and forwards it to another, say:

Plugin A:

public void OnPluginStart()
{
  gForward = new GlobalForward("FORWARD_NAME", ET_Ignore, Param_Cell);
  
  JSON_Object obj = new JSON_Object();
  Call_StartForward(gForward);
  Call_PushCell(obj);
  Call_Finish();
  
  json_cleanup_and_delete(obj);
}

Consumed by another plugin like this:

Plugin B:

public void FORWARD_NAME(const JSON_Object obj) {
  obj.Iterate(); // Do whatever, just call Iterate(), either directly
  // or via DeepCopy() or ShallowCopy():
  JSON_Object copy = obj.DeepCopy();
  // Do something, maybe async (SQL for instance), with copy
  json_cleanup_and_delete(copy);
}

The fact that Iterate() calls delete on the cached snapshot that belongs to obj (this silently fails) and creates a new one (which will fail when Plugin A calls delete on it) causes the consuming plugin to leak the snapshot it creates every time this method is called. From the current code:

public int Iterate()
{
    if (! this.OrderedKeys) {
        if (this.Snap != null) {
            delete this.Snap; // This will attempt to delete the handle created by Plugin A,
            // if one exists - say if you had iterated on the object before passing it to the forward.
            this.Snap = null;
        }

        this.Snap = this.Snapshot(); // And this will create a new handle on Plugin A's object, which belongs to Plugin B.
        // Plugin A cannot clean this up in its `Cleanup`, and it will linger in Plugin B.
    }

    return this.Length;
}

Versions

sm-json: v4.x
SourceMod Compiler: 1.11
SourceMod Build: Don't remember

Additional Information

I've been working on trying to solve this all day, with the help of @KillStr3aK on the SM Discord. I don't have a solution to this problem as long as Iterate is being used like it is. If the snapshot was returned and it was the callers responsibility to clean it up, like when normally iterating a string map, it would be solvable.

An issue was just opened on the AlliedModders GitHub (alliedmodders/sourcemod#1957), but that is more generic and about the lack of an exception when accessing handles you shouldn't. If there is no solution from SourceMod's side, then sm-json will likely need to take a different approach to iterating objects.

compile problem

Hello,

I'm using sm-json to doing compile, and using json for plugins.

I got this error, and I'm stuck over, what I need to changes:

cannot find method or property JSON_Object.GetIndexString
number of arguments does not match definition

What should I replace "JSON_Object.GetIndexString" with?

Example code:

array.GetIndexString(keyAsString, sizeof(keyAsString), i);
data.GetIndexString(keyAsString, sizeof(keyAsString), i);

[Bug] Compilation error with global defined array

Description

A clear and concise description of what the bug is.
Error on compilation.
In case if you try to define JSON_Array as global in plugin, you will get error if you pushObject inside of it.

Reproduction

#include <json>
JSON_Array argr = new JSON_Array();

public void OnPluginStart()
{
argr.PushObject(null);
}
Result:
error 008 | must be a constant expression; assumed zero

Versions

sm-json: v3.0
SourceMod Compiler: SPEDIT
SourceMod Build: ver 1.8

json_decode: expected key string at position 1

So, I'm trying to decode a josn HTTP Response retrieved using SteamWorks and I get this message: json_decode: expected key string at position 1.

I saw the include files and saw that it was expecting a double quotes at the position 1... I don't see what I'm doing wrong here... Any help?

The json I'm trying to decode with some information hidden:

{'name': 'MY TEST', 'mp_maxrounds': 30, 'bo': 1, 'maps': ['aim_glockon', 'aim_map', 'aim_map2_go', 'aim_redline', 'awp_bridge', 'awp_map_classic', 'fy_poolparty_go', 'scoutknifez'
    ], 'maxplayers': 4, 'tournament_id': 'test', 'bracket': {'id': 'test', 'participants': {'test': {'name': 'test', 'players': [
                    {'name': 'test', 'steamId64': 'test', 'captain': False
                    },
                    {'name': 'test', 'steamId64': 'test', 'captain': True
                    }
                ]
            }
        }, 'server_config': {'mp_overtime_enable': 2, 'mp_overtime_maxrounds': 6, 'tv_delay': 5, 'bot_quota': 0, 'mp_friendlyfire': 1, 'tv_enable': 0, 'mp_freezetime': 15, 'mp_halftime_duration': 30, 'sv_deadtalk': 0, 'sv_alltalk': 0, 'sv_prime_accounts_only': 0
        }
    }
}

Code:

JSON_Object json = new JSON_Object();
json.Decode(body);

[Feature Req] Sort order of keys of the second level depth

Hello! Thanks for the great library.

Is there any sort order?

This code gives a randomly sorted values on the second level object (paramsJson):

JSON_Object outputJson = new JSON_Object();
JSON_Object paramsJson = new JSON_Object();

char output[1024];

outputJson.SetString("matchid", g_sMatchID);

paramsJson.SetString("team", teamString);
paramsJson.SetString("name", name);

paramsJson.SetInt(STAT_KILLS, kv.GetNum(STAT_KILLS));
paramsJson.SetInt(STAT_DEATHS, kv.GetNum(STAT_DEATHS));
paramsJson.SetInt(STAT_ASSISTS, kv.GetNum(STAT_ASSISTS));
paramsJson.SetInt(STAT_FLASHBANG_ASSISTS, kv.GetNum(STAT_FLASHBANG_ASSISTS));
paramsJson.SetInt(STAT_TEAMKILLS, kv.GetNum(STAT_TEAMKILLS));
paramsJson.SetInt(STAT_SUICIDES, kv.GetNum(STAT_SUICIDES));
paramsJson.SetInt(STAT_DAMAGE, kv.GetNum(STAT_DAMAGE));
paramsJson.SetInt(STAT_HEADSHOT_KILLS, kv.GetNum(STAT_HEADSHOT_KILLS));
paramsJson.SetInt(STAT_ROUNDSPLAYED, kv.GetNum(STAT_ROUNDSPLAYED));
paramsJson.SetInt(STAT_BOMBDEFUSES, kv.GetNum(STAT_BOMBDEFUSES));
paramsJson.SetInt(STAT_BOMBPLANTS, kv.GetNum(STAT_BOMBPLANTS));
paramsJson.SetInt(STAT_1K, kv.GetNum(STAT_1K));
paramsJson.SetInt(STAT_2K, kv.GetNum(STAT_2K));
paramsJson.SetInt(STAT_3K, kv.GetNum(STAT_3K));
paramsJson.SetInt(STAT_4K, kv.GetNum(STAT_4K));
paramsJson.SetInt(STAT_5K, kv.GetNum(STAT_5K));
paramsJson.SetInt(STAT_V1, kv.GetNum(STAT_V1));
paramsJson.SetInt(STAT_V2, kv.GetNum(STAT_V2));
paramsJson.SetInt(STAT_V3, kv.GetNum(STAT_V3));
paramsJson.SetInt(STAT_V4, kv.GetNum(STAT_V4));
paramsJson.SetInt(STAT_V5, kv.GetNum(STAT_V5));
paramsJson.SetInt(STAT_FIRSTKILL_T, kv.GetNum(STAT_FIRSTKILL_T));
paramsJson.SetInt(STAT_FIRSTKILL_CT, kv.GetNum(STAT_FIRSTKILL_CT));
paramsJson.SetInt(STAT_FIRSTDEATH_T, kv.GetNum(STAT_FIRSTDEATH_T));
paramsJson.SetInt(STAT_FIRSTDEATH_CT, kv.GetNum(STAT_FIRSTDEATH_CT));
paramsJson.SetInt(STAT_TRADEKILL, kv.GetNum(STAT_TRADEKILL));
paramsJson.SetInt(STAT_KAST, kv.GetNum(STAT_KAST));
paramsJson.SetInt(STAT_CONTRIBUTION_SCORE, kv.GetNum(STAT_CONTRIBUTION_SCORE));

outputJson.SetObject("params", paramsJson);

outputJson.Encode(output, sizeof(output));

delete outputJson;
delete paramsJson;

Output:

{
  matchid: 'mage_match',
  params: {
    v5: 0,
    '1kill_rounds': 0,
    name: 'Bender',
    team: 'team2',
    roundsplayed: 1,
    '2kill_rounds': 0,
    assists: 0,
    '3kill_rounds': 0,
    v3: 0,
    suicides: 0,
    tradekill: 0,
    '4kill_rounds': 0,
    v1: 0,
    deaths: 1,
    '5kill_rounds': 0,
    firstkill_t: 0,
    firstdeath_ct: 0,
    bomb_defuses: 0,
    damage: 0,
    contribution_score: 0,
    kills: 0,
    v4: 0,
    headshot_kills: 0,
    v2: 0,
    mvp: 0,
    firstkill_ct: 0,
    firstdeath_t: 1,
    teamkills: 0,
    flashbang_assists: 0,
    bomb_plants: 0,
    kast: 0
  }
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.